Repository: owncloud/ocis
Pull request #5974 (2023-03-30 16:50:05)

graph: user groupTypes property to indicate read-only groups
By setting GRAPH_LDAP_GROUP_CREATE_BASE_DN a distinct subtree can be configure
where new LDAP groups are created. That subtree needs to be subordinate to
GRAPH_LDAP_GROUP_BASE_DN. All groups outside for
GRAPH_LDAP_GROUP_CREATE_BASE_DN are considered read-only and only group below
that DN can be updated and deleted.
    
This is introduced for a pretty specific usecase where most groups are managed
in an external source (e.g. a read-only replica of an LDAP tree). But we still
want to allow the local administrator to create groups in a writeable subtree
attached to that replica.

To test this you can start ocis with

```
IDM_CREATE_DEMO_USERS=true GRAPH_LDAP_GROUP_CREATE_BASE_DN=ou=users,o=libregraph-idm GRAPH_LDAP_GROUP_BASE_DN=o=libregraph-idm ocis/bin/ocis server
```

This will make all the demo groups "readonly" (indicated through "ReadOnly" value in "groupTypes":

```
~> curl -s -k -u admin:admin  'https://localhost:9200/graph/v1.0/groups/users'   -H 'accept: application/json' | jq .
{
  "displayName": "users",
  "groupTypes": [
    "ReadOnly"
  ],
  "id": "509a9dcd-bb37-4f4f-a01a-19dca27d9cfa"
}
```

Trying to update the group will end with an error (currently a 500), but I still need to fix that to return a 405 or 403.

Adding groups is still possible, but they'll endup in a seperate part of the LDAP tree:

```
~> curl -s -k -u admin:admin  'https://localhost:9200/graph/v1.0/groups'   -H 'accept: application/json' -X POST --data '{"displayName": "Lokal"}' | jq .
{
  "displayName": "Lokal",
  "groupTypes": [],
  "id": "0a0f201c-c38f-40fd-a4c4-cd87d9168ee9"
}
```

Groups without the "ReadOnly" value in the groupTypes can be updated:

```
~> curl -i -k -u admin:admin  'https://localhost:9200/graph/v1.0/groups/0a0f201c-c38f-40fd-a4c4-cd87d9168ee9'   -H 'accept: application/json' -X PATCH --data '{"displayName": "Lokale Gruppe"}' 
HTTP/1.1 204 No Content
Date: Thu, 30 Mar 2023 16:48:47 GMT
X-Graph-Version: 3.0.0-alpha.1+093d65dcd
```

update-docs[bot]: Thanks for opening this pull request! The maintainers of this repository would appreciate it if you would create a [changelog](https://github.com/owncloud/ocis/blob/master/changelog/README.md) item based on your changes.

rhafer: @JammingBen This should be of interest for you.

Note: We opted for using the `groupTypes` list property for indicating the readOnly state of a group as we didn't want to introduce some incompatible (with MS graph) property on the groups resource. If that is too clumsy for you I am open for discussion. But I guess @micbar and @butonic have some opinion about this as well.

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=5974)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5974&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5974&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5974&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5974&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5974&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5974&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5974&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5974&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5974&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5974&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5974&resolved=false&types=CODE_SMELL) [2 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5974&resolved=false&types=CODE_SMELL)

[![24.5%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/0-16px.png '24.5%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5974&metric=new_coverage&view=list) [24.5% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5974&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5974&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5974&metric=new_duplicated_lines_density&view=list)


ownclouders: E2E tests failed: https://drone.owncloud.com/owncloud/ocis/20982/68/1
<details><summary>:boom: To see the trace, please open the link in the console ...</summary>

<p>


#### npx playwright show-trace https://cache.owncloud.com/public/owncloud/ocis/20982/tracing/file-alice-2023-3-30-05-05-40.zip 

#### npx playwright show-trace https://cache.owncloud.com/public/owncloud/ocis/20982/tracing/space-members-can-be-invited-with-an-expiration-date-alice-2023-3-30-05-07-15.zip 

#### npx playwright show-trace https://cache.owncloud.com/public/owncloud/ocis/20982/tracing/space-members-can-be-invited-with-an-expiration-date-brian-2023-3-30-05-07-21.zip 


</p></details>

<!-- id: pr-5974 -->

JammingBen: > @JammingBen This should be of interest for you.
> 
> Note: We opted for using the `groupTypes` list property for indicating the readOnly state of a group as we didn't want to introduce some incompatible (with MS graph) property on the groups resource. If that is too clumsy for you I am open for discussion. But I guess @micbar and @butonic have some opinion about this as well.

That's okay, I can work with that. Thanks for the heads-up!
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=5974)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5974&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5974&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5974&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5974&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5974&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5974&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5974&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5974&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5974&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5974&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5974&resolved=false&types=CODE_SMELL) [2 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5974&resolved=false&types=CODE_SMELL)

