Add cli-subbcomands to mange users

ocis-accounts [list|delete|update|add|inspect]

Implements UpdateMask for the update request. Changed server-handler accordingly.
The commands use service-discovery to discover the backend.
This commit is contained in:
Jörn Friedrich Dreyer
2020-07-21 14:22:19 +02:00
committed by Ilja Neumann
parent 229e5e01d7
commit 694fe677aa
12 changed files with 777 additions and 83 deletions

View File

@@ -0,0 +1,17 @@
Enhancement: Add early version of cli tools for user-management
Following commands are available:
list, ls List existing accounts
add, create, Create a new account
update Make changes to an existing account
remove, rm Removes an existing account
inspect Show detailed data on an existing account
See --help for details.
Note that not all account-attributes have an effect yet. This is due to ocis
being in an early development stage.
https://github.com/owncloud/ocis-accounts/pull/69
https://github.com/owncloud/product/issues/115

8
go.mod
View File

@@ -19,22 +19,24 @@ require (
github.com/golang/protobuf v1.4.2
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
github.com/jmhodges/levigo v1.0.0 // indirect
github.com/mennanov/fieldmask-utils v0.3.2
github.com/micro/cli/v2 v2.1.2
github.com/micro/go-micro/v2 v2.6.0
github.com/micro/protoc-gen-micro/v2 v2.3.0 // indirect
github.com/oklog/run v1.1.0
github.com/olekukonko/tablewriter v0.0.4
github.com/owncloud/ocis-pkg/v2 v2.2.2-0.20200602070144-cd0620668170
github.com/owncloud/ocis-settings v0.0.0-20200522101320-46ea31026363
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/restic/calens v0.2.0
github.com/rs/zerolog v1.18.0
github.com/spf13/viper v1.7.0
github.com/stretchr/testify v1.4.0
github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect
github.com/tredoe/osutil v1.0.5
golang.org/x/net v0.0.0-20200301022130-244492dfa37a
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 // indirect
google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece
google.golang.org/protobuf v1.25.0
gopkg.in/go-playground/assert.v1 v1.2.1
honnef.co/go/tools v0.0.1-2020.1.4 // indirect
)

24
go.sum
View File

@@ -175,13 +175,10 @@ github.com/cespare/xxhash/v2 v2.1.0 h1:yTUvW7Vhb89inJ+8irsUqiWjh8iT6sQPZiQzI6ReG
github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/cheggaaa/pb v1.0.28/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/cloudflare/cloudflare-go v0.10.2/go.mod h1:qhVI5MKwBGhdNU89ZRz2plgYutcJ5PCekLxXn56w6SY=
github.com/cloudflare/cloudflare-go v0.10.6/go.mod h1:dcRl7AXBH5Bf7QFTBVc3TRzwvotSeO4AlnMhuxORAX8=
github.com/cloudflare/cloudflare-go v0.10.9/go.mod h1:5TrsWH+3f4NV6WjtS5QFp+DifH81rph40gU374Sh0dQ=
github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=
github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
@@ -255,7 +252,6 @@ github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
@@ -597,18 +593,19 @@ github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mennanov/fieldmask-utils v0.3.2 h1:AkHXYBEOoyvocl8YhzoStATRnto5OH1PY4Rj78I5Cuc=
github.com/mennanov/fieldmask-utils v0.3.2/go.mod h1:JpaanSp6Ql5A8dGktEFxTmA9uBXmz3F+2LAXDZwiimU=
github.com/mholt/certmagic v0.7.5/go.mod h1:91uJzK5K8IWtYQqTi5R2tsxV1pCde+wdGfaRaOZi6aQ=
github.com/mholt/certmagic v0.8.3/go.mod h1:91uJzK5K8IWtYQqTi5R2tsxV1pCde+wdGfaRaOZi6aQ=
github.com/mholt/certmagic v0.9.1/go.mod h1:nu8jbsbtwK4205EDH/ZUMTKsfYpJA1Q7MKXHfgTihNw=
@@ -623,7 +620,6 @@ github.com/micro/go-micro v1.17.1/go.mod h1:klwUJL1gkdY1MHFyz+fFJXn52dKcty4hoe95
github.com/micro/go-micro v1.18.0 h1:gP70EZVHpJuUIT0YWth192JmlIci+qMOEByHm83XE9E=
github.com/micro/go-micro v1.18.0/go.mod h1:klwUJL1gkdY1MHFyz+fFJXn52dKcty4hoe95Mp571AA=
github.com/micro/go-micro/v2 v2.0.0/go.mod h1:v7QP5UhKRt37ixjJe8DouWmg0/eE6dltr5h0idJ9BpE=
github.com/micro/go-micro/v2 v2.5.1-0.20200417165434-16db76bee2fb/go.mod h1:qz2UT4UFdFVs+qUGMuDK3xuHgude1BgntqQ29sbpPlE=
github.com/micro/go-micro/v2 v2.6.0 h1:HH6uEqTu6pkBtAlwAqQW2sf33640iEa1s9puGIctpO0=
github.com/micro/go-micro/v2 v2.6.0/go.mod h1:60HMKlDN4ShZDJRrlgdcAmkCWNhQbYv+CDG3r7iLE34=
github.com/micro/go-plugins v1.5.1 h1:swcFD7ynCTUo98APqIEIbPu2XMd6yVGTnI8PqdnCwOQ=
@@ -633,12 +629,8 @@ github.com/micro/go-plugins/wrapper/trace/opencensus/v2 v2.0.1/go.mod h1:QrkcwcD
github.com/micro/mdns v0.3.0/go.mod h1:KJ0dW7KmicXU2BV++qkLlmHYcVv7/hHnbtguSWt9Aoc=
github.com/micro/micro v1.16.0 h1:qCZV20WoTOtJ1IyLU/a0A0BMSertfu+iOj/2AJ4Uvrk=
github.com/micro/micro v1.16.0/go.mod h1:TO5Ng0KidbfRYIxVM4Q3deZ0A+qwRyP9WeXp+k2fWNA=
github.com/micro/micro/v2 v2.5.1-0.20200418121137-24e9b206767c h1:0xGuo2yepDL8p+id/kXqVka+5iiOBSyfqOX01csnOYk=
github.com/micro/micro/v2 v2.5.1-0.20200418121137-24e9b206767c/go.mod h1:fqqaYbJGYzSBi7Ms2Adly7Xzw9+WIRBAucUjwGmYeFY=
github.com/micro/protoc-gen-micro v1.0.0 h1:qKh5S3I1RfenhIs5mqDFJLwRlRDlgin7XWiUKZbpwLM=
github.com/micro/protoc-gen-micro v1.0.0/go.mod h1:C8ij4DJhapBmypcT00AXdb0cZ675/3PqUO02buWWqbE=
github.com/micro/protoc-gen-micro/v2 v2.3.0 h1:PBbGeNh4BOy1w4eRdeo4yWJJNWGLnaJX6/h55I74EXE=
github.com/micro/protoc-gen-micro/v2 v2.3.0/go.mod h1:gcsUvKSTTTalq+pqdUbFS40OTsURpYgL5+yUguR1djk=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.3/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
@@ -704,7 +696,6 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nats-io/stan.go v0.5.0/go.mod h1:dYqB+vMN3C2F9pT1FRQpg9eHbjPj6mP0yYuyBNuXHZE=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/netdata/go-orchestrator v0.0.0-20190905093727-c793edba0e8f/go.mod h1:ECF8anFVCt/TfTIWVPgPrNaYJXtAtpAOF62ugDbw41A=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nlopes/slack v0.6.0/go.mod h1:JzQ9m3PMAqcpeCam7UaHSuBuupz7CmpjehYMayT6YOk=
@@ -724,7 +715,7 @@ github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DV
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/oleiade/reflections v1.0.0/go.mod h1:RbATFBbKYkVdqmSFtx13Bb/tVhR0lgOBXunWTZKeL4w=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.3/go.mod h1:YZeBtGzYYEsCHp2LST/u/0NDwGkRoBtmn1cIWCJiS6M=
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -931,7 +922,6 @@ github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGr
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
@@ -1069,11 +1059,9 @@ golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20191027093000-83d349e8ac1a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191109021931-daa7c04131f5/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191207000613-e7e4b65ae663/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -1219,6 +1207,8 @@ google.golang.org/genproto v0.0.0-20200420144010-e5e8543f8aeb h1:nAFaltAMbNVA0ri
google.golang.org/genproto v0.0.0-20200420144010-e5e8543f8aeb/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece h1:1YM0uhfumvoDu9sx8+RyWwTI63zoCQvI23IYFRlvte0=
google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
@@ -1230,6 +1220,7 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/DataDog/dd-trace-go.v1 v1.19.0/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg=
@@ -1268,7 +1259,6 @@ gopkg.in/ldap.v2 v2.5.1/go.mod h1:oI0cpe/D7HRtBQl8aTg+ZmzFUAvu4lsv3eLXMLGFxWk=
gopkg.in/ldap.v3 v3.1.0/go.mod h1:dQjCc0R0kfyFjIlWNMH1DORwUASZyDxo2Ry1B51dXaQ=
gopkg.in/ns1/ns1-go.v2 v2.0.0-20190730140822-b51389932cbc/go.mod h1:VV+3haRsgDiVLxyifmMBrBIuCWFBPYKbRssXB9z67Hw=
gopkg.in/olivere/elastic.v5 v5.0.82/go.mod h1:uhHoB4o3bvX5sorxBU29rPcmBQdV2Qfg0FBrx5D6pV0=
gopkg.in/olivere/elastic.v5 v5.0.83/go.mod h1:LXF6q9XNBxpMqrcgax95C6xyARXWbbCXUrtTxrNrxJI=
gopkg.in/redis.v3 v3.6.4/go.mod h1:6XeGv/CrsUFDU9aVbUdNykN7k1zVmoeg83KC9RbQfiU=
gopkg.in/resty.v1 v1.9.1/go.mod h1:vo52Hzryw9PnPHcJfPsBiFW62XhNx5OczbV9y+IMpgc=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=

View File

@@ -0,0 +1,56 @@
package command
import (
"fmt"
"github.com/micro/cli/v2"
"github.com/micro/go-micro/v2/client/grpc"
"github.com/owncloud/ocis-accounts/pkg/config"
"github.com/owncloud/ocis-accounts/pkg/flagset"
accounts "github.com/owncloud/ocis-accounts/pkg/proto/v0"
)
// AddAccount command creates a new account
func AddAccount(cfg *config.Config) *cli.Command {
a := &accounts.Account{
PasswordProfile: &accounts.PasswordProfile{},
}
return &cli.Command{
Name: "add",
Usage: "Create a new account",
Aliases: []string{"create", "a"},
Flags: flagset.AddAccountWithConfig(cfg, a),
Before: func(c *cli.Context) error {
// Write value of username to the flags beneath, as preferred name
// and on-premises-sam-account-name is probably confusing for users.
if username := c.String("username"); username != "" {
if !c.IsSet("on-premises-sam-account-name") {
if err := c.Set("on-premises-sam-account-name", username); err != nil {
return err
}
}
if !c.IsSet("preferred-name") {
if err := c.Set("preferred-name", username); err != nil {
return err
}
}
}
return nil
},
Action: func(c *cli.Context) error {
accSvcID := cfg.GRPC.Namespace + "." + cfg.Server.Name
accSvc := accounts.NewAccountsService(accSvcID, grpc.NewClient())
_, err := accSvc.CreateAccount(c.Context, &accounts.CreateAccountRequest{
Account: a,
})
if err != nil {
fmt.Println(fmt.Errorf("could not create account %w", err))
return err
}
return nil
}}
}

