mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-05 11:51:16 -06:00
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:
committed by
Ilja Neumann
parent
229e5e01d7
commit
694fe677aa
17
changelog/unreleased/add-cli-user-management.md
Normal file
17
changelog/unreleased/add-cli-user-management.md
Normal 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
8
go.mod
@@ -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
24
go.sum
@@ -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=
|
||||
|
||||
56
pkg/command/add_account.go
Normal file
56
pkg/command/add_account.go
Normal 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
|
||||
}}
|
||||
}
|
||||
76
pkg/command/inspect_account.go
Normal file
76
pkg/command/inspect_account.go
Normal 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
|
||||
}
|
||||
50
pkg/command/list_accounts.go
Normal file
50
pkg/command/list_accounts.go
Normal 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
|
||||
}
|
||||
39
pkg/command/remove_account.go
Normal file
39
pkg/command/remove_account.go
Normal 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
|
||||
}}
|
||||
}
|
||||
@@ -42,6 +42,11 @@ func Execute() error {
|
||||
|
||||
Commands: []*cli.Command{
|
||||
Server(cfg),
|
||||
AddAccount(cfg),
|
||||
UpdateAccount(cfg),
|
||||
ListAccounts(cfg),
|
||||
InspectAccount(cfg),
|
||||
RemoveAccount(cfg),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
87
pkg/command/update_account.go
Normal file
87
pkg/command/update_account.go
Normal 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}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 := ×tamppb.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,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user