[![25.9%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/25-16px.png '25.9%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5974&metric=new_coverage&view=list) [25.9% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5974&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5974&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5974&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6d8b38e3eab..c6a86c7afc3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,7 @@ The following sections list the changes for unreleased.
 * Enhancement - Async Postprocessing: [#5207](https://github.com/owncloud/ocis/pull/5207)
 * Enhancement - Automate md creation: [#5901](https://github.com/owncloud/ocis/pull/5901)
 * Enhancement - Add endpoints to upload a custom logo: [#5735](https://github.com/owncloud/ocis/pull/5735)
+* Enhancement - Bump go-ldap version: [#6004](https://github.com/owncloud/ocis/pull/6004)
 * Enhancement - Bump libre-graph-api-go: [#5309](https://github.com/owncloud/ocis/pull/5309)
 * Enhancement - Bump reva version: [#5243](https://github.com/owncloud/ocis/pull/5243)
 * Enhancement - Collect global envvars: [#5367](https://github.com/owncloud/ocis/pull/5367)
diff --git a/changelog/unreleased/bump-go-ldap-version.md b/changelog/unreleased/bump-go-ldap-version.md
new file mode 100644
index 00000000000..cd125f18780
--- /dev/null
+++ b/changelog/unreleased/bump-go-ldap-version.md
@@ -0,0 +1,5 @@
+Enhancement: Bump go-ldap version
+
+Use master version of go-ldap to get rid of nasty `=` bug. See https://github.com/go-ldap/ldap/issues/416
+
+https://github.com/owncloud/ocis/pull/6004
diff --git a/changelog/unreleased/enhancement-ldap-group-create-basedn.md b/changelog/unreleased/enhancement-ldap-group-create-basedn.md
new file mode 100644
index 00000000000..cfe791665ed
--- /dev/null
+++ b/changelog/unreleased/enhancement-ldap-group-create-basedn.md
@@ -0,0 +1,13 @@
+Enhancement: Make the LDAP base DN for new groups configurable
+
+The LDAP backend for the Graph service introduced a new config option for setting the
+Parent DN for new groups created via the `/groups/` endpoint. (`GRAPH_LDAP_GROUP_CREATE_BASE_DN`)
+
+It defaults to the value of `GRAPH_LDAP_GROUP_BASE_DN`. If set to a different value the
+`GRAPH_LDAP_GROUP_CREATE_BASE_DN` needs to be a subordinate DN of `GRAPH_LDAP_GROUP_BASE_DN`.
+
+All existing groups with a DN outside the `GRAPH_LDAP_GROUP_CREATE_BASE_DN` tree will be treated as
+read-only groups. So it is not possible to edit these groups.
+
+https://github.com/owncloud/ocis/pull/5974
+
diff --git a/go.mod b/go.mod
index 2d215f664a1..437f61c7fdd 100644
--- a/go.mod
+++ b/go.mod
@@ -22,7 +22,7 @@ require (
 	github.com/go-chi/chi/v5 v5.0.8
 	github.com/go-chi/cors v1.2.1
 	github.com/go-chi/render v1.0.2
-	github.com/go-ldap/ldap/v3 v3.4.4
+	github.com/go-ldap/ldap/v3 v3.4.5-0.20230327113050-32d292ef5ded
 	github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3
 	github.com/go-micro/plugins/v4/client/grpc v1.2.0
 	github.com/go-micro/plugins/v4/events/natsjs v1.2.0
@@ -67,7 +67,7 @@ require (
 	github.com/onsi/gomega v1.27.4
 	github.com/open-policy-agent/opa v0.50.2
 	github.com/orcaman/concurrent-map v1.0.0
-	github.com/owncloud/libre-graph-api-go v1.0.2-0.20230309112802-ff71ba8c90aa
+	github.com/owncloud/libre-graph-api-go v1.0.2-0.20230330145712-ea267ccd404a
 	github.com/pkg/errors v0.9.1
 	github.com/pkg/xattr v0.4.9
 	github.com/prometheus/client_golang v1.14.0
@@ -108,7 +108,7 @@ require (
 
 require (
 	contrib.go.opencensus.io/exporter/prometheus v0.4.2 // indirect
-	github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect
+	github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
 	github.com/BurntSushi/toml v1.2.1 // indirect
 	github.com/Masterminds/goutils v1.1.1 // indirect
 	github.com/Masterminds/sprig v2.22.0+incompatible // indirect
diff --git a/go.sum b/go.sum
index 03ac3a0d27d..3c74e039fc9 100644
--- a/go.sum
+++ b/go.sum
@@ -411,8 +411,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ
 github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88=
 github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
 github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
-github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU=
-github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
+github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
+github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
 github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
@@ -464,6 +464,8 @@ 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-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
+github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
+github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
 github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387 h1:loy0fjI90vF44BPW4ZYOkE3tDkGTy7yHURusOJimt+I=
 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=
@@ -625,8 +627,6 @@ github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo
 github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4=
 github.com/crewjam/saml v0.4.13 h1:TYHggH/hwP7eArqiXSJUvtOPNzQDyQ7vwmwEqlFWhMc=
 github.com/crewjam/saml v0.4.13/go.mod h1:igEejV+fihTIlHXYP8zOec3V5A8y3lws5bQBFsTm4gA=
-github.com/cs3org/reva/v2 v2.12.1-0.20230331184913-eca8953fb6e9 h1:zUMD0UvKVPbR3UaodnA0GXuBycxrIuaO8Kshgi3iKKI=
-github.com/cs3org/reva/v2 v2.12.1-0.20230331184913-eca8953fb6e9/go.mod h1:FNAYs5H3xs8v0OFmNgZtiMAzIMXd/6TJmO0uZuNn8pQ=
 github.com/cs3org/reva/v2 v2.12.1-0.20230403080425-f93a726ab656 h1:FAlxhUtG0/57X4i05x3suRd/rtAecM/7ZWeJ9ZtqrIc=
 github.com/cs3org/reva/v2 v2.12.1-0.20230403080425-f93a726ab656/go.mod h1:FNAYs5H3xs8v0OFmNgZtiMAzIMXd/6TJmO0uZuNn8pQ=
 github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8 h1:Z9lwXumT5ACSmJ7WGnFl+OMLLjpz5uR2fyz7dC255FI=
@@ -754,8 +754,8 @@ github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBj
 github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
 github.com/go-kit/log v0.2.1/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.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs=
-github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI=
+github.com/go-ldap/ldap/v3 v3.4.5-0.20230327113050-32d292ef5ded h1:DCi4j5aIjsT27tSluBdFlDkYvHnNSxwnua+hjt+1wtk=
+github.com/go-ldap/ldap/v3 v3.4.5-0.20230327113050-32d292ef5ded/go.mod h1:bMGIq3AGbytbaMwf8wdv5Phdxz0FWHTIYMSzyrYgnQs=
 github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3 h1:sfz1YppV05y4sYaW7kXZtrocU/+vimnIWt4cxAYh7+o=
 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=
@@ -1375,8 +1375,8 @@ github.com/oracle/oci-go-sdk v24.3.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35uk
 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 v1.0.2-0.20230309112802-ff71ba8c90aa h1:Lab1HHZ7Z8cBjojLDSfGYbGSZT08iww25uTW+EV1Sao=
-github.com/owncloud/libre-graph-api-go v1.0.2-0.20230309112802-ff71ba8c90aa/go.mod h1:iKdVH6nYpI8RBeK9sjeLfzrPByST6r9d+NG2IJHoJmU=
+github.com/owncloud/libre-graph-api-go v1.0.2-0.20230330145712-ea267ccd404a h1:C7YCoyXn/8pkapUhw2KoIxEMdgIFUx3JjZQKtsXaaLc=
+github.com/owncloud/libre-graph-api-go v1.0.2-0.20230330145712-ea267ccd404a/go.mod h1:iKdVH6nYpI8RBeK9sjeLfzrPByST6r9d+NG2IJHoJmU=
 github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
 github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
diff --git a/services/graph/pkg/config/config.go b/services/graph/pkg/config/config.go
index ea920a20fca..a4983ba5db1 100644
--- a/services/graph/pkg/config/config.go
+++ b/services/graph/pkg/config/config.go
@@ -69,6 +69,7 @@ type LDAP struct {
 	LdapDisabledUsersGroupDN string `yaml:"ldap_disabled_users_group_dn" env:"LDAP_DISABLED_USERS_GROUP_DN;GRAPH_DISABLED_USERS_GROUP_DN" desc:"The distinguished name of the group to which added users will be classified as disabled when 'disable_user_mechanism' is set to 'group'."`
 
 	GroupBaseDN        string `yaml:"group_base_dn" env:"LDAP_GROUP_BASE_DN;GRAPH_LDAP_GROUP_BASE_DN" desc:"Search base DN for looking up LDAP groups."`
+	GroupCreateBaseDN  string `yaml:"group_create_base_dn" env:"GRAPH_LDAP_GROUP_CREATE_BASE_DN" desc:"Parent DN under which new groups are created. This DN needs to be subordinate to the 'GRAPH_LDAP_GROUP_BASE_DN'. This setting is only relevant when 'GRAPH_LDAP_SERVER_WRITE_ENABLED' is 'true'. It defaults to the value of 'GRAPH_LDAP_GROUP_BASE_DN'. All groups outside of this subtree are treated as readonly groups and cannot be updated."`
 	GroupSearchScope   string `yaml:"group_search_scope" env:"LDAP_GROUP_SCOPE;GRAPH_LDAP_GROUP_SEARCH_SCOPE" desc:"LDAP search scope to use when looking up groups. Supported scopes are 'base', 'one' and 'sub'."`
 	GroupFilter        string `yaml:"group_filter" env:"LDAP_GROUP_FILTER;GRAPH_LDAP_GROUP_FILTER" desc:"LDAP filter to add to the default filters for group searches."`
 	GroupObjectClass   string `yaml:"group_objectclass" env:"LDAP_GROUP_OBJECTCLASS;GRAPH_LDAP_GROUP_OBJECTCLASS" desc:"The object class to use for groups in the default group search filter ('groupOfNames'). "`
diff --git a/services/graph/pkg/config/defaults/defaultconfig.go b/services/graph/pkg/config/defaults/defaultconfig.go
index ed85793c288..db1cbf5a1bc 100644
--- a/services/graph/pkg/config/defaults/defaultconfig.go
+++ b/services/graph/pkg/config/defaults/defaultconfig.go
@@ -159,6 +159,10 @@ func EnsureDefaults(cfg *config.Config) {
 	if cfg.MachineAuthAPIKey == "" && cfg.Commons != nil && cfg.Commons.MachineAuthAPIKey != "" {
 		cfg.MachineAuthAPIKey = cfg.Commons.MachineAuthAPIKey
 	}
+
+	if cfg.Identity.LDAP.GroupCreateBaseDN == "" {
+		cfg.Identity.LDAP.GroupCreateBaseDN = cfg.Identity.LDAP.GroupBaseDN
+	}
 }
 
 // Sanitize sanitized the configuration
diff --git a/services/graph/pkg/config/parser/parse.go b/services/graph/pkg/config/parser/parse.go
index 6f48d5c4833..893870f4543 100644
--- a/services/graph/pkg/config/parser/parse.go
+++ b/services/graph/pkg/config/parser/parse.go
@@ -4,6 +4,7 @@ import (
 	"errors"
 	"fmt"
 
+	"github.com/go-ldap/ldap/v3"
 	ociscfg "github.com/owncloud/ocis/v2/ocis-pkg/config"
 	defaults2 "github.com/owncloud/ocis/v2/ocis-pkg/config/defaults"
 	"github.com/owncloud/ocis/v2/ocis-pkg/shared"
@@ -40,8 +41,10 @@ func Validate(cfg *config.Config) error {
 		return shared.MissingJWTTokenError(cfg.Service.Name)
 	}
 
-	if cfg.Identity.Backend == "ldap" && cfg.Identity.LDAP.BindPassword == "" {
-		return shared.MissingLDAPBindPassword(cfg.Service.Name)
+	if cfg.Identity.Backend == "ldap" {
+		if err := validateLDAPSettings(cfg); err != nil {
+			return err
+		}
 	}
 
 	if cfg.Application.ID == "" {
@@ -64,3 +67,26 @@ func Validate(cfg *config.Config) error {
 
 	return nil
 }
+
+func validateLDAPSettings(cfg *config.Config) error {
+	if cfg.Identity.LDAP.BindPassword == "" {
+		return shared.MissingLDAPBindPassword(cfg.Service.Name)
+	}
+
+	// ensure that "GroupBaseDN" is below "GroupBaseDN"
+	if cfg.Identity.LDAP.WriteEnabled && cfg.Identity.LDAP.GroupCreateBaseDN != cfg.Identity.LDAP.GroupBaseDN {
+		baseDN, err := ldap.ParseDN(cfg.Identity.LDAP.GroupBaseDN)
+		if err != nil {
+			return fmt.Errorf("Unable to parse the LDAP Group Base DN '%s': %w ", cfg.Identity.LDAP.GroupBaseDN, err)
+		}
+		createBaseDN, err := ldap.ParseDN(cfg.Identity.LDAP.GroupCreateBaseDN)
+		if err != nil {
+			return fmt.Errorf("Unable to parse the LDAP Group Create Base DN '%s': %w ", cfg.Identity.LDAP.GroupCreateBaseDN, err)
+		}
+
+		if !baseDN.AncestorOfFold(createBaseDN) {
+			return fmt.Errorf("The LDAP Group Create Base DN (%s) must be subordinate to the LDAP Group Base DN (%s)", cfg.Identity.LDAP.GroupCreateBaseDN, cfg.Identity.LDAP.GroupBaseDN)
+		}
+	}
+	return nil
+}
diff --git a/services/graph/pkg/identity/ldap.go b/services/graph/pkg/identity/ldap.go
index 696d1647772..e784c3f2ffd 100644
--- a/services/graph/pkg/identity/ldap.go
+++ b/services/graph/pkg/identity/ldap.go
@@ -58,6 +58,7 @@ type LDAP struct {
 	localUserDisableGroupDN string
 
 	groupBaseDN       string
+	groupCreateBaseDN string
 	groupFilter       string
 	groupObjectClass  string
 	groupScope        int
@@ -148,6 +149,7 @@ func NewLDAPBackend(lc ldap.Client, config config.LDAP, logger *log.Logger) (*LD
 		userScope:               userScope,
 		userAttributeMap:        uam,
 		groupBaseDN:             config.GroupBaseDN,
+		groupCreateBaseDN:       config.GroupCreateBaseDN,
 		groupFilter:             config.GroupFilter,
 		groupObjectClass:        config.GroupObjectClass,
 		groupScope:              groupScope,
diff --git a/services/graph/pkg/identity/ldap_group.go b/services/graph/pkg/identity/ldap_group.go
index 820d44804ee..b3a2472f9a8 100644
--- a/services/graph/pkg/identity/ldap_group.go
+++ b/services/graph/pkg/identity/ldap_group.go
@@ -204,10 +204,16 @@ 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
 	}
+
+	if i.isLDAPGroupReadOnly(e) {
+		return errorcode.New(errorcode.NotAllowed, "group is read-only")
+	}
+
 	dr := ldap.DelRequest{DN: e.DN}
 	if err = i.conn.Del(&dr); err != nil {
 		return err
@@ -219,12 +225,19 @@ func (i *LDAP) DeleteGroup(ctx context.Context, id string) error {
 func (i *LDAP) UpdateGroupName(ctx context.Context, groupID string, groupName string) error {
 	logger := i.logger.SubloggerWithRequestID(ctx)
 	logger.Debug().Str("backend", "ldap").Msg("AddMembersToGroup")
+	if !i.writeEnabled {
+		return errorcode.New(errorcode.NotAllowed, "server is configured read-only")
+	}
 
 	ge, err := i.getLDAPGroupByID(groupID, true)
 	if err != nil {
 		return err
 	}
 
+	if i.isLDAPGroupReadOnly(ge) {
+		return errorcode.New(errorcode.NotAllowed, "group is read-only")
+	}
+
 	if ge.GetEqualFoldAttributeValue(i.groupAttributeMap.name) == groupName {
 		return nil
 	}
@@ -258,11 +271,18 @@ func (i *LDAP) UpdateGroupName(ctx context.Context, groupID string, groupName st
 func (i *LDAP) AddMembersToGroup(ctx context.Context, groupID string, memberIDs []string) error {
 	logger := i.logger.SubloggerWithRequestID(ctx)
 	logger.Debug().Str("backend", "ldap").Msg("AddMembersToGroup")
+	if !i.writeEnabled {
+		return errorcode.New(errorcode.NotAllowed, "server is configured read-only")
+	}
 	ge, err := i.getLDAPGroupByNameOrID(groupID, true)
 	if err != nil {
 		return err
 	}
 
+	if i.isLDAPGroupReadOnly(ge) {
+		return errorcode.New(errorcode.NotAllowed, "group is read-only")
+	}
+
 	mr := ldap.ModifyRequest{DN: ge.DN}
 	// Handle empty groups (using the empty member attribute)
 	current := ge.GetEqualFoldAttributeValues(i.groupAttributeMap.member)
@@ -326,11 +346,20 @@ func (i *LDAP) AddMembersToGroup(ctx context.Context, groupID string, memberIDs
 func (i *LDAP) RemoveMemberFromGroup(ctx context.Context, groupID string, memberID string) error {
 	logger := i.logger.SubloggerWithRequestID(ctx)
 	logger.Debug().Str("backend", "ldap").Msg("RemoveMemberFromGroup")
+	if !i.writeEnabled {
+		return errorcode.New(errorcode.NotAllowed, "server is configured read-only")
+	}
+
 	ge, err := i.getLDAPGroupByID(groupID, true)
 	if err != nil {
 		logger.Debug().Str("backend", "ldap").Str("groupID", groupID).Msg("Error looking up group")
 		return err
 	}
+
+	if i.isLDAPGroupReadOnly(ge) {
+		return errorcode.New(errorcode.NotAllowed, "group is read-only")
+	}
+
 	me, err := i.getLDAPUserByID(memberID)
 	if err != nil {
 		logger.Debug().Str("backend", "ldap").Str("memberID", memberID).Msg("Error looking up group member")
@@ -345,7 +374,7 @@ func (i *LDAP) RemoveMemberFromGroup(ctx context.Context, groupID string, member
 }
 
 func (i *LDAP) groupToAddRequest(group libregraph.Group) (*ldap.AddRequest, error) {
-	ar := ldap.NewAddRequest(i.getGroupLDAPDN(group), nil)
+	ar := ldap.NewAddRequest(i.getGroupCreateLDAPDN(group), nil)
 
 	attrMap, err := i.groupToLDAPAttrValues(group)
 	if err != nil {
@@ -357,12 +386,12 @@ func (i *LDAP) groupToAddRequest(group libregraph.Group) (*ldap.AddRequest, erro
 	return ar, nil
 }
 
-func (i *LDAP) getGroupLDAPDN(group libregraph.Group) string {
+func (i *LDAP) getGroupCreateLDAPDN(group libregraph.Group) string {
 	attributeTypeAndValue := ldap.AttributeTypeAndValue{
 		Type:  "cn",
 		Value: group.GetDisplayName(),
 	}
-	return fmt.Sprintf("%s,%s", attributeTypeAndValue.String(), i.groupBaseDN)
+	return fmt.Sprintf("%s,%s", attributeTypeAndValue.String(), i.groupCreateBaseDN)
 }
 
 func (i *LDAP) groupToLDAPAttrValues(group libregraph.Group) (map[string][]string, error) {
@@ -482,17 +511,43 @@ func (i *LDAP) getGroupsForUser(dn string) ([]*ldap.Entry, error) {
 func (i *LDAP) createGroupModelFromLDAP(e *ldap.Entry) *libregraph.Group {
 	name := e.GetEqualFoldAttributeValue(i.groupAttributeMap.name)
 	id := e.GetEqualFoldAttributeValue(i.groupAttributeMap.id)
+	groupTypes := []string{}
+
+	if i.isLDAPGroupReadOnly(e) {
+		groupTypes = []string{"ReadOnly"}
+	}
 
 	if id != "" && name != "" {
 		return &libregraph.Group{
 			DisplayName: &name,
 			Id:          &id,
+			GroupTypes:  groupTypes,
 		}
 	}
 	i.logger.Warn().Str("dn", e.DN).Msg("Group is missing name or id")
 	return nil
 }
 
+func (i *LDAP) isLDAPGroupReadOnly(e *ldap.Entry) bool {
+	if !i.writeEnabled {
+		return true
+	}
+
+	groupDN, err := ldap.ParseDN(e.DN)
+	if err != nil {
+		i.logger.Warn().Err(err).Str("dn", e.DN).Msg("Failed to parse DN")
+		return false
+	}
+
+	baseDN, err := ldap.ParseDN(i.groupCreateBaseDN)
+	if err != nil {
+		i.logger.Warn().Err(err).Str("dn", i.groupCreateBaseDN).Msg("Failed to parse DN")
+		return false
+	}
+
+	return !baseDN.AncestorOfFold(groupDN)
+}
+
 func (i *LDAP) groupsFromLDAPEntries(e []*ldap.Entry) []libregraph.Group {
 	groups := make([]libregraph.Group, 0, len(e))
 	for _, g := range e {
diff --git a/services/graph/pkg/service/v0/errorcode/errorcode.go b/services/graph/pkg/service/v0/errorcode/errorcode.go
index 4f2f9301044..911bb27f9df 100644
--- a/services/graph/pkg/service/v0/errorcode/errorcode.go
+++ b/services/graph/pkg/service/v0/errorcode/errorcode.go
@@ -106,6 +106,8 @@ func (e Error) Render(w http.ResponseWriter, r *http.Request) {
 		status = http.StatusNotFound
 	case NameAlreadyExists:
 		status = http.StatusConflict
+	case NotAllowed:
+		status = http.StatusForbidden
 	default:
 		status = http.StatusInternalServerError
 	}
diff --git a/services/graph/pkg/service/v0/groups.go b/services/graph/pkg/service/v0/groups.go
index 0e2dbaa68c0..895d21ab9d3 100644
--- a/services/graph/pkg/service/v0/groups.go
+++ b/services/graph/pkg/service/v0/groups.go
@@ -82,8 +82,13 @@ func (g Graph) PostGroup(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if grp, err = g.identityBackend.CreateGroup(r.Context(), *grp); err != nil {
-		logger.Debug().Interface("group", grp).Msg("could not create group: backend error")
-		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
+		var errcode errorcode.Error
+		if errors.As(err, &errcode) {
+			errcode.Render(w, r)
+		} else {
+			logger.Debug().Interface("group", grp).Msg("could not create group: backend error")
+			errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
+		}
 		return
 	}
 


Test patch:
diff --git a/services/graph/pkg/identity/ldap_group_test.go b/services/graph/pkg/identity/ldap_group_test.go
index 585b5e65af6..f3557cb1978 100644
--- a/services/graph/pkg/identity/ldap_group_test.go
+++ b/services/graph/pkg/identity/ldap_group_test.go
@@ -21,22 +21,42 @@ var groupEntry = ldap.NewEntry("cn=group",
 			"uid=invalid,ou=people,dc=test",
 		},
 	})
+
 var invalidGroupEntry = ldap.NewEntry("cn=invalid",
 	map[string][]string{
 		"cn": {"invalid"},
 	})
 
+var queryParamExpand = url.Values{
+	"$expand": []string{"members"},
+}
+
+var queryParamSelect = url.Values{
+	"$select": []string{"members"},
+}
+
+var groupLookupSearchRequest = &ldap.SearchRequest{
+	BaseDN:     "ou=groups,dc=test",
+	Scope:      2,
+	SizeLimit:  1,
+	Filter:     "(&(objectClass=groupOfNames)(|(cn=group)(entryUUID=group)))",
+	Attributes: []string{"cn", "entryUUID", "member"},
+	Controls:   []ldap.Control(nil),
+}
+
+var groupListSeachRequest = &ldap.SearchRequest{
+	BaseDN:     "ou=groups,dc=test",
+	Scope:      2,
+	Filter:     "(&(objectClass=groupOfNames))",
+	Attributes: []string{"cn", "entryUUID", "member"},
+	Controls:   []ldap.Control(nil),
+}
+
 func TestGetGroup(t *testing.T) {
 	// Mock a Sizelimit Error
 	lm := &mocks.Client{}
 	lm.On("Search", mock.Anything).Return(nil, ldap.NewError(ldap.LDAPResultSizeLimitExceeded, errors.New("mock")))
 
-	queryParamExpand := url.Values{
-		"$expand": []string{"members"},
-	}
-	queryParamSelect := url.Values{
-		"$select": []string{"members"},
-	}
 	b, _ := getMockedBackend(lm, lconfig, &logger)
 	_, err := b.GetGroup(context.Background(), "group", nil)
 	if err == nil || err.Error() != "itemNotFound" {
@@ -89,14 +109,6 @@ func TestGetGroup(t *testing.T) {
 
 	// Mock a valid	Search Result
 	lm = &mocks.Client{}
-	sr1 := &ldap.SearchRequest{
-		BaseDN:     "ou=groups,dc=test",
-		Scope:      2,
-		SizeLimit:  1,
-		Filter:     "(&(objectClass=groupOfNames)(|(cn=group)(entryUUID=group)))",
-		Attributes: []string{"cn", "entryUUID", "member"},
-		Controls:   []ldap.Control(nil),
-	}
 	sr2 := &ldap.SearchRequest{
 		BaseDN:     "uid=user,ou=people,dc=test",
 		SizeLimit:  1,
@@ -112,7 +124,7 @@ func TestGetGroup(t *testing.T) {
 		Controls:   []ldap.Control(nil),
 	}
 
-	lm.On("Search", sr1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{groupEntry}}, nil)
+	lm.On("Search", groupLookupSearchRequest).Return(&ldap.SearchResult{Entries: []*ldap.Entry{groupEntry}}, nil)
 	lm.On("Search", sr2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{userEntry}}, nil)
 	lm.On("Search", sr3).Return(&ldap.SearchResult{Entries: []*ldap.Entry{invalidUserEntry}}, nil)
 	b, _ = getMockedBackend(lm, lconfig, &logger)
@@ -146,13 +158,80 @@ func TestGetGroup(t *testing.T) {
 	}
 }
 
-func TestGetGroups(t *testing.T) {
-	queryParamExpand := url.Values{
-		"$expand": []string{"members"},
+func TestGetGroupReadOnlyBackend(t *testing.T) {
+	readOnlyConfig := lconfig
+	readOnlyConfig.WriteEnabled = false
+
+	lm := &mocks.Client{}
+	lm.On("Search", groupLookupSearchRequest).Return(&ldap.SearchResult{Entries: []*ldap.Entry{groupEntry}}, nil)
+	b, _ := getMockedBackend(lm, readOnlyConfig, &logger)
+	g, err := b.GetGroup(context.Background(), "group", url.Values{})
+	switch {
+	case err != nil:
+		t.Errorf("Expected GetGroup to succeed. Got %s", err.Error())
+	case g.GetId() != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id):
+		t.Errorf("Expected GetGroup to return a valid group")
+	}
+	types := g.GetGroupTypes()
+	switch {
+	case len(types) == 0:
+		t.Errorf("No groupTypes attribute on readonly Group")
+	case len(types) > 1:
+		t.Errorf("Expected a single groupTypes value on readonly Group")
+	case types[0] != "ReadOnly":
+		t.Errorf("Expected a groupTypes 'ReadOnly' on readonly Group")
+	}
+}
+func TestGetGroupReadOnlySubtree(t *testing.T) {
+	readOnlyTreeConfig := lconfig
+	readOnlyTreeConfig.GroupCreateBaseDN = "ou=write,ou=groups,dc=test"
+	var writeGroupEntry = ldap.NewEntry("cn=group,ou=write,ou=groups,dc=test",
+		map[string][]string{
+			"cn":        {"group"},
+			"entryuuid": {"abcd-defg"},
+			"member": {
+				"uid=user,ou=people,dc=test",
+				"uid=invalid,ou=people,dc=test",
+			},
+		})
+
+	lm := &mocks.Client{}
+	lm.On("Search", groupLookupSearchRequest).Return(&ldap.SearchResult{Entries: []*ldap.Entry{groupEntry}}, nil)
+	b, _ := getMockedBackend(lm, readOnlyTreeConfig, &logger)
+	g, err := b.GetGroup(context.Background(), "group", url.Values{})
+	switch {
+	case err != nil:
+		t.Errorf("Expected GetGroup to succeed. Got %s", err.Error())
+	case g.GetId() != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id):
+		t.Errorf("Expected GetGroup to return a valid group")
 	}
-	queryParamSelect := url.Values{
-		"$select": []string{"members"},
+	types := g.GetGroupTypes()
+	switch {
+	case len(types) == 0:
+		t.Errorf("No groupTypes attribute on readonly Group")
+	case len(types) > 1:
+		t.Errorf("Expected a single groupTypes value on readonly Group")
+	case types[0] != "ReadOnly":
+		t.Errorf("Expected a groupTypes 'ReadOnly' on readonly Group")
 	}
+
+	lm = &mocks.Client{}
+	lm.On("Search", groupLookupSearchRequest).Return(&ldap.SearchResult{Entries: []*ldap.Entry{writeGroupEntry}}, nil)
+	b, _ = getMockedBackend(lm, readOnlyTreeConfig, &logger)
+	g, err = b.GetGroup(context.Background(), "group", url.Values{})
+	switch {
+	case err != nil:
+		t.Errorf("Expected GetGroup to succeed. Got %s", err.Error())
+	case g.GetId() != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id):
+		t.Errorf("Expected GetGroup to return a valid group")
+	}
+	types = g.GetGroupTypes()
+	if len(types) != 0 {
+		t.Errorf("No groupTypes attribute expected on writeable Group")
+	}
+}
+
+func TestGetGroups(t *testing.T) {
 	lm := &mocks.Client{}
 	lm.On("Search", mock.Anything).Return(nil, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("mock")))
 	b, _ := getMockedBackend(lm, lconfig, &logger)
@@ -185,13 +264,6 @@ func TestGetGroups(t *testing.T) {
 
 	// Mock a valid	Search Result with expanded group members
 	lm = &mocks.Client{}
-	sr1 := &ldap.SearchRequest{
-		BaseDN:     "ou=groups,dc=test",
-		Scope:      2,
-		Filter:     "(&(objectClass=groupOfNames))",
-		Attributes: []string{"cn", "entryUUID", "member"},
-		Controls:   []ldap.Control(nil),
-	}
 	sr2 := &ldap.SearchRequest{
 		BaseDN:     "uid=user,ou=people,dc=test",
 		SizeLimit:  1,
@@ -208,7 +280,7 @@ func TestGetGroups(t *testing.T) {
 	}
 
 	for _, param := range []url.Values{queryParamSelect, queryParamExpand} {
-		lm.On("Search", sr1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{groupEntry}}, nil)
+		lm.On("Search", groupListSeachRequest).Return(&ldap.SearchResult{Entries: []*ldap.Entry{groupEntry}}, nil)
 		lm.On("Search", sr2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{userEntry}}, nil)
 		lm.On("Search", sr3).Return(&ldap.SearchResult{Entries: []*ldap.Entry{invalidUserEntry}}, nil)
 		b, _ = getMockedBackend(lm, lconfig, &logger)
diff --git a/tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md b/tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md
index 8938aacdf50..ed335cdf4ec 100644
--- a/tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md
+++ b/tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md
@@ -62,9 +62,6 @@ The expected failures in this file are from features in the owncloud/ocis repo.
 #### [Share lists deleted user as 'user'](https://github.com/owncloud/ocis/issues/903)
 - [apiGraph/deleteGroup.feature:68](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiGraph/deleteGroup.feature#L68)
 
-#### [Updating group displayName request seems OK but group is not being renamed](https://github.com/owncloud/ocis/issues/5977)
-- [apiGraph/editGroup.feature:23](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiGraph/editGroup.feature#L23)
-
 #### [CORS headers are not identical with oC10 headers](https://github.com/owncloud/ocis/issues/5195)
 - [apiCors/cors.feature:25](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiCors/cors.feature#L25)
 - [apiCors/cors.feature:26](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiCors/cors.feature#L26)