View File

@@ -0,0 +1,76 @@
package command
import (
"fmt"
"github.com/micro/cli/v2"
"github.com/micro/go-micro/v2/client/grpc"
tw "github.com/olekukonko/tablewriter"
"github.com/owncloud/ocis-accounts/pkg/config"
"github.com/owncloud/ocis-accounts/pkg/flagset"
accounts "github.com/owncloud/ocis-accounts/pkg/proto/v0"
"os"
"strconv"
)
// InspectAccount command shows detailed information about a specific account.
func InspectAccount(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "inspect",
Usage: "Show detailed data on an existing account",
ArgsUsage: "id",
Flags: flagset.InspectAccountWithConfig(cfg),
Action: func(c *cli.Context) error {
accServiceID := cfg.GRPC.Namespace + "." + cfg.Server.Name
if c.NArg() != 1 {
fmt.Println("Please provide a user-id")
os.Exit(1)
}
uid := c.Args().First()
accSvc := accounts.NewAccountsService(accServiceID, grpc.NewClient())
acc, err := accSvc.GetAccount(c.Context, &accounts.GetAccountRequest{
Id: uid,
})
if err != nil {
fmt.Println(fmt.Errorf("could not view account %w", err))
return err
}
buildAccountInspectTable(acc).Render()
return nil
}}
}
func buildAccountInspectTable(acc *accounts.Account) *tw.Table {
table := tw.NewWriter(os.Stdout)
table.SetAutoMergeCells(true)
table.AppendBulk([][]string{
{"ID", acc.Id},
{"Mail", acc.Mail},
{"DisplayName", acc.DisplayName},
{"PreferredName", acc.PreferredName},
{"AccountEnabled", strconv.FormatBool(acc.AccountEnabled)},
{"CreationType", acc.CreationType},
{"CreatedDateTime", acc.CreatedDateTime.String()},
{"Description", acc.Description},
{"ExternalUserState", acc.ExternalUserState},
{"UidNumber", fmt.Sprintf("%+d", acc.UidNumber)},
{"GidNumber", fmt.Sprintf("%+d", acc.GidNumber)},
{"IsResourceAccount", strconv.FormatBool(acc.IsResourceAccount)},
{"OnPremisesDistinguishedName", acc.OnPremisesDistinguishedName},
{"OnPremisesDomainName", acc.OnPremisesDomainName},
{"OnPremisesImmutableId", acc.OnPremisesImmutableId},
{"OnPremisesSamAccountName", acc.OnPremisesSamAccountName},
{"OnPremisesSecurityIdentifier", acc.OnPremisesSecurityIdentifier},
{"OnPremisesUserPrincipalName", acc.OnPremisesUserPrincipalName},
{"RefreshTokenValidFromDateTime", acc.RefreshTokensValidFromDateTime.String()},
})
// Merged cell with group memberships
for k := range acc.MemberOf {
table.Append([]string{"MemberOf", acc.MemberOf[k].DisplayName})
}
return table
}

View File

@@ -0,0 +1,50 @@
package command
import (
"fmt"
"github.com/micro/cli/v2"
"github.com/micro/go-micro/v2/client/grpc"
tw "github.com/olekukonko/tablewriter"
"github.com/owncloud/ocis-accounts/pkg/config"
"github.com/owncloud/ocis-accounts/pkg/flagset"
accounts "github.com/owncloud/ocis-accounts/pkg/proto/v0"
"os"
"strconv"
)
// ListAccounts command lists all accounts
func ListAccounts(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "list",
Usage: "List existing accounts",
Aliases: []string{"ls"},
Flags: flagset.ListAccountsWithConfig(cfg),
Action: func(c *cli.Context) error {
accSvcID := cfg.GRPC.Namespace + "." + cfg.Server.Name
accSvc := accounts.NewAccountsService(accSvcID, grpc.NewClient())
resp, err := accSvc.ListAccounts(c.Context, &accounts.ListAccountsRequest{})
if err != nil {
fmt.Println(fmt.Errorf("could not list accounts %w", err))
return err
}
buildAccountsListTable(resp.Accounts).Render()
return nil
}}
}
// buildAccountsListTable creates an ascii table for printing on the cli
func buildAccountsListTable(accs []*accounts.Account) *tw.Table {
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Id", "DisplayName", "Mail", "AccountEnabled"})
table.SetAutoFormatHeaders(false)
for _, acc := range accs {
table.Append([]string{
acc.Id,
acc.DisplayName,
acc.Mail,
strconv.FormatBool(acc.AccountEnabled)})
}
return table
}

View File

@@ -0,0 +1,39 @@
package command
import (
"fmt"
"github.com/micro/cli/v2"
"github.com/micro/go-micro/v2/client/grpc"
"github.com/owncloud/ocis-accounts/pkg/config"
"github.com/owncloud/ocis-accounts/pkg/flagset"
accounts "github.com/owncloud/ocis-accounts/pkg/proto/v0"
"os"
)
// RemoveAccount command deletes an existing account.
func RemoveAccount(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "remove",
Usage: "Removes an existing account",
ArgsUsage: "id",
Aliases: []string{"rm"},
Flags: flagset.RemoveAccountWithConfig(cfg),
Action: func(c *cli.Context) error {
accServiceID := cfg.GRPC.Namespace + "." + cfg.Server.Name
if c.NArg() != 1 {
fmt.Println("Please provide a user-id")
os.Exit(1)
}
uid := c.Args().First()
accSvc := accounts.NewAccountsService(accServiceID, grpc.NewClient())
_, err := accSvc.DeleteAccount(c.Context, &accounts.DeleteAccountRequest{Id: uid})
if err != nil {
fmt.Println(fmt.Errorf("could not delete account %w", err))
return err
}
return nil
}}
}

