diff --git a/changelog/unreleased/graph-api-user-groups.md b/changelog/unreleased/graph-api-user-groups.md new file mode 100644 index 000000000..76a84d9f6 --- /dev/null +++ b/changelog/unreleased/graph-api-user-groups.md @@ -0,0 +1,6 @@ +Enhancement: Introduce User and Group Management capabilities on GraphAPI + +The GraphAPI LDAP Backend is now able to add/modify and delete Users and Groups + +https://github.com/owncloud/ocis/pull/2947 +https://github.com/owncloud/ocis/pull/2996 diff --git a/go.mod b/go.mod index 6797890fb..0d089f184 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.3 github.com/iancoleman/strcase v0.2.0 github.com/justinas/alice v1.2.0 + github.com/libregraph/idm v0.3.1-0.20220120080913-d0003ade935e github.com/libregraph/lico v0.53.1 github.com/mennanov/fieldmask-utils v0.5.0 github.com/mitchellh/mapstructure v1.4.3 @@ -190,7 +191,7 @@ require ( github.com/nats-io/nkeys v0.3.0 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/nxadm/tail v1.4.8 // indirect - github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc // indirect + github.com/orcaman/concurrent-map v1.0.0 // indirect github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pkg/xattr v0.4.4 // indirect diff --git a/go.sum b/go.sum index 3dc4f48de..63a9a8a65 100644 --- a/go.sum +++ b/go.sum @@ -120,6 +120,7 @@ github.com/RoaringBitmap/roaring v0.9.4 h1:ckvZSX5gwCRaJYBNe7syNawCU5oruY9gQmjXl github.com/RoaringBitmap/roaring v0.9.4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/Songmu/prompter v0.5.0/go.mod h1:S4Eg25l60kPlnfB2ttFVpvBKYw7RKJexzB3gzpAansY= github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= @@ -132,7 +133,9 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387/go.mod h1:GuR5j/NW7AU7tDAQUDGCtpiPxWIOy/c3kiRDnlwiCHc= github.com/aliyun/alibaba-cloud-sdk-go v1.61.976/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA= +github.com/amoghe/go-crypt v0.0.0-20191109212615-b2ff80594b7f/go.mod h1:eFiR01PwTcpbzXtdMces7zxg6utvFM5puiWHpWB8D/k= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= @@ -352,6 +355,7 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8 github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/dnsimple/dnsimple-go v0.63.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg= github.com/docker/docker v20.10.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= @@ -412,6 +416,8 @@ github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aev github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/go-acme/lego/v4 v4.4.0/go.mod h1:l3+tFUFZb590dWcqhWZegynUthtaHJbG2fevUpoOOE0= +github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-asn1-ber/asn1-ber v1.4.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.3 h1:u7utq56RUFiynqUzgVMFDymapcOtQ/MZkh3H4QYkxag= github.com/go-asn1-ber/asn1-ber v1.5.3/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= @@ -442,8 +448,10 @@ github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.2.0 h1:7i2K3eKTos3Vc0enKCfnVcgHh2olr/MyfboYq7cAcFw= github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-ldap/ldap/v3 v3.1.7/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= github.com/go-ldap/ldap/v3 v3.4.1 h1:fU/0xli6HY02ocbMuozHAYsaHLcnkLjvho2r5a34BUU= github.com/go-ldap/ldap/v3 v3.4.1/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg= +github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3/go.mod h1:ZXFhGda43Z2TVbfGZefXyMJzsDHhCh0go3bZUcwTx7o= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= @@ -888,6 +896,8 @@ github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvf github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/libregraph/idm v0.3.1-0.20220120080913-d0003ade935e h1:iO7Gf/cMCn2sn28CoWsHUQ/O7VfH0DAJrhHyTE/xQk4= +github.com/libregraph/idm v0.3.1-0.20220120080913-d0003ade935e/go.mod h1:gdDXfHsvrrmoNwqcXroOGIi2+Lrx5iAipbpVwR7goeY= github.com/libregraph/lico v0.53.1 h1:c5JYFg0mrIgk6Ktj77rNJ2K8X01hs28JJ9Vw1N1iMAk= github.com/libregraph/lico v0.53.1/go.mod h1:EXf6Y9s9TygW6unSXosBpVXWUURoN2sPX0053o4zVfQ= github.com/linode/linodego v0.25.3/go.mod h1:GSBKPpjoQfxEfryoCRcgkuUOCuVtGHWhzI8OMdycNTE= @@ -1086,8 +1096,9 @@ github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFSt github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/oracle/oci-go-sdk v24.3.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888= -github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc h1:Ak86L+yDSOzKFa7WM5bf5itSOo1e3Xh8bm5YCMUXIjQ= github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= +github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY= +github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA= github.com/owncloud/libre-graph-api-go v0.9.0 h1:hMgjztJRCe1kRYqZ+w0paMFFzeUUpBX2o1TPNMNTmdU= github.com/owncloud/libre-graph-api-go v0.9.0/go.mod h1:579sFrPP7aP24LZXGPopLfvE+hAka/2DYHk0+Ij+w+U= @@ -1134,6 +1145,7 @@ github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQ github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_model v0.0.0-20170216185247-6f3806018612/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= @@ -1253,6 +1265,7 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spacewander/go-suffix-tree v0.0.0-20191010040751-0865e368c784/go.mod h1:ff/5myEGgtsAwf26goQCO905GrEm5ugEZSd6OWTsrhM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= @@ -1299,6 +1312,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/studio-b12/gowebdav v0.0.0-20211109083228-3f8721cd4b6f/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= github.com/thanhpk/randstr v1.0.4 h1:IN78qu/bR+My+gHCvMEXhR/i5oriVHcTB/BJJIRTsNo= github.com/thanhpk/randstr v1.0.4/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U= github.com/thejerf/suture/v4 v4.0.1 h1:CLnC1wxLAiHA5zTbbvhSWMupVuGe5ZJ7YddWE3lvb4M= @@ -1307,6 +1321,7 @@ github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhV github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/transip/gotransip/v6 v6.2.0/go.mod h1:pQZ36hWWRahCUXkFWlx9Hs711gLd8J4qdgLdRzmtY+g= +github.com/trustelem/zxcvbn v1.0.1/go.mod h1:zonUyKeh7sw6psPf/e3DtRqkRyZvAbOfjNz/aO7YQ5s= github.com/tus/tusd v1.1.0/go.mod h1:3DWPOdeCnjBwKtv98y5dSws3itPqfce5TVa0s59LRiA= github.com/tus/tusd v1.8.0 h1:QODQ5uMhL2tFX3Ouk7rUHHqPqeDBvi2+gYIoyUO0n8Q= github.com/tus/tusd v1.8.0/go.mod h1:stZzKpol4qz7lX2HXy/1H526dn5mRnkIICTW2lrh9NM= @@ -1475,6 +1490,8 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e h1:1SzTfNOXwIS2oWiMF+6qu0OUDKb0dauo6MoDUQyu+yU= golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1591,6 +1608,7 @@ golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211111083644-e5c967477495/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1711,6 +1729,7 @@ golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1744,8 +1763,10 @@ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/term v0.0.0-20201113234701-d7a72108b828/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210916214954-140adaaadfaf h1:Ihq/mm/suC88gF8WFcVwk+OV6Tq+wyA1O0E5UEvDglI= +golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210916214954-140adaaadfaf/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/graph/pkg/identity/backend.go b/graph/pkg/identity/backend.go index 62d9ccd13..49e71709b 100644 --- a/graph/pkg/identity/backend.go +++ b/graph/pkg/identity/backend.go @@ -11,18 +11,24 @@ import ( type Backend interface { // CreateUser creates a given user in the identity backend. CreateUser(ctx context.Context, user libregraph.User) (*libregraph.User, error) - // DeleteUser deletes a given user, identified by username or id, from the backend DeleteUser(ctx context.Context, nameOrID string) error - // UpdateUser applies changes to given user, identified by username or id UpdateUser(ctx context.Context, nameOrID string, user libregraph.User) (*libregraph.User, error) - GetUser(ctx context.Context, nameOrID string) (*libregraph.User, error) GetUsers(ctx context.Context, queryParam url.Values) ([]*libregraph.User, error) + // CreateGroup creates the supplied group in the identity backend. + CreateGroup(ctx context.Context, group libregraph.Group) (*libregraph.Group, error) + // DeleteGroup deletes a given group, identified by id + DeleteGroup(ctx context.Context, id string) error GetGroup(ctx context.Context, nameOrID string) (*libregraph.Group, error) GetGroups(ctx context.Context, queryParam url.Values) ([]*libregraph.Group, error) + GetGroupMembers(ctx context.Context, id string) ([]*libregraph.User, error) + // AddMembersToGroup adds new members (reference by a slice of IDs) to supplied group in the identity backend. + AddMembersToGroup(ctx context.Context, groupID string, memberID []string) error + // RemoveMemberFromGroup removes a single member (by ID) from a group + RemoveMemberFromGroup(ctx context.Context, groupID string, memberID string) error } func CreateUserModelFromCS3(u *cs3.User) *libregraph.User { diff --git a/graph/pkg/identity/cs3.go b/graph/pkg/identity/cs3.go index 36e6108bd..e4b7c2054 100644 --- a/graph/pkg/identity/cs3.go +++ b/graph/pkg/identity/cs3.go @@ -142,6 +142,11 @@ func (i *CS3) GetGroups(ctx context.Context, queryParam url.Values) ([]*libregra return groups, nil } +// CreateGroup implements the Backend Interface. It's currently not supported for the CS3 backend +func (i *CS3) CreateGroup(ctx context.Context, group libregraph.Group) (*libregraph.Group, error) { + return nil, errorcode.New(errorcode.NotSupported, "not implemented") +} + func (i *CS3) GetGroup(ctx context.Context, groupID string) (*libregraph.Group, error) { client, err := pool.GetGatewayServiceClient(i.Config.Address) if err != nil { @@ -169,6 +174,26 @@ func (i *CS3) GetGroup(ctx context.Context, groupID string) (*libregraph.Group, return createGroupModelFromCS3(res.Group), nil } +// DeleteGroup implements the Backend Interface. It's currently not supported for the CS3 backend +func (i *CS3) DeleteGroup(ctx context.Context, id string) error { + return errorcode.New(errorcode.NotSupported, "not implemented") +} + +// GetGroupMembers implements the Backend Interface. It's currently not supported for the CS3 backend +func (i *CS3) GetGroupMembers(ctx context.Context, groupID string) ([]*libregraph.User, error) { + return nil, errorcode.New(errorcode.NotSupported, "not implemented") +} + +// AddMembersToGroup implements the Backend Interface. It's currently not supported for the CS3 backend +func (i *CS3) AddMembersToGroup(ctx context.Context, groupID string, memberID []string) error { + return errorcode.New(errorcode.NotSupported, "not implemented") +} + +// RemoveMemberFromGroup implements the Backend Interface. It's currently not supported for the CS3 backend +func (i *CS3) RemoveMemberFromGroup(ctx context.Context, groupID string, memberID string) error { + return errorcode.New(errorcode.NotSupported, "not implemented") +} + func createGroupModelFromCS3(g *cs3group.Group) *libregraph.Group { if g.Id == nil { g.Id = &cs3group.GroupId{} diff --git a/graph/pkg/identity/ldap.go b/graph/pkg/identity/ldap.go index 04c8d7e97..71f402eae 100644 --- a/graph/pkg/identity/ldap.go +++ b/graph/pkg/identity/ldap.go @@ -8,6 +8,7 @@ import ( "github.com/go-ldap/ldap/v3" "github.com/gofrs/uuid" + ldapdn "github.com/libregraph/idm/pkg/ldapdn" libregraph "github.com/owncloud/libre-graph-api-go" "github.com/owncloud/ocis/graph/pkg/config" @@ -46,8 +47,10 @@ type userAttributeMap struct { } type groupAttributeMap struct { - name string - id string + name string + id string + member string + memberSyntax string } func NewLDAPBackend(lc ldap.Client, config config.LDAP, logger *log.Logger) (*LDAP, error) { @@ -66,8 +69,10 @@ func NewLDAPBackend(lc ldap.Client, config config.LDAP, logger *log.Logger) (*LD return nil, errors.New("invalid group attribute mappings") } gam := groupAttributeMap{ - name: config.GroupNameAttribute, - id: config.GroupIDAttribute, + name: config.GroupNameAttribute, + id: config.GroupIDAttribute, + member: "member", + memberSyntax: "dn", } var userScope, groupScope int @@ -180,7 +185,7 @@ func (i *LDAP) DeleteUser(ctx context.Context, nameOrID string) error { return nil } -// UpdateUser implements the Backend Interface. It's currently not suported for the CS3 backedn +// UpdateUser implements the Backend Interface for the LDAP Backend func (i *LDAP) UpdateUser(ctx context.Context, nameOrID string, user libregraph.User) (*libregraph.User, error) { if !i.writeEnabled { return nil, errReadOnly @@ -235,15 +240,28 @@ func (i *LDAP) UpdateUser(ctx context.Context, nameOrID string, user libregraph. } func (i *LDAP) getUserByDN(dn string) (*ldap.Entry, error) { + attrs := []string{ + i.userAttributeMap.displayName, + i.userAttributeMap.id, + i.userAttributeMap.mail, + i.userAttributeMap.userName, + } + return i.getEntryByDN(dn, attrs) +} + +func (i *LDAP) getGroupByDN(dn string) (*ldap.Entry, error) { + attrs := []string{ + i.groupAttributeMap.id, + i.groupAttributeMap.name, + } + return i.getEntryByDN(dn, attrs) +} + +func (i *LDAP) getEntryByDN(dn string, attrs []string) (*ldap.Entry, error) { searchRequest := ldap.NewSearchRequest( dn, ldap.ScopeBaseObject, ldap.NeverDerefAliases, 1, 0, false, "(objectclass=*)", - []string{ - i.userAttributeMap.displayName, - i.userAttributeMap.id, - i.userAttributeMap.mail, - i.userAttributeMap.userName, - }, + attrs, nil, ) @@ -261,11 +279,22 @@ func (i *LDAP) getUserByDN(dn string) (*ldap.Entry, error) { return res.Entries[0], nil } +func (i *LDAP) getLDAPUserByID(id string) (*ldap.Entry, error) { + id = ldap.EscapeFilter(id) + filter := fmt.Sprintf("(%s=%s)", i.userAttributeMap.id, id) + return i.getLDAPUserByFilter(filter) +} + func (i *LDAP) getLDAPUserByNameOrID(nameOrID string) (*ldap.Entry, error) { nameOrID = ldap.EscapeFilter(nameOrID) + filter := fmt.Sprintf("(|(%s=%s)(%s=%s))", i.userAttributeMap.userName, nameOrID, i.userAttributeMap.id, nameOrID) + return i.getLDAPUserByFilter(filter) +} + +func (i *LDAP) getLDAPUserByFilter(filter string) (*ldap.Entry, error) { searchRequest := ldap.NewSearchRequest( i.userBaseDN, i.userScope, ldap.NeverDerefAliases, 1, 0, false, - fmt.Sprintf("(&%s(|(%s=%s)(%s=%s)))", i.userFilter, i.userAttributeMap.userName, nameOrID, i.userAttributeMap.id, nameOrID), + fmt.Sprintf("(&%s%s)", i.userFilter, filter), []string{ i.userAttributeMap.displayName, i.userAttributeMap.id, @@ -281,9 +310,9 @@ func (i *LDAP) getLDAPUserByNameOrID(nameOrID string) (*ldap.Entry, error) { var errmsg string if lerr, ok := err.(*ldap.Error); ok { if lerr.ResultCode == ldap.LDAPResultSizeLimitExceeded { - errmsg = fmt.Sprintf("too many results searching for user '%s'", nameOrID) + errmsg = fmt.Sprintf("too many results searching for user '%s'", filter) i.logger.Debug().Str("backend", "ldap").Err(lerr). - Str("user", nameOrID).Msg("too many results searching for user") + Str("userfilter", filter).Msg("too many results searching for user") } } return nil, errorcode.New(errorcode.ItemNotFound, errmsg) @@ -347,16 +376,44 @@ func (i *LDAP) GetUsers(ctx context.Context, queryParam url.Values) ([]*libregra return users, nil } -func (i *LDAP) GetGroup(ctx context.Context, groupID string) (*libregraph.Group, error) { +func (i *LDAP) GetGroup(ctx context.Context, nameOrID string) (*libregraph.Group, error) { i.logger.Debug().Str("backend", "ldap").Msg("GetGroup") - groupID = ldap.EscapeFilter(groupID) + e, err := i.getLDAPGroupByNameOrID(nameOrID, false) + if err != nil { + return nil, err + } + return i.createGroupModelFromLDAP(e), nil +} + +func (i *LDAP) getLDAPGroupByID(id string, requestMembers bool) (*ldap.Entry, error) { + id = ldap.EscapeFilter(id) + filter := fmt.Sprintf("(%s=%s)", i.groupAttributeMap.id, id) + return i.getLDAPGroupByFilter(filter, requestMembers) +} + +func (i *LDAP) getLDAPGroupByNameOrID(nameOrID string, requestMembers bool) (*ldap.Entry, error) { + nameOrID = ldap.EscapeFilter(nameOrID) + filter := fmt.Sprintf("(|(%s=%s)(%s=%s))", i.groupAttributeMap.name, nameOrID, i.groupAttributeMap.id, nameOrID) + return i.getLDAPGroupByFilter(filter, requestMembers) +} + +// Search for LDAP Groups matching the specified filter, if requestMembers is true the groupMemberShip +// attribute will be part of the result attributes. The LDAP filter is combined with the configured groupFilter +// resulting in a filter like "(&(LDAP.groupFilter)())" +func (i *LDAP) getLDAPGroupByFilter(filter string, requestMembers bool) (*ldap.Entry, error) { + attrs := []string{ + i.groupAttributeMap.name, + i.groupAttributeMap.id, + } + + if requestMembers { + attrs = append(attrs, i.groupAttributeMap.member) + } + searchRequest := ldap.NewSearchRequest( i.groupBaseDN, i.groupScope, ldap.NeverDerefAliases, 1, 0, false, - fmt.Sprintf("(&%s(|(%s=%s)(%s=%s)))", i.groupFilter, i.groupAttributeMap.name, groupID, i.groupAttributeMap.id, groupID), - []string{ - i.groupAttributeMap.name, - i.groupAttributeMap.id, - }, + fmt.Sprintf("(&%s%s)", i.groupFilter, filter), + attrs, nil, ) i.logger.Debug().Str("backend", "ldap").Msgf("Search %s", i.groupBaseDN) @@ -366,7 +423,7 @@ func (i *LDAP) GetGroup(ctx context.Context, groupID string) (*libregraph.Group, var errmsg string if lerr, ok := err.(*ldap.Error); ok { if lerr.ResultCode == ldap.LDAPResultSizeLimitExceeded { - errmsg = fmt.Sprintf("too many results searching for group '%s'", groupID) + errmsg = fmt.Sprintf("too many results searching for group '%s'", filter) i.logger.Debug().Str("backend", "ldap").Err(lerr).Msg(errmsg) } } @@ -376,7 +433,7 @@ func (i *LDAP) GetGroup(ctx context.Context, groupID string) (*libregraph.Group, return nil, errNotFound } - return i.createGroupModelFromLDAP(res.Entries[0]), nil + return res.Entries[0], nil } func (i *LDAP) GetGroups(ctx context.Context, queryParam url.Values) ([]*libregraph.Group, error) { @@ -419,6 +476,212 @@ func (i *LDAP) GetGroups(ctx context.Context, queryParam url.Values) ([]*libregr return groups, nil } +// GetGroupMembers implements the Backend Interface for the LDAP Backend +func (i *LDAP) GetGroupMembers(ctx context.Context, groupID string) ([]*libregraph.User, error) { + e, err := i.getLDAPGroupByNameOrID(groupID, true) + if err != nil { + return nil, err + } + + result := []*libregraph.User{} + + for _, memberDN := range e.GetEqualFoldAttributeValues(i.groupAttributeMap.member) { + if memberDN == "" { + continue + } + i.logger.Debug().Str("memberDN", memberDN).Msg("lookup") + ue, err := i.getUserByDN(memberDN) + if err != nil { + // Ignore errors when reading a specific member fails, just log them and continue + i.logger.Warn().Err(err).Str("member", memberDN).Msg("error reading group member") + continue + } + result = append(result, i.createUserModelFromLDAP(ue)) + } + + return result, nil +} + +// CreateGroup implements the Backend Interface for the LDAP Backend +// It is currently restricted to managing groups based on the "groupOfNames" ObjectClass. +// As "groupOfNames" requires a "member" Attribute to be present. Empty Groups (groups +// without a member) a represented by adding an empty DN as the single member. +func (i *LDAP) CreateGroup(ctx context.Context, group libregraph.Group) (*libregraph.Group, error) { + if !i.writeEnabled { + return nil, errorcode.New(errorcode.NotAllowed, "server is configured read-only") + } + ar := ldap.AddRequest{ + DN: fmt.Sprintf("cn=%s,%s", *group.DisplayName, i.groupBaseDN), + Attributes: []ldap.Attribute{ + { + Type: i.groupAttributeMap.name, + Vals: []string{*group.DisplayName}, + }, + // This is a crutch to allow groups without members for LDAP Server's which + // that apply strict Schema checking. The RFCs define "member/uniqueMember" + // as required attribute for groupOfNames/groupOfUniqueNames. So we + // add an empty string (which is a valid DN) as the initial member. + // It will be replace once real members are added. + // We might wanna use the newer, but not so broadly used "groupOfMembers" + // objectclass (RFC2307bis-02) where "member" is optional. + { + Type: i.groupAttributeMap.member, + Vals: []string{""}, + }, + }, + } + + // TODO make group objectclass configurable to support e.g. posixGroup, groupOfUniqueNames, groupOfMembers?} + objectClasses := []string{"groupOfNames", "top"} + + if !i.useServerUUID { + ar.Attribute("owncloudUUID", []string{uuid.Must(uuid.NewV4()).String()}) + objectClasses = append(objectClasses, "owncloud") + } + ar.Attribute("objectClass", objectClasses) + + if err := i.conn.Add(&ar); err != nil { + return nil, err + } + + // Read back group from LDAP to get the generated UUID + e, err := i.getGroupByDN(ar.DN) + if err != nil { + return nil, err + } + return i.createGroupModelFromLDAP(e), nil +} + +// DeleteGroup implements the Backend Interface. +func (i *LDAP) DeleteGroup(ctx context.Context, id string) error { + if !i.writeEnabled { + return errorcode.New(errorcode.NotAllowed, "server is configured read-only") + } + e, err := i.getLDAPGroupByID(id, false) + if err != nil { + return err + } + dr := ldap.DelRequest{DN: e.DN} + if err = i.conn.Del(&dr); err != nil { + return err + } + return nil +} + +// AddMembersToGroup implements the Backend Interface for the LDAP backend. +// Currently it is limited to adding Users as Group members. Adding other groups +// as members is not yet implemented +func (i *LDAP) AddMembersToGroup(ctx context.Context, groupID string, memberIDs []string) error { + ge, err := i.getLDAPGroupByID(groupID, true) + if err != nil { + return err + } + + mr := ldap.ModifyRequest{DN: ge.DN} + // Handle empty groups (using the empty member attribute) + current := ge.GetEqualFoldAttributeValues(i.groupAttributeMap.member) + if len(current) == 1 && current[0] == "" { + mr.Delete(i.groupAttributeMap.member, []string{""}) + } + + // Create a Set of current members for faster lookups + currentSet := make(map[string]struct{}, len(current)) + for _, currentMember := range current { + // We can ignore any empty member value here + if currentMember == "" { + continue + } + nCurrentMember, err := ldapdn.ParseNormalize(currentMember) + if err != nil { + // We couldn't parse the member value as a DN. Let's skip it, but log a warning + i.logger.Warn().Str("memberDN", currentMember).Err(err).Msg("Couldn't parse DN") + continue + } + currentSet[nCurrentMember] = struct{}{} + } + + var newMemberDNs []string + for _, memberID := range memberIDs { + me, err := i.getLDAPUserByID(memberID) + if err != nil { + return err + } + nDN, err := ldapdn.ParseNormalize(me.DN) + if err != nil { + i.logger.Error().Str("new member", me.DN).Err(err).Msg("Couldn't parse DN") + return err + } + if _, present := currentSet[nDN]; !present { + newMemberDNs = append(newMemberDNs, me.DN) + } else { + i.logger.Debug().Str("memberDN", me.DN).Msg("Member already present in group. Skipping") + } + } + + if len(newMemberDNs) > 0 { + mr.Add(i.groupAttributeMap.member, newMemberDNs) + + if err := i.conn.Modify(&mr); err != nil { + return err + } + } + return nil +} + +// RemoveMemberFromGroup implements the Backend Interface. +func (i *LDAP) RemoveMemberFromGroup(ctx context.Context, groupID string, memberID string) error { + ge, err := i.getLDAPGroupByID(groupID, true) + if err != nil { + i.logger.Warn().Str("backend", "ldap").Str("groupID", groupID).Msg("Error looking up group") + return err + } + me, err := i.getLDAPUserByID(memberID) + if err != nil { + i.logger.Warn().Str("backend", "ldap").Str("memberID", memberID).Msg("Error looking up group member") + return err + } + i.logger.Debug().Str("backend", "ldap").Str("groupdn", ge.DN).Str("member", me.DN).Msg("remove member") + + nOldMemberDN, err := ldapdn.ParseNormalize(me.DN) + if err != nil { + i.logger.Error().Str("old member", me.DN).Err(err).Msg("Couldn't parse DN") + return err + } + members := ge.GetEqualFoldAttributeValues(i.groupAttributeMap.member) + found := false + for _, member := range members { + if member == "" { + continue + } + if nMember, err := ldapdn.ParseNormalize(member); err != nil { + // We couldn't parse the member value as a DN. Let's keep it + // as it is but log a warning + i.logger.Warn().Str("memberDN", member).Err(err).Msg("Couldn't parse DN") + continue + } else { + if nMember == nOldMemberDN { + found = true + } + } + } + if !found { + i.logger.Debug().Str("backend", "ldap").Str("groupdn", ge.DN).Str("member", me.DN). + Msg("The target is not a member of the group") + return nil + } + + mr := ldap.ModifyRequest{DN: ge.DN} + if len(members) == 1 { + mr.Add(i.groupAttributeMap.member, []string{""}) + } + mr.Delete(i.groupAttributeMap.member, []string{me.DN}) + + if err := i.conn.Modify(&mr); err != nil { + return err + } + return nil +} + func (i *LDAP) createUserModelFromLDAP(e *ldap.Entry) *libregraph.User { if e == nil { return nil @@ -433,11 +696,11 @@ func (i *LDAP) createUserModelFromLDAP(e *ldap.Entry) *libregraph.User { func (i *LDAP) createGroupModelFromLDAP(e *ldap.Entry) *libregraph.Group { return &libregraph.Group{ - DisplayName: pointerOrNil(e.GetEqualFoldAttributeValue(i.groupAttributeMap.name)), - OnPremisesSamAccountName: pointerOrNil(e.GetEqualFoldAttributeValue(i.groupAttributeMap.name)), - Id: pointerOrNil(e.GetEqualFoldAttributeValue(i.groupAttributeMap.id)), + DisplayName: pointerOrNil(e.GetEqualFoldAttributeValue(i.groupAttributeMap.name)), + Id: pointerOrNil(e.GetEqualFoldAttributeValue(i.groupAttributeMap.id)), } } + func pointerOrNil(val string) *string { if val == "" { return nil diff --git a/graph/pkg/service/v0/groups.go b/graph/pkg/service/v0/groups.go index 059812636..f261cfc10 100644 --- a/graph/pkg/service/v0/groups.go +++ b/graph/pkg/service/v0/groups.go @@ -1,17 +1,22 @@ package svc import ( + "encoding/json" "errors" + "fmt" "net/http" "net/url" + "strings" + libregraph "github.com/owncloud/libre-graph-api-go" "github.com/owncloud/ocis/graph/pkg/service/v0/errorcode" "github.com/go-chi/chi/v5" "github.com/go-chi/render" - //msgraph "github.com/owncloud/open-graph-api-go" // FIXME add groups to open graph, needs OnPremisesSamAccountName and OnPremisesDomainName ) +const memberRefsLimit = 20 + // GetGroups implements the Service interface. func (g Graph) GetGroups(w http.ResponseWriter, r *http.Request) { groups, err := g.identityBackend.GetGroups(r.Context(), r.URL.Query()) @@ -28,6 +33,97 @@ func (g Graph) GetGroups(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, &listResponse{Value: groups}) } +// PostGroup implements the Service interface. +func (g Graph) PostGroup(w http.ResponseWriter, r *http.Request) { + grp := libregraph.NewGroup() + err := json.NewDecoder(r.Body).Decode(grp) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error()) + return + } + + if isNilOrEmpty(grp.DisplayName) { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "Missing Required Attribute") + return + } + + // Disallow user-supplied IDs. It's supposed to be readonly. We're either + // generating them in the backend ourselves or rely on the Backend's + // storage (e.g. LDAP) to provide a unique ID. + if !isNilOrEmpty(grp.Id) { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "group id is a read-only attribute") + return + } + + if grp, err = g.identityBackend.CreateGroup(r.Context(), *grp); err != nil { + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + return + } + + render.Status(r, http.StatusOK) + render.JSON(w, r, grp) +} + +// PatchGroup implements the Service interface. +func (g Graph) PatchGroup(w http.ResponseWriter, r *http.Request) { + g.logger.Debug().Msg("Calling PatchGroup") + groupID := chi.URLParam(r, "groupID") + groupID, err := url.PathUnescape(groupID) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping group id failed") + return + } + + if groupID == "" { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing group id") + return + } + changes := libregraph.NewGroup() + err = json.NewDecoder(r.Body).Decode(changes) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error()) + return + } + + if memberRefs, ok := changes.GetMembersodataBindOk(); ok { + // The spec defines a limit of 20 members maxium per Request + if len(memberRefs) > memberRefsLimit { + errorcode.NotAllowed.Render(w, r, http.StatusInternalServerError, + fmt.Sprintf("Request is limited to %d members", memberRefsLimit)) + return + } + memberIDs := make([]string, 0, len(memberRefs)) + for _, memberRef := range memberRefs { + memberType, id, err := g.parseMemberRef(memberRef) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "Error parsing member@odata.bind values") + return + } + g.logger.Debug().Str("memberType", memberType).Str("memberid", id).Msg("Add Member") + // The MS Graph spec allows "directoryObject", "user", "group" and "organizational Contact" + // we restrict this to users for now. Might add Groups as members later + if memberType != "users" { + errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "Only user are allowed as group members") + return + } + memberIDs = append(memberIDs, id) + } + err = g.identityBackend.AddMembersToGroup(r.Context(), groupID, memberIDs) + } + + if err != nil { + var errcode errorcode.Error + if errors.As(err, &errcode) { + errcode.Render(w, r) + } else { + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + } + return + } + render.Status(r, http.StatusNoContent) + render.NoContent(w, r) +} + // GetGroup implements the Service interface. func (g Graph) GetGroup(w http.ResponseWriter, r *http.Request) { groupID := chi.URLParam(r, "groupID") @@ -54,3 +150,171 @@ func (g Graph) GetGroup(w http.ResponseWriter, r *http.Request) { render.Status(r, http.StatusOK) render.JSON(w, r, group) } + +// DeleteGroup implements the Service interface. +func (g Graph) DeleteGroup(w http.ResponseWriter, r *http.Request) { + groupID := chi.URLParam(r, "groupID") + groupID, err := url.PathUnescape(groupID) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping group id failed") + return + } + + if groupID == "" { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing group id") + return + } + + err = g.identityBackend.DeleteGroup(r.Context(), groupID) + + if err != nil { + var errcode errorcode.Error + if errors.As(err, &errcode) { + errcode.Render(w, r) + } else { + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + } + return + } + render.Status(r, http.StatusNoContent) + render.NoContent(w, r) +} + +func (g Graph) GetGroupMembers(w http.ResponseWriter, r *http.Request) { + groupID := chi.URLParam(r, "groupID") + groupID, err := url.PathUnescape(groupID) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping group id failed") + return + } + + if groupID == "" { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing group id") + return + } + + members, err := g.identityBackend.GetGroupMembers(r.Context(), groupID) + if err != nil { + var errcode errorcode.Error + if errors.As(err, &errcode) { + errcode.Render(w, r) + } else { + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + } + return + } + + render.Status(r, http.StatusOK) + render.JSON(w, r, members) +} + +// PostGroupMember implements the Service interface. +func (g Graph) PostGroupMember(w http.ResponseWriter, r *http.Request) { + g.logger.Info().Msg("Calling PostGroupMember") + + groupID := chi.URLParam(r, "groupID") + groupID, err := url.PathUnescape(groupID) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping group id failed") + return + } + + if groupID == "" { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing group id") + return + } + memberRef := libregraph.NewMemberReference() + err = json.NewDecoder(r.Body).Decode(memberRef) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error()) + return + } + memberRefURL, ok := memberRef.GetOdataIdOk() + if !ok { + errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "@odata.id refernce is missing") + return + } + memberType, id, err := g.parseMemberRef(*memberRefURL) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "Error parsing @odata.id url") + return + } + // The MS Graph spec allows "directoryObject", "user", "group" and "organizational Contact" + // we restrict this to users for now. Might add Groups as members later + if memberType != "users" { + errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "Only user are allowed as group members") + return + } + + g.logger.Debug().Str("memberType", memberType).Str("id", id).Msg("Add Member") + err = g.identityBackend.AddMembersToGroup(r.Context(), groupID, []string{id}) + + if err != nil { + var errcode errorcode.Error + if errors.As(err, &errcode) { + errcode.Render(w, r) + } else { + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + } + return + } + render.Status(r, http.StatusNoContent) + render.NoContent(w, r) +} + +// DeleteGroupMember implements the Service interface. +func (g Graph) DeleteGroupMember(w http.ResponseWriter, r *http.Request) { + g.logger.Info().Msg("Calling DeleteGroupMember") + + groupID := chi.URLParam(r, "groupID") + groupID, err := url.PathUnescape(groupID) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping group id failed") + return + } + + if groupID == "" { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing group id") + return + } + + memberID := chi.URLParam(r, "memberID") + memberID, err = url.PathUnescape(memberID) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping group id failed") + return + } + + if memberID == "" { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing group id") + return + } + g.logger.Debug().Str("groupID", groupID).Str("memberID", memberID).Msg("DeleteGroupMember") + err = g.identityBackend.RemoveMemberFromGroup(r.Context(), groupID, memberID) + + if err != nil { + var errcode errorcode.Error + if errors.As(err, &errcode) { + errcode.Render(w, r) + } else { + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + } + return + } + render.Status(r, http.StatusNoContent) + render.NoContent(w, r) +} + +func (g Graph) parseMemberRef(ref string) (string, string, error) { + memberURL, err := url.ParseRequestURI(ref) + if err != nil { + return "", "", err + } + segments := strings.Split(memberURL.Path, "/") + if len(segments) < 2 { + return "", "", errors.New("invalid member reference") + } + id := segments[len(segments)-1] + memberType := segments[len(segments)-2] + return memberType, id, nil +} diff --git a/graph/pkg/service/v0/instrument.go b/graph/pkg/service/v0/instrument.go index 05cb9306c..96b0183e5 100644 --- a/graph/pkg/service/v0/instrument.go +++ b/graph/pkg/service/v0/instrument.go @@ -54,6 +54,46 @@ func (i instrument) PatchUser(w http.ResponseWriter, r *http.Request) { i.next.PatchUser(w, r) } +// GetGroups implements the Service interface. +func (i instrument) GetGroups(w http.ResponseWriter, r *http.Request) { + i.next.GetGroups(w, r) +} + +// GetGroup implements the Service interface. +func (i instrument) GetGroup(w http.ResponseWriter, r *http.Request) { + i.next.GetGroup(w, r) +} + +// PostGroup implements the Service interface. +func (i instrument) PostGroup(w http.ResponseWriter, r *http.Request) { + i.next.PostGroup(w, r) +} + +// PatchGroup implements the Service interface. +func (i instrument) PatchGroup(w http.ResponseWriter, r *http.Request) { + i.next.PatchGroup(w, r) +} + +// DeleteGroup implements the Service interface. +func (i instrument) DeleteGroup(w http.ResponseWriter, r *http.Request) { + i.next.DeleteGroup(w, r) +} + +// GetGroupMembers implements the Service interface. +func (i instrument) GetGroupMembers(w http.ResponseWriter, r *http.Request) { + i.next.GetGroupMembers(w, r) +} + +// PostGroupMember implements the Service interface. +func (i instrument) PostGroupMember(w http.ResponseWriter, r *http.Request) { + i.next.PostGroupMember(w, r) +} + +// DeleteGroupMember implements the Service interface. +func (i instrument) DeleteGroupMember(w http.ResponseWriter, r *http.Request) { + i.next.DeleteGroupMember(w, r) +} + // GetDrives implements the Service interface. func (i instrument) GetDrives(w http.ResponseWriter, r *http.Request) { i.next.GetDrives(w, r) diff --git a/graph/pkg/service/v0/logging.go b/graph/pkg/service/v0/logging.go index 6a29c3ef8..3ee85ff25 100644 --- a/graph/pkg/service/v0/logging.go +++ b/graph/pkg/service/v0/logging.go @@ -54,6 +54,46 @@ func (l logging) PatchUser(w http.ResponseWriter, r *http.Request) { l.next.PatchUser(w, r) } +// GetGroups implements the Service interface. +func (l logging) GetGroups(w http.ResponseWriter, r *http.Request) { + l.next.GetGroups(w, r) +} + +// GetGroup implements the Service interface. +func (l logging) GetGroup(w http.ResponseWriter, r *http.Request) { + l.next.GetGroup(w, r) +} + +// PostGroup implements the Service interface. +func (l logging) PostGroup(w http.ResponseWriter, r *http.Request) { + l.next.PostGroup(w, r) +} + +// PatchGroup implements the Service interface. +func (l logging) PatchGroup(w http.ResponseWriter, r *http.Request) { + l.next.PatchGroup(w, r) +} + +// DeleteGroup implements the Service interface. +func (l logging) DeleteGroup(w http.ResponseWriter, r *http.Request) { + l.next.DeleteGroup(w, r) +} + +// GetGroupMembers implements the Service interface. +func (l logging) GetGroupMembers(w http.ResponseWriter, r *http.Request) { + l.next.GetGroupMembers(w, r) +} + +// PostGroupMember implements the Service interface. +func (l logging) PostGroupMember(w http.ResponseWriter, r *http.Request) { + l.next.PostGroupMember(w, r) +} + +// DeleteGroupMember implements the Service interface. +func (l logging) DeleteGroupMember(w http.ResponseWriter, r *http.Request) { + l.next.DeleteGroupMember(w, r) +} + // GetDrives implements the Service interface. func (l logging) GetDrives(w http.ResponseWriter, r *http.Request) { l.next.GetDrives(w, r) diff --git a/graph/pkg/service/v0/service.go b/graph/pkg/service/v0/service.go index 870ea8786..79859e070 100644 --- a/graph/pkg/service/v0/service.go +++ b/graph/pkg/service/v0/service.go @@ -31,6 +31,15 @@ type Service interface { DeleteUser(http.ResponseWriter, *http.Request) PatchUser(http.ResponseWriter, *http.Request) + GetGroups(http.ResponseWriter, *http.Request) + GetGroup(http.ResponseWriter, *http.Request) + PostGroup(http.ResponseWriter, *http.Request) + PatchGroup(http.ResponseWriter, *http.Request) + DeleteGroup(http.ResponseWriter, *http.Request) + GetGroupMembers(http.ResponseWriter, *http.Request) + PostGroupMember(http.ResponseWriter, *http.Request) + DeleteGroupMember(http.ResponseWriter, *http.Request) + GetDrives(w http.ResponseWriter, r *http.Request) } @@ -109,8 +118,16 @@ func NewService(opts ...Option) Service { }) r.Route("/groups", func(r chi.Router) { r.Get("/", svc.GetGroups) + r.Post("/", svc.PostGroup) r.Route("/{groupID}", func(r chi.Router) { r.Get("/", svc.GetGroup) + r.Delete("/", svc.DeleteGroup) + r.Patch("/", svc.PatchGroup) + r.Route("/members", func(r chi.Router) { + r.Get("/", svc.GetGroupMembers) + r.Post("/$ref", svc.PostGroupMember) + r.Delete("/{memberID}/$ref", svc.DeleteGroupMember) + }) }) }) r.Group(func(r chi.Router) { diff --git a/graph/pkg/service/v0/tracing.go b/graph/pkg/service/v0/tracing.go index 59e9197c7..0d2581ba9 100644 --- a/graph/pkg/service/v0/tracing.go +++ b/graph/pkg/service/v0/tracing.go @@ -50,6 +50,46 @@ func (t tracing) PatchUser(w http.ResponseWriter, r *http.Request) { t.next.PatchUser(w, r) } +// GetGroups implements the Service interface. +func (t tracing) GetGroups(w http.ResponseWriter, r *http.Request) { + t.next.GetGroups(w, r) +} + +// GetGroup implements the Service interface. +func (t tracing) GetGroup(w http.ResponseWriter, r *http.Request) { + t.next.GetGroup(w, r) +} + +// PostGroup implements the Service interface. +func (t tracing) PostGroup(w http.ResponseWriter, r *http.Request) { + t.next.PostGroup(w, r) +} + +// PatchGroup implements the Service interface. +func (t tracing) PatchGroup(w http.ResponseWriter, r *http.Request) { + t.next.PatchGroup(w, r) +} + +// DeleteGroup implements the Service interface. +func (t tracing) DeleteGroup(w http.ResponseWriter, r *http.Request) { + t.next.DeleteGroup(w, r) +} + +// GetGroupMembers implements the Service interface. +func (t tracing) GetGroupMembers(w http.ResponseWriter, r *http.Request) { + t.next.GetGroupMembers(w, r) +} + +// PostGroupMember implements the Service interface. +func (t tracing) PostGroupMember(w http.ResponseWriter, r *http.Request) { + t.next.PostGroupMember(w, r) +} + +// DeleteGroupMember implements the Service interface. +func (t tracing) DeleteGroupMember(w http.ResponseWriter, r *http.Request) { + t.next.DeleteGroupMember(w, r) +} + // GetDrives implements the Service interface. func (t tracing) GetDrives(w http.ResponseWriter, r *http.Request) { t.next.GetDrives(w, r)