View File

@@ -42,6 +42,11 @@ func Execute() error {
Commands: []*cli.Command{
Server(cfg),
AddAccount(cfg),
UpdateAccount(cfg),
ListAccounts(cfg),
InspectAccount(cfg),
RemoveAccount(cfg),
},
}

View File

@@ -0,0 +1,87 @@
package command
import (
"errors"
"fmt"
"github.com/micro/cli/v2"
"github.com/micro/go-micro/v2/client/grpc"
"github.com/owncloud/ocis-accounts/pkg/config"
"github.com/owncloud/ocis-accounts/pkg/flagset"
accounts "github.com/owncloud/ocis-accounts/pkg/proto/v0"
"google.golang.org/genproto/protobuf/field_mask"
)
// UpdateAccount command for modifying accounts including password policies
func UpdateAccount(cfg *config.Config) *cli.Command {
a := &accounts.Account{
PasswordProfile: &accounts.PasswordProfile{},
}
return &cli.Command{
Name: "update",
Usage: "Make changes to an existing account",
ArgsUsage: "id",
Flags: flagset.UpdateAccountWithConfig(cfg, a),
Before: func(c *cli.Context) error {
if len(c.StringSlice("password_policies")) > 0 {
// StringSliceFlag doesn't support Destination
a.PasswordProfile.PasswordPolicies = c.StringSlice("password_policies")
}
if c.NArg() != 1 {
return errors.New("missing account-id")
}
if c.NumFlags() == 0 {
return errors.New("missing attribute-flags for update")
}
return nil
},
Action: func(c *cli.Context) error {
a.Id = c.Args().First()
accSvcID := cfg.GRPC.Namespace + "." + cfg.Server.Name
accSvc := accounts.NewAccountsService(accSvcID, grpc.NewClient())
_, err := accSvc.UpdateAccount(c.Context, &accounts.UpdateAccountRequest{
Account: a,
UpdateMask: buildAccUpdateMask(c.FlagNames()),
})
if err != nil {
fmt.Println(fmt.Errorf("could not update account %w", err))
return err
}
return nil
}}
}
// buildAccUpdateMask by mapping passed update flags to account fieldNames.
//
// The UpdateMask is passed with the update-request to the server so that
// only the modified values are transferred.
func buildAccUpdateMask(setFlags []string) *field_mask.FieldMask {
var flagToPath = map[string]string{
"enabled": "AccountEnabled",
"displayname": "DisplayName",
"preferred-name": "PreferredName",
"uidnumber": "UidNumber",
"gidnumber": "GidNumber",
"mail": "Mail",
"description": "Description",
"password": "PasswordProfile.Password",
"password-policies": "PasswordProfile.PasswordPolicies",
"force-password-change": "PasswordProfile.ForceChangePasswordNextSignIn",
"force-password-change-mfa": "PasswordProfile.ForceChangePasswordNextSignInWithMfa",
"on-premises-sam-account-name": "OnPremisesSamAccountName",
}
updatedPaths := make([]string, 0)
for _, v := range setFlags {
if _, ok := flagToPath[v]; ok {
updatedPaths = append(updatedPaths, flagToPath[v])
}
}
return &field_mask.FieldMask{Paths: updatedPaths}
}

View File

@@ -3,6 +3,7 @@ package flagset
import (
"github.com/micro/cli/v2"
"github.com/owncloud/ocis-accounts/pkg/config"
accounts "github.com/owncloud/ocis-accounts/pkg/proto/v0"
)
// RootWithConfig applies cfg to the root flagset
@@ -88,8 +89,240 @@ func ServerWithConfig(cfg *config.Config) []cli.Flag {
Name: "asset-path",
Value: "",
Usage: "Path to custom assets",
EnvVars: []string{"HELLO_ASSET_PATH"},
EnvVars: []string{"ACCOUNTS_ASSET_PATH"},
Destination: &cfg.Asset.Path,
},
}
}
// UpdateAccountWithConfig applies update command flags to cfg
func UpdateAccountWithConfig(cfg *config.Config, a *accounts.Account) []cli.Flag {
if a.PasswordProfile == nil {
a.PasswordProfile = &accounts.PasswordProfile{}
}
return []cli.Flag{
&cli.StringFlag{
Name: "grpc-namespace",
Value: "com.owncloud.api",
Usage: "Set the base namespace for the grpc namespace",
EnvVars: []string{"ACCOUNTS_GRPC_NAMESPACE"},
Destination: &cfg.GRPC.Namespace,
},
&cli.StringFlag{
Name: "name",
Value: "accounts",
Usage: "service name",
EnvVars: []string{"ACCOUNTS_NAME"},
Destination: &cfg.Server.Name,
},
&cli.BoolFlag{
Name: "enabled",
Usage: "Enable the account",
Destination: &a.AccountEnabled,
},
&cli.StringFlag{
Name: "displayname",
Usage: "Set the displayname for the account",
Destination: &a.DisplayName,
},
&cli.StringFlag{
Name: "preferred-name",
Usage: "Set the preferred-name for the account",
Destination: &a.PreferredName,
},
&cli.StringFlag{
Name: "on-premises-sam-account-name",
Usage: "Set the on-premises-sam-account-name",
Destination: &a.OnPremisesSamAccountName,
},
&cli.Int64Flag{
Name: "uidnumber",
Usage: "Set the uidnumber for the account",
Destination: &a.UidNumber,
},
&cli.Int64Flag{
Name: "gidnumber",
Usage: "Set the gidnumber for the account",
Destination: &a.GidNumber,
},
&cli.StringFlag{
Name: "mail",
Usage: "Set the mail for the account",
Destination: &a.Mail,
},
&cli.StringFlag{
Name: "description",
Usage: "Set the description for the account",
Destination: &a.Description,
},
&cli.StringFlag{
Name: "password",
Usage: "Set the password for the account",
Destination: &a.PasswordProfile.Password,
// TODO read password from ENV?
},
&cli.StringSliceFlag{
Name: "password-policies",
Usage: "Possible policies: DisableStrongPassword, DisablePasswordExpiration",
},
&cli.BoolFlag{
Name: "force-password-change",
Usage: "Force password change on next sign-in",
Destination: &a.PasswordProfile.ForceChangePasswordNextSignIn,
},
&cli.BoolFlag{
Name: "force-password-change-mfa",
Usage: "Force password change on next sign-in with mfa",
Destination: &a.PasswordProfile.ForceChangePasswordNextSignInWithMfa,
},
}
}
// AddAccountWithConfig applies create command flags to cfg
func AddAccountWithConfig(cfg *config.Config, a *accounts.Account) []cli.Flag {
if a.PasswordProfile == nil {
a.PasswordProfile = &accounts.PasswordProfile{}
}
return []cli.Flag{
&cli.StringFlag{
Name: "grpc-namespace",
Value: "com.owncloud.api",
Usage: "Set the base namespace for the grpc namespace",
EnvVars: []string{"ACCOUNTS_GRPC_NAMESPACE"},
Destination: &cfg.GRPC.Namespace,
},
&cli.StringFlag{
Name: "name",
Value: "accounts",
Usage: "service name",
EnvVars: []string{"ACCOUNTS_NAME"},
Destination: &cfg.Server.Name,
},
&cli.BoolFlag{
Name: "enabled",
Usage: "Enable the account",
Destination: &a.AccountEnabled,
},
&cli.StringFlag{
Name: "displayname",
Usage: "Set the displayname for the account",
Destination: &a.DisplayName,
},
&cli.StringFlag{
Name: "username",
Usage: "Username will be written to preferred-name and on_premises_sam_account_name",
},
&cli.StringFlag{
Name: "preferred-name",
Usage: "Set the preferred-name for the account",
Destination: &a.PreferredName,
},
&cli.StringFlag{
Name: "on-premises-sam-account-name",
Usage: "Set the on-premises-sam-account-name",
Destination: &a.OnPremisesSamAccountName,
},
&cli.Int64Flag{
Name: "uidnumber",
Usage: "Set the uidnumber for the account",
Destination: &a.UidNumber,
},
&cli.Int64Flag{
Name: "gidnumber",
Usage: "Set the gidnumber for the account",
Destination: &a.GidNumber,
},
&cli.StringFlag{
Name: "mail",
Usage: "Set the mail for the account",
Destination: &a.Mail,
},
&cli.StringFlag{
Name: "description",
Usage: "Set the description for the account",
Destination: &a.Description,
},
&cli.StringFlag{
Name: "password",
Usage: "Set the password for the account",
Destination: &a.PasswordProfile.Password,
// TODO read password from ENV?
},
&cli.StringSliceFlag{
Name: "password-policies",
Usage: "Possible policies: DisableStrongPassword, DisablePasswordExpiration",
},
&cli.BoolFlag{
Name: "force-password-change",
Usage: "Force password change on next sign-in",
Destination: &a.PasswordProfile.ForceChangePasswordNextSignIn,
},
&cli.BoolFlag{
Name: "force-password-change-mfa",
Usage: "Force password change on next sign-in with mfa",
Destination: &a.PasswordProfile.ForceChangePasswordNextSignInWithMfa,
},
}
}
// ListAccountsWithConfig applies list command flags to cfg
func ListAccountsWithConfig(cfg *config.Config) []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "grpc-namespace",
Value: "com.owncloud.api",
Usage: "Set the base namespace for the grpc namespace",
EnvVars: []string{"ACCOUNTS_GRPC_NAMESPACE"},
Destination: &cfg.GRPC.Namespace,
},
&cli.StringFlag{
Name: "name",
Value: "accounts",
Usage: "service name",
EnvVars: []string{"ACCOUNTS_NAME"},
Destination: &cfg.Server.Name,
},
}
}
// RemoveAccountWithConfig applies remove command flags to cfg
func RemoveAccountWithConfig(cfg *config.Config) []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "grpc-namespace",
Value: "com.owncloud.api",
Usage: "Set the base namespace for the grpc namespace",
EnvVars: []string{"ACCOUNTS_GRPC_NAMESPACE"},
Destination: &cfg.GRPC.Namespace,
},
&cli.StringFlag{
Name: "name",
Value: "accounts",
Usage: "service name",
EnvVars: []string{"ACCOUNTS_NAME"},
Destination: &cfg.Server.Name,
},
}
}
// InspectAccountWithConfig applies inspect command flags to cfg
func InspectAccountWithConfig(cfg *config.Config) []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "grpc-namespace",
Value: "com.owncloud.api",
Usage: "Set the base namespace for the grpc namespace",
EnvVars: []string{"ACCOUNTS_GRPC_NAMESPACE"},
Destination: &cfg.GRPC.Namespace,
},
&cli.StringFlag{
Name: "name",
Value: "accounts",
Usage: "service name",
EnvVars: []string{"ACCOUNTS_NAME"},
Destination: &cfg.Server.Name,
},
}
}

View File

@@ -2,7 +2,10 @@ package proto_test
import (
context "context"
"errors"
"fmt"
"google.golang.org/genproto/protobuf/field_mask"
"google.golang.org/protobuf/types/known/timestamppb"
"log"
"os"
"path/filepath"
@@ -17,6 +20,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/golang/protobuf/ptypes/empty"
merrors "github.com/micro/go-micro/v2/errors"
)
var service = grpc.Service{}
@@ -949,3 +953,55 @@ func TestListMembersEmptyGroup(t *testing.T) {
cleanUp(t)
}
func TestAccountUpdateMask(t *testing.T) {
createAccount(t, "user1")
user1 := getAccount("user1")
client := service.Client()
req := &proto.UpdateAccountRequest{
// We only want to update the display-name, rest should be ignored
UpdateMask: &field_mask.FieldMask{Paths: []string{"DisplayName"}},
Account: &proto.Account{
Id: user1.Id,
DisplayName: "ShouldBeUpdated",
PreferredName: "ShouldStaySame",
}}
cl := proto.NewAccountsService("com.owncloud.api.accounts", client)
res, err := cl.UpdateAccount(context.Background(), req)
checkError(t, err)
assert.Equal(t, "ShouldBeUpdated", res.DisplayName)
assert.Equal(t, user1.PreferredName, res.PreferredName)
cleanUp(t)
}
func TestAccountUpdateReadOnlyField(t *testing.T) {
createAccount(t, "user1")
user1 := getAccount("user1")
client := service.Client()
req := &proto.UpdateAccountRequest{
// We only want to update the display-name, rest should be ignored
UpdateMask: &field_mask.FieldMask{Paths: []string{"CreatedDateTime"}},
Account: &proto.Account{
Id: user1.Id,
CreatedDateTime: timestamppb.Now(),
}}
cl := proto.NewAccountsService("com.owncloud.api.accounts", client)
res, err := cl.UpdateAccount(context.Background(), req)
assert.Nil(t, res)
assert.Error(t, err)
var e *merrors.Error
if errors.As(err, &e) {
assert.EqualValues(t, 400, e.Code)
assert.Equal(t, "Bad Request", e.Status)
} else {
t.Fatal("Unexpected error type")
}
cleanUp(t)
}

View File

@@ -4,6 +4,9 @@ import (
"context"
"encoding/json"
"fmt"
fieldmask_utils "github.com/mennanov/fieldmask-utils"
"github.com/rs/zerolog"
"google.golang.org/genproto/protobuf/field_mask"
"io/ioutil"
"os"
"path/filepath"
@@ -28,7 +31,6 @@ import (
)
func (s Service) indexAccounts(path string) (err error) {
var f *os.File
if f, err = os.Open(path); err != nil {
s.log.Error().Err(err).Str("dir", path).Msg("could not open accounts folder")
@@ -81,14 +83,6 @@ func (s Service) loadAccount(id string, a *proto.Account) (err error) {
return
}
// loggableAccount redacts the password from the account
func loggableAccount(a *proto.Account) *proto.Account {
if a != nil && a.PasswordProfile != nil {
a.PasswordProfile.Password = "***REMOVED***"
}
return a
}
func (s Service) writeAccount(a *proto.Account) (err error) {
// leave only the group id
@@ -207,7 +201,6 @@ func (s Service) ListAccounts(ctx context.Context, in *proto.ListAccountsRequest
out.Accounts = make([]*proto.Account, 0)
for _, hit := range searchResult.Hits {
a := &proto.Account{}
if err = s.loadAccount(hit.ID, a); err != nil {
s.log.Error().Err(err).Str("account", hit.ID).Msg("could not load account, skipping")
@@ -217,11 +210,12 @@ func (s Service) ListAccounts(ctx context.Context, in *proto.ListAccountsRequest
if a.PasswordProfile != nil {
currentHash = a.PasswordProfile.Password
}
s.log.Debug().Interface("account", loggableAccount(a)).Msg("found account")
s.debugLogAccount(a).Msg("found account")
if password != "" {
if a.PasswordProfile == nil {
s.log.Debug().Interface("account", loggableAccount(a)).Msg("no password profile")
s.debugLogAccount(a).Msg("no password profile")
return merrors.Unauthorized(s.id, "invalid password")
}
if !s.passwordIsValid(currentHash, password) {
@@ -233,7 +227,6 @@ func (s Service) ListAccounts(ctx context.Context, in *proto.ListAccountsRequest
s.expandMemberOf(a)
// remove password before returning
if a.PasswordProfile != nil {
a.PasswordProfile.Password = ""
}
@@ -255,7 +248,8 @@ func (s Service) GetAccount(c context.Context, in *proto.GetAccountRequest, out
s.log.Error().Err(err).Str("id", id).Msg("could not load account")
return
}
s.log.Debug().Interface("account", loggableAccount(out)).Msg("found account")
s.debugLogAccount(out).Msg("found account")
// TODO add groups if requested
// if in.FieldMask ...
@@ -272,40 +266,48 @@ func (s Service) GetAccount(c context.Context, in *proto.GetAccountRequest, out
// CreateAccount implements the AccountsServiceHandler interface
func (s Service) CreateAccount(c context.Context, in *proto.CreateAccountRequest, out *proto.Account) (err error) {
var id string
if in.Account == nil {
var acc = in.Account
if acc == nil {
return merrors.BadRequest(s.id, "account missing")
}
if in.Account.Id == "" {
in.Account.Id = uuid.Must(uuid.NewV4()).String()
if acc.Id == "" {
acc.Id = uuid.Must(uuid.NewV4()).String()
}
if !s.isValidUsername(in.Account.PreferredName) {
return merrors.BadRequest(s.id, "preferred_name '%s' must be at least the local part of an email", in.Account.PreferredName)
if !s.isValidUsername(acc.PreferredName) {
return merrors.BadRequest(s.id, "preferred_name '%s' must be at least the local part of an email", acc.PreferredName)
}
if !s.isValidEmail(in.Account.Mail) {
return merrors.BadRequest(s.id, "mail '%s' must be a valid email", in.Account.Mail)
if !s.isValidEmail(acc.Mail) {
return merrors.BadRequest(s.id, "mail '%s' must be a valid email", acc.Mail)
}
if id, err = cleanupID(in.Account.Id); err != nil {
if id, err = cleanupID(acc.Id); err != nil {
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
}
if in.Account.PasswordProfile != nil && in.Account.PasswordProfile.Password != "" {
// encrypt password
c := crypt.New(crypt.SHA512)
if in.Account.PasswordProfile.Password, err = c.Generate([]byte(in.Account.PasswordProfile.Password), nil); err != nil {
s.log.Error().Err(err).Str("id", id).Interface("account", loggableAccount(in.Account)).Msg("could not hash password")
return merrors.InternalServerError(s.id, "could not hash password: %v", err.Error())
if acc.PasswordProfile != nil {
if acc.PasswordProfile.Password != "" {
// encrypt password
c := crypt.New(crypt.SHA512)
if acc.PasswordProfile.Password, err = c.Generate([]byte(acc.PasswordProfile.Password), nil); err != nil {
s.log.Error().Err(err).Str("id", id).Msg("could not hash password")
return merrors.InternalServerError(s.id, "could not hash password: %v", err.Error())
}
}
if err := passwordPoliciesValid(acc.PasswordProfile.PasswordPolicies); err != nil {
return merrors.BadRequest(s.id, "%s", err)
}
}
// extract group id
// TODO groups should be ignored during create, use groups.AddMember? return error?
if err = s.writeAccount(in.Account); err != nil {
s.log.Error().Err(err).Interface("account", loggableAccount(in.Account)).Msg("could not persist new account")
if err = s.writeAccount(acc); err != nil {
s.log.Error().Err(err).Str("id", id).Msg("could not persist new account")
s.debugLogAccount(acc).Msg("could not persist new account")
return
}
if err = s.indexAccount(in.Account.Id); err != nil {
if err = s.indexAccount(acc.Id); err != nil {
return merrors.InternalServerError(s.id, "could not index new account: %v", err.Error())
}
@@ -323,23 +325,19 @@ func (s Service) UpdateAccount(c context.Context, in *proto.UpdateAccountRequest
if in.Account.Id == "" {
return merrors.BadRequest(s.id, "account id missing")
}
if !s.isValidUsername(in.Account.PreferredName) {
return merrors.BadRequest(s.id, "preferred_name '%s' must be at least the local part of an email", in.Account.PreferredName)
}
if !s.isValidEmail(in.Account.Mail) {
return merrors.BadRequest(s.id, "mail '%s' must be a valid email", in.Account.Mail)
}
if id, err = cleanupID(in.Account.Id); err != nil {
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
}
path := filepath.Join(s.Config.Server.AccountsDataPath, "accounts", id)
if err = s.loadAccount(id, out); err != nil {
s.log.Error().Err(err).Str("id", id).Msg("could not load account")
return
}
s.log.Debug().Interface("account", loggableAccount(out)).Msg("found account")
s.debugLogAccount(out).Msg("found account")
t := time.Now()
tsnow := &timestamppb.Timestamp{
@@ -347,17 +345,14 @@ func (s Service) UpdateAccount(c context.Context, in *proto.UpdateAccountRequest
Nanos: int32(t.Nanosecond()),
}
// id read-only
out.AccountEnabled = in.Account.AccountEnabled
out.IsResourceAccount = in.Account.IsResourceAccount
// creation-type read only
out.Identities = in.Account.Identities
out.DisplayName = in.Account.DisplayName
out.PreferredName = in.Account.PreferredName
out.UidNumber = in.Account.UidNumber
out.GidNumber = in.Account.GidNumber
out.Mail = in.Account.Mail // read only?
out.Description = in.Account.Description
validMask, err := validateUpdate(in.UpdateMask, updatableAccountPaths)
if err != nil {
return merrors.BadRequest(s.id, "%s", err)
}
if err := fieldmask_utils.StructToStruct(validMask, in.Account, out); err != nil {
return merrors.InternalServerError(s.id, "%s", err)
}
if in.Account.PasswordProfile != nil {
if out.PasswordProfile == nil {
@@ -367,39 +362,39 @@ func (s Service) UpdateAccount(c context.Context, in *proto.UpdateAccountRequest
// encrypt password
c := crypt.New(crypt.SHA512)
if out.PasswordProfile.Password, err = c.Generate([]byte(in.Account.PasswordProfile.Password), nil); err != nil {
s.log.Error().Err(err).Str("id", id).Interface("account", loggableAccount(in.Account)).Msg("could not hash password")
in.Account.PasswordProfile.Password = ""
s.log.Error().Err(err).Str("id", id).Msg("could not hash password")
return merrors.InternalServerError(s.id, "could not hash password: %v", err.Error())
}
in.Account.PasswordProfile.Password = ""
}
if err := passwordPoliciesValid(in.Account.PasswordProfile.PasswordPolicies); err != nil {
return merrors.BadRequest(s.id, "%s", err)
}
// lastPasswordChangeDateTime calculated, see password
out.PasswordProfile.PasswordPolicies = in.Account.PasswordProfile.PasswordPolicies
out.PasswordProfile.ForceChangePasswordNextSignIn = in.Account.PasswordProfile.ForceChangePasswordNextSignIn
out.PasswordProfile.ForceChangePasswordNextSignInWithMfa = in.Account.PasswordProfile.ForceChangePasswordNextSignInWithMfa
out.PasswordProfile.LastPasswordChangeDateTime = tsnow
}
// memberOf read only
// createdDateTime read only
// deleteDateTime read only
// out.RefreshTokensValidFromDateTime TODO use to invalidate all existing sessions
// out.SignInSessionsValidFromDateTime TODO use to invalidate all existing sessions
out.OnPremisesSyncEnabled = in.Account.OnPremisesSyncEnabled
out.OnPremisesSamAccountName = in.Account.OnPremisesSamAccountName
// ... TODO on prem for sync
if out.ExternalUserState != in.Account.ExternalUserState {
out.ExternalUserState = in.Account.ExternalUserState
out.ExternalUserStateChangeDateTime = tsnow
}
// out.RefreshTokensValidFromDateTime TODO use to invalidate all existing sessions
// out.SignInSessionsValidFromDateTime TODO use to invalidate all existing sessions
if err = s.writeAccount(out); err != nil {
s.log.Error().Err(err).Interface("account", loggableAccount(out)).Msg("could not persist updated account")
s.log.Error().Err(err).Str("id", out.Id).Msg("could not persist updated account")
return
}
if err = s.indexAccount(id); err != nil {
s.log.Error().Err(err).Str("id", id).Str("path", path).Interface("account", loggableAccount(out)).Msg("could not index new account")
s.log.Error().Err(err).Str("id", id).Str("path", path).Msg("could not index new account")
return merrors.InternalServerError(s.id, "could not index updated account: %v", err.Error())
}
@@ -411,6 +406,25 @@ func (s Service) UpdateAccount(c context.Context, in *proto.UpdateAccountRequest
return
}
// whitelist of all paths/fields which can be updated by clients
var updatableAccountPaths = map[string]struct{}{
"AccountEnabled": {},
"IsResourceAccount": {},
"Identities": {},
"DisplayName": {},
"PreferredName": {},
"UidNumber": {},
"GidNumber": {},
"Description": {},
"Mail": {}, // read only?,
"PasswordProfile.Password": {},
"PasswordProfile.PasswordPolicies": {},
"PasswordProfile.ForceChangePasswordNextSignIn": {},
"PasswordProfile.ForceChangePasswordNextSignInWithMfa": {},
"OnPremisesSyncEnabled": {},
"OnPremisesSamAccountName": {},
}
// DeleteAccount implements the AccountsServiceHandler interface
func (s Service) DeleteAccount(c context.Context, in *proto.DeleteAccountRequest, out *empty.Empty) (err error) {
var id string
@@ -471,3 +485,72 @@ func (s Service) isValidEmail(e string) bool {
}
return emailRegex.MatchString(e)
}
const (
policyDisableStrongPassword = "DisableStrongPassword"
policyDisablePasswordExpiration = "DisablePasswordExpiration"
)
func passwordPoliciesValid(policies []string) error {
for _, v := range policies {
if v != policyDisableStrongPassword && v != policyDisablePasswordExpiration {
return fmt.Errorf("invalid password-policy %s", v)
}
}
return nil
}
// validateUpdate takes a update field-mask and validates it against a whitelist of updatable paths.
// Returns a FieldFilter on success which can be passed to the fieldmask_utils..StructToStruct. An error is returned
// if the mask tries to update no whitelisted fields.
//
// Given an empty or nil mask we assume that the client wants to update all whitelisted fields.
//
func validateUpdate(mask *field_mask.FieldMask, updatablePaths map[string]struct{}) (fieldmask_utils.FieldFilterContainer, error) {
nop := func(s string) string { return s }
// Assume that the client wants to update all updatable path if
// no field-mask is given, so we create a mask with all paths
if mask == nil || len(mask.Paths) == 0 {
paths := make([]string, 0, len(updatablePaths))
for fieldName := range updatablePaths {
paths = append(paths, fieldName)
}
return fieldmask_utils.MaskFromPaths(paths, nop)
}
// Check that only allowed fields are updated
for _, v := range mask.Paths {
if _, ok := updatablePaths[v]; !ok {
return nil, fmt.Errorf("can not update field %s, either unknown or readonly", v)
}
}
return fieldmask_utils.MaskFromPaths(mask.Paths, nop)
}
// debugLogAccount returns a debug-log event with detailed account-info, and filtered password data
func (s Service) debugLogAccount(a *proto.Account) *zerolog.Event {
return s.log.Debug().Fields(map[string]interface{}{
"Id": a.Id,
"Mail": a.Mail,
"DisplayName": a.DisplayName,
"AccountEnabled": a.AccountEnabled,
"IsResourceAccount": a.IsResourceAccount,
"Identities": a.Identities,
"PreferredName": a.PreferredName,
"UidNumber": a.UidNumber,
"GidNumber": a.GidNumber,
"Description": a.Description,
"OnPremisesSyncEnabled": a.OnPremisesSyncEnabled,
"OnPremisesSamAccountName": a.OnPremisesSamAccountName,
"OnPremisesUserPrincipalName": a.OnPremisesUserPrincipalName,
"OnPremisesSecurityIdentifier": a.OnPremisesSecurityIdentifier,
"OnPremisesDistinguishedName": a.OnPremisesDistinguishedName,
"OnPremisesLastSyncDateTime": a.OnPremisesLastSyncDateTime,
"MemberOf": a.MemberOf,
"CreatedDateTime": a.CreatedDateTime,
"DeletedDateTime": a.DeletedDateTime,
})
}