Repository: owncloud/ocis
Pull request #5101 (2022-11-23 09:33:01)

[tests-only][full-ci] Add API tests for editing group displayName (graph API)
## Description
Added API tests for editing group display name

Added scenarios:
1. `Scenario Outline: admin user renames a group`
```feature
      | grp1      | grp101        |
      | grp1      | España§àôœ€   |
      | grp1      | नेपाली          |
      | grp1      | $x<=>[y*z^2]! |
      | grp1      | staff?group   |
      | grp1      | 50%pass       |
```

## Related Issue
- Part of https://github.com/owncloud/ocis/issues/4937

## How Has This Been Tested?
- test environment: local

## Screenshots (if appropriate):

## Types of changes
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Technical debt
- [x] Tests only (no source changes)

## Checklist:
- [ ] Code changes
- [ ] Unit tests added
- [x] Acceptance tests added
- [ ] Documentation ticket raised: <link> 


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=5101)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5101&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=5101&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5101&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=5101&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=5101&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5101&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=5101&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=5101&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5101&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=5101&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=5101&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5101&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5101&metric=coverage&view=list) No Coverage information  
[![No Duplication information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo-16px.png 'No Duplication information')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5101&metric=duplicated_lines_density&view=list) No Duplication information



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7f9aeff5e4f..57930beb4ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ The following sections list the changes for unreleased.
 * Bugfix - Adjust cache related configuration options: [#5087](https://github.com/owncloud/ocis/pull/5087)
 * Bugfix - Fix notifications Web UI url: [#4998](https://github.com/owncloud/ocis/pull/4998)
 * Bugfix - Initial role assingment with external IDM: [#5045](https://github.com/owncloud/ocis/issues/5045)
+* Bugfix - Update reva: [#5092](https://github.com/owncloud/ocis/pull/5092)
 * Enhancement - Add the "hidden" state to the search index: [#5018](https://github.com/owncloud/ocis/pull/5018)
 * Enhancement - Remove windows from ci & release makefile: [#5026](https://github.com/owncloud/ocis/pull/5026)
 * Enhancement - Update ownCloud Web to v6.0.0-rc.6: [#5086](https://github.com/owncloud/ocis/pull/5086)
@@ -60,6 +61,20 @@ The following sections list the changes for unreleased.
 
    https://github.com/owncloud/ocis/issues/5045
 
+* Bugfix - Update reva: [#5092](https://github.com/owncloud/ocis/pull/5092)
+
+   Tags: reva
+
+   We updated reva to pull in several Bugfixes: * Bugfix
+   [owncloud/ocis#5061](https://github.com/owncloud/ocis/issues/5061): [Performance]
+   Upload speed drops while uploading 1k files in a folder * Bugfix
+   [owncloud/ocis#3868](https://github.com/owncloud/ocis/issues/3868): Downloading
+   the older version of shared file as share receiver gives 403 * Bugfix
+   [owncloud/ocis#765](https://github.com/owncloud/ocis/issues/765): [OCIS-storage]
+   Restoring an older version of a shared file deletes the share
+
+   https://github.com/owncloud/ocis/pull/5092
+
 * Enhancement - Add the "hidden" state to the search index: [#5018](https://github.com/owncloud/ocis/pull/5018)
 
    We changed the search service to store the "hidden" state of entries in the search index. That
diff --git a/changelog/unreleased/update-reva.md b/changelog/unreleased/update-reva.md
new file mode 100644
index 00000000000..acff2b50881
--- /dev/null
+++ b/changelog/unreleased/update-reva.md
@@ -0,0 +1,10 @@
+Bugfix: update reva
+
+Tags: reva
+
+We updated reva to pull in several Bugfixes:
+* Bugfix [owncloud/ocis#5061](https://github.com/owncloud/ocis/issues/5061): [Performance] Upload speed drops while uploading 1k files in a folder
+* Bugfix [owncloud/ocis#3868](https://github.com/owncloud/ocis/issues/3868): Downloading the older version of shared file as share receiver gives 403
+* Bugfix [owncloud/ocis#765](https://github.com/owncloud/ocis/issues/765): [OCIS-storage] Restoring an older version of a shared file deletes the share
+
+https://github.com/owncloud/ocis/pull/5092
diff --git a/deployments/examples/oc10_ocis_parallel/config/ldap/ldif/30_groups.ldif b/deployments/examples/oc10_ocis_parallel/config/ldap/ldif/30_groups.ldif
index 8617b977f44..a43edf133dd 100644
--- a/deployments/examples/oc10_ocis_parallel/config/ldap/ldif/30_groups.ldif
+++ b/deployments/examples/oc10_ocis_parallel/config/ldap/ldif/30_groups.ldif
@@ -3,95 +3,79 @@ objectClass: organizationalUnit
 ou: groups
 
 dn: cn=users,ou=groups,dc=owncloud,dc=com
-objectClass: groupOfUniqueNames
-objectClass: posixGroup
+objectClass: groupOfNames
 objectClass: ownCloud
 objectClass: top
 cn: users
 description: Users
-gidNumber: 30000
 ownCloudUUID:: NTA5YTlkY2QtYmIzNy00ZjRmLWEwMWEtMTlkY2EyN2Q5Y2Zh
-uniqueMember: uid=einstein,ou=users,dc=owncloud,dc=com
-uniqueMember: uid=marie,ou=users,dc=owncloud,dc=com
-uniqueMember: uid=richard,ou=users,dc=owncloud,dc=com
-uniqueMember: uid=moss,ou=users,dc=owncloud,dc=com
-uniqueMember: uid=admin,ou=users,dc=owncloud,dc=com
+member: uid=einstein,ou=users,dc=owncloud,dc=com
+member: uid=marie,ou=users,dc=owncloud,dc=com
+member: uid=richard,ou=users,dc=owncloud,dc=com
+member: uid=moss,ou=users,dc=owncloud,dc=com
+member: uid=admin,ou=users,dc=owncloud,dc=com
 
 dn: cn=sailing-lovers,ou=groups,dc=owncloud,dc=com
-objectClass: groupOfUniqueNames
-objectClass: posixGroup
+objectClass: groupOfNames
 objectClass: ownCloud
 objectClass: top
 cn: sailing-lovers
 description: Sailing lovers
-gidNumber: 30001
 ownCloudUUID:: NjA0MGFhMTctOWM2NC00ZmVmLTliZDAtNzcyMzRkNzFiYWQw
-uniqueMember: uid=einstein,ou=users,dc=owncloud,dc=com
+member: uid=einstein,ou=users,dc=owncloud,dc=com
 
 dn: cn=violin-haters,ou=groups,dc=owncloud,dc=com
-objectClass: groupOfUniqueNames
-objectClass: posixGroup
+objectClass: groupOfNames
 objectClass: ownCloud
 objectClass: top
 cn: violin-haters
 description: Violin haters
-gidNumber: 30002
 ownCloudUUID:: ZGQ1OGU1ZWMtODQyZS00OThiLTg4MDAtNjFmMmVjNmY5MTFm
-uniqueMember: uid=einstein,ou=users,dc=owncloud,dc=com
+member: uid=einstein,ou=users,dc=owncloud,dc=com
 
 dn: cn=radium-lovers,ou=groups,dc=owncloud,dc=com
-objectClass: groupOfUniqueNames
-objectClass: posixGroup
+objectClass: groupOfNames
 objectClass: ownCloud
 objectClass: top
 cn: radium-lovers
 description: Radium lovers
-gidNumber: 30003
 ownCloudUUID:: N2I4N2ZkNDktMjg2ZS00YTVmLWJhZmQtYzUzNWQ1ZGQ5OTdh
-uniqueMember: uid=marie,ou=users,dc=owncloud,dc=com
+member: uid=marie,ou=users,dc=owncloud,dc=com
 
 dn: cn=polonium-lovers,ou=groups,dc=owncloud,dc=com
-objectClass: groupOfUniqueNames
-objectClass: posixGroup
+objectClass: groupOfNames
 objectClass: ownCloud
 objectClass: top
 cn: polonium-lovers
 description: Polonium lovers
-gidNumber: 30004
 ownCloudUUID:: Y2VkYzIxYWEtNDA3Mi00NjE0LTg2NzYtZmE5MTY1ZjU5OGZm
-uniqueMember: uid=marie,ou=users,dc=owncloud,dc=com
+member: uid=marie,ou=users,dc=owncloud,dc=com
 
 dn: cn=quantum-lovers,ou=groups,dc=owncloud,dc=com
-objectClass: groupOfUniqueNames
-objectClass: posixGroup
+objectClass: groupOfNames
 objectClass: ownCloud
 objectClass: top
 cn: quantum-lovers
 description: Quantum lovers
-gidNumber: 30005
 ownCloudUUID:: YTE3MjYxMDgtMDFmOC00YzMwLTg4ZGYtMmIxYTlkMWNiYTFh
-uniqueMember: uid=richard,ou=users,dc=owncloud,dc=com
+member: uid=richard,ou=users,dc=owncloud,dc=com
 
 dn: cn=philosophy-haters,ou=groups,dc=owncloud,dc=com
-objectClass: groupOfUniqueNames
-objectClass: posixGroup
+objectClass: groupOfNames
 objectClass: ownCloud
 objectClass: top
 cn: philosophy-haters
 description: Philosophy haters
-gidNumber: 30006
 ownCloudUUID:: MTY3Y2JlZTItMDUxOC00NTVhLWJmYjItMDMxZmUwNjIxZTVk
-uniqueMember: uid=richard,ou=users,dc=owncloud,dc=com
+member: uid=richard,ou=users,dc=owncloud,dc=com
 
 dn: cn=physics-lovers,ou=groups,dc=owncloud,dc=com
-objectClass: groupOfUniqueNames
-objectClass: posixGroup
+objectClass: groupOfNames
 objectClass: ownCloud
 objectClass: top
 cn: physics-lovers
 description: Physics lovers
-gidNumber: 30007
 ownCloudUUID:: MjYyOTgyYzEtMjM2Mi00YWZhLWJmZGYtOGNiZmVmNjRhMDZl
-uniqueMember: uid=einstein,ou=users,dc=owncloud,dc=com
-uniqueMember: uid=marie,ou=users,dc=owncloud,dc=com
-uniqueMember: uid=richard,ou=users,dc=owncloud,dc=com
+member: uid=einstein,ou=users,dc=owncloud,dc=com
+member: uid=marie,ou=users,dc=owncloud,dc=com
+member: uid=richard,ou=users,dc=owncloud,dc=com
diff --git a/deployments/examples/ocis_ldap/config/ldap/ldif/30_groups.ldif b/deployments/examples/ocis_ldap/config/ldap/ldif/30_groups.ldif
index 8617b977f44..a43edf133dd 100644
--- a/deployments/examples/ocis_ldap/config/ldap/ldif/30_groups.ldif
+++ b/deployments/examples/ocis_ldap/config/ldap/ldif/30_groups.ldif
@@ -3,95 +3,79 @@ objectClass: organizationalUnit
 ou: groups
 
 dn: cn=users,ou=groups,dc=owncloud,dc=com
-objectClass: groupOfUniqueNames
-objectClass: posixGroup
+objectClass: groupOfNames
 objectClass: ownCloud
 objectClass: top
 cn: users
 description: Users
-gidNumber: 30000
 ownCloudUUID:: NTA5YTlkY2QtYmIzNy00ZjRmLWEwMWEtMTlkY2EyN2Q5Y2Zh
-uniqueMember: uid=einstein,ou=users,dc=owncloud,dc=com
-uniqueMember: uid=marie,ou=users,dc=owncloud,dc=com
-uniqueMember: uid=richard,ou=users,dc=owncloud,dc=com
-uniqueMember: uid=moss,ou=users,dc=owncloud,dc=com
-uniqueMember: uid=admin,ou=users,dc=owncloud,dc=com
+member: uid=einstein,ou=users,dc=owncloud,dc=com
+member: uid=marie,ou=users,dc=owncloud,dc=com
+member: uid=richard,ou=users,dc=owncloud,dc=com
+member: uid=moss,ou=users,dc=owncloud,dc=com
+member: uid=admin,ou=users,dc=owncloud,dc=com
 
 dn: cn=sailing-lovers,ou=groups,dc=owncloud,dc=com
-objectClass: groupOfUniqueNames
-objectClass: posixGroup
+objectClass: groupOfNames
 objectClass: ownCloud
 objectClass: top
 cn: sailing-lovers
 description: Sailing lovers
-gidNumber: 30001
 ownCloudUUID:: NjA0MGFhMTctOWM2NC00ZmVmLTliZDAtNzcyMzRkNzFiYWQw
-uniqueMember: uid=einstein,ou=users,dc=owncloud,dc=com
+member: uid=einstein,ou=users,dc=owncloud,dc=com
 
 dn: cn=violin-haters,ou=groups,dc=owncloud,dc=com
-objectClass: groupOfUniqueNames
-objectClass: posixGroup
+objectClass: groupOfNames
 objectClass: ownCloud
 objectClass: top
 cn: violin-haters
 description: Violin haters
-gidNumber: 30002
 ownCloudUUID:: ZGQ1OGU1ZWMtODQyZS00OThiLTg4MDAtNjFmMmVjNmY5MTFm
-uniqueMember: uid=einstein,ou=users,dc=owncloud,dc=com
+member: uid=einstein,ou=users,dc=owncloud,dc=com
 
 dn: cn=radium-lovers,ou=groups,dc=owncloud,dc=com
-objectClass: groupOfUniqueNames
-objectClass: posixGroup
+objectClass: groupOfNames
 objectClass: ownCloud
 objectClass: top
 cn: radium-lovers
 description: Radium lovers
-gidNumber: 30003
 ownCloudUUID:: N2I4N2ZkNDktMjg2ZS00YTVmLWJhZmQtYzUzNWQ1ZGQ5OTdh
-uniqueMember: uid=marie,ou=users,dc=owncloud,dc=com
+member: uid=marie,ou=users,dc=owncloud,dc=com
 
 dn: cn=polonium-lovers,ou=groups,dc=owncloud,dc=com
-objectClass: groupOfUniqueNames
-objectClass: posixGroup
+objectClass: groupOfNames
 objectClass: ownCloud
 objectClass: top
 cn: polonium-lovers
 description: Polonium lovers
-gidNumber: 30004
 ownCloudUUID:: Y2VkYzIxYWEtNDA3Mi00NjE0LTg2NzYtZmE5MTY1ZjU5OGZm
-uniqueMember: uid=marie,ou=users,dc=owncloud,dc=com
+member: uid=marie,ou=users,dc=owncloud,dc=com
 
 dn: cn=quantum-lovers,ou=groups,dc=owncloud,dc=com
-objectClass: groupOfUniqueNames
-objectClass: posixGroup
+objectClass: groupOfNames
 objectClass: ownCloud
 objectClass: top
 cn: quantum-lovers
 description: Quantum lovers
-gidNumber: 30005
 ownCloudUUID:: YTE3MjYxMDgtMDFmOC00YzMwLTg4ZGYtMmIxYTlkMWNiYTFh
-uniqueMember: uid=richard,ou=users,dc=owncloud,dc=com
+member: uid=richard,ou=users,dc=owncloud,dc=com
 
 dn: cn=philosophy-haters,ou=groups,dc=owncloud,dc=com
-objectClass: groupOfUniqueNames
-objectClass: posixGroup
+objectClass: groupOfNames
 objectClass: ownCloud
 objectClass: top
 cn: philosophy-haters
 description: Philosophy haters
-gidNumber: 30006
 ownCloudUUID:: MTY3Y2JlZTItMDUxOC00NTVhLWJmYjItMDMxZmUwNjIxZTVk
-uniqueMember: uid=richard,ou=users,dc=owncloud,dc=com
+member: uid=richard,ou=users,dc=owncloud,dc=com
 
 dn: cn=physics-lovers,ou=groups,dc=owncloud,dc=com
-objectClass: groupOfUniqueNames
-objectClass: posixGroup
+objectClass: groupOfNames
 objectClass: ownCloud
 objectClass: top
 cn: physics-lovers
 description: Physics lovers
-gidNumber: 30007
 ownCloudUUID:: MjYyOTgyYzEtMjM2Mi00YWZhLWJmZGYtOGNiZmVmNjRhMDZl
-uniqueMember: uid=einstein,ou=users,dc=owncloud,dc=com
-uniqueMember: uid=marie,ou=users,dc=owncloud,dc=com
-uniqueMember: uid=richard,ou=users,dc=owncloud,dc=com
+member: uid=einstein,ou=users,dc=owncloud,dc=com
+member: uid=marie,ou=users,dc=owncloud,dc=com
+member: uid=richard,ou=users,dc=owncloud,dc=com
diff --git a/deployments/examples/ocis_ldap/docker-compose.yml b/deployments/examples/ocis_ldap/docker-compose.yml
index 31fb754ae76..b5538240ee0 100644
--- a/deployments/examples/ocis_ldap/docker-compose.yml
+++ b/deployments/examples/ocis_ldap/docker-compose.yml
@@ -66,7 +66,7 @@ services:
       LDAP_BIND_PASSWORD: ${LDAP_ADMIN_PASSWORD:-admin}
       LDAP_GROUP_BASE_DN: "dc=owncloud,dc=com"
       LDAP_GROUP_FILTER: "(objectclass=owncloud)"
-      LDAP_GROUP_OBJECTCLASS: "groupOfUniqueNames"
+      LDAP_GROUP_OBJECTCLASS: "groupOfNames"
       LDAP_USER_BASE_DN: "dc=owncloud,dc=com"
       LDAP_USER_FILTER: "(objectclass=owncloud)"
       LDAP_USER_OBJECTCLASS: "inetOrgPerson"
diff --git a/go.mod b/go.mod
index 25eb4faeeec..e8868c80af8 100644
--- a/go.mod
+++ b/go.mod
@@ -10,7 +10,7 @@ require (
 	github.com/blevesearch/bleve/v2 v2.3.5
 	github.com/coreos/go-oidc/v3 v3.4.0
 	github.com/cs3org/go-cs3apis v0.0.0-20221012090518-ef2996678965
-	github.com/cs3org/reva/v2 v2.10.1-0.20221111140957-723ad781d916
+	github.com/cs3org/reva/v2 v2.10.1-0.20221123110342-cf8ce449fc83
 	github.com/disintegration/imaging v1.6.2
 	github.com/ggwhite/go-masker v1.0.9
 	github.com/go-chi/chi/v5 v5.0.7
@@ -19,8 +19,8 @@ require (
 	github.com/go-ldap/ldap/v3 v3.4.4
 	github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3
 	github.com/go-micro/plugins/v4/client/grpc v1.1.0
-	github.com/go-micro/plugins/v4/events/natsjs v1.1.0
-	github.com/go-micro/plugins/v4/logger/zerolog v1.1.0
+	github.com/go-micro/plugins/v4/events/natsjs v1.2.0
+	github.com/go-micro/plugins/v4/logger/zerolog v1.2.0
 	github.com/go-micro/plugins/v4/registry/consul v1.2.0
 	github.com/go-micro/plugins/v4/registry/etcd v1.2.0
 	github.com/go-micro/plugins/v4/registry/kubernetes v1.1.0
diff --git a/go.sum b/go.sum
index 969058a1354..4f6e1dcce0b 100644
--- a/go.sum
+++ b/go.sum
@@ -342,8 +342,8 @@ github.com/crewjam/saml v0.4.6 h1:XCUFPkQSJLvzyl4cW9OvpWUbRf0gE7VUpU8ZnilbeM4=
 github.com/crewjam/saml v0.4.6/go.mod h1:ZBOXnNPFzB3CgOkRm7Nd6IVdkG+l/wF+0ZXLqD96t1A=
 github.com/cs3org/go-cs3apis v0.0.0-20221012090518-ef2996678965 h1:y4n2j68LLnvac+zw/al8MfPgO5aQiIwLmHM/JzYN8AM=
 github.com/cs3org/go-cs3apis v0.0.0-20221012090518-ef2996678965/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
-github.com/cs3org/reva/v2 v2.10.1-0.20221111140957-723ad781d916 h1:uzhlNkkzwS1Gk/6hqUE445HFV2XpGTdd77hLVnyhvhc=
-github.com/cs3org/reva/v2 v2.10.1-0.20221111140957-723ad781d916/go.mod h1:+lH5G0UmNjMNj4F0bDhbh+HqL1UihlbL8zPBa57Y2QI=
+github.com/cs3org/reva/v2 v2.10.1-0.20221123110342-cf8ce449fc83 h1:cyYWkYxEFr08PDMQHVxOyCAUzCT+nh0qsFDmMYiK2O8=
+github.com/cs3org/reva/v2 v2.10.1-0.20221123110342-cf8ce449fc83/go.mod h1:+lH5G0UmNjMNj4F0bDhbh+HqL1UihlbL8zPBa57Y2QI=
 github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8 h1:Z9lwXumT5ACSmJ7WGnFl+OMLLjpz5uR2fyz7dC255FI=
 github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8/go.mod h1:4abs/jPXcmJzYoYGF91JF9Uq9s/KL5n1jvFDix8KcqY=
 github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
@@ -474,10 +474,10 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-micro/plugins/v4/client/grpc v1.1.0 h1:1MlhnyLxlZpzYY7gepj5zGJNn06yL+3OBbDfh0Z6jH0=
 github.com/go-micro/plugins/v4/client/grpc v1.1.0/go.mod h1:7mskyLlKoKT2V5mC4mdBcrpDx+CiLZxXtTMD+jpFuRY=
-github.com/go-micro/plugins/v4/events/natsjs v1.1.0 h1:eQ3JbN0mnLuiExBM29pRi/EhlefWv3DTtdMk3BljAi4=
-github.com/go-micro/plugins/v4/events/natsjs v1.1.0/go.mod h1:ibNwJyP1Z5y8unW4sryIn8EkdwGkrcaURGwm0DVeMcY=
-github.com/go-micro/plugins/v4/logger/zerolog v1.1.0 h1:sG20ktK9YN/h0TlmQXTuL4/yavxBy5QbzifcVARN5ww=
-github.com/go-micro/plugins/v4/logger/zerolog v1.1.0/go.mod h1:lUEQAHt8I+gJDmx3DbpU7aqwM3+MwFmFRnATxlp650w=
+github.com/go-micro/plugins/v4/events/natsjs v1.2.0 h1:c+iG3FholfRJI27QkIVPWOERazwDZro4GLl6Odx2g1c=
+github.com/go-micro/plugins/v4/events/natsjs v1.2.0/go.mod h1:lYuiEYKQTpbE2LA8HEcC8D6kQ29M7ILfEak3dzeucEg=
+github.com/go-micro/plugins/v4/logger/zerolog v1.2.0 h1:JZ516VQ9zekRoi868XG7x0EWxZ2AMq/euHIBChITsTI=
+github.com/go-micro/plugins/v4/logger/zerolog v1.2.0/go.mod h1:AieYOIeOxobYa5B8WGEqxXM3Ndi26tDIu9fZ4RYkCvQ=
 github.com/go-micro/plugins/v4/registry/consul v1.2.0 h1:nqrTzfWUTWKAy+M+i2FazbHnQn4m77EOtoch57kbCH4=
 github.com/go-micro/plugins/v4/registry/consul v1.2.0/go.mod h1:wTat7/K9XQ+i64VbbcMYFcEwipYfSgJM51HcA/sgsM4=
 github.com/go-micro/plugins/v4/registry/etcd v1.2.0 h1:tcHlU1GzvX3oZa8WQH8ylMCGie5qD5g98YWTESJjeqQ=
diff --git a/ocis-pkg/shared/shared_types.go b/ocis-pkg/shared/shared_types.go
index 5afabd10603..855c409398c 100644
--- a/ocis-pkg/shared/shared_types.go
+++ b/ocis-pkg/shared/shared_types.go
@@ -37,19 +37,20 @@ type Reva struct {
 
 type GRPCClientTLS struct {
 	Mode   string `yaml:"mode" env:"OCIS_GRPC_CLIENT_TLS_MODE" desc:"TLS mode for grpc connection to the go-micro based grpc services. Possible values are 'off', 'insecure' and 'on'. 'off': disables transport security for the clients. 'insecure' allows to use transport security, but disables certificate verification (to be used with the autogenerated self-signed certificates). 'on' enables transport security, including server ceritificate verification."`
-	CACert string `yaml:"cacert" env:"OCIS_GRPC_CLIENT_TLS_CACERT" desc:"The root CA certificate used to validate TLS server certificates of the go-micro based grpc services."`
+	CACert string `yaml:"cacert" env:"OCIS_GRPC_CLIENT_TLS_CACERT" desc:"Path/File name for the root CA certificate (in PEM format) used to validate TLS server certificates of the go-micro based grpc services."`
 }
 
 type GRPCServiceTLS struct {
-	Enabled bool   `yaml:"enabled" env:"OCIS_GRPC_TLS_ENABLED" desc:"Activates TLS for the grpcs based services using the server certifcate and key configured via OCIS_GRPC_TLS_CERTIFICATE and OCIS_GRPC_TLS_KEY. If OCIS_GRPC_TLS_CERTIFICATE is not set a temporary server certificate is generated - to be used with OCIS_GRPC_CLIENT_TLS_MODE=insecure."`
+	Enabled bool   `yaml:"enabled" env:"OCIS_GRPC_TLS_ENABLED" desc:"Activates TLS for the grpc based services using the server certifcate and key configured via OCIS_GRPC_TLS_CERTIFICATE and OCIS_GRPC_TLS_KEY. If OCIS_GRPC_TLS_CERTIFICATE is not set a temporary server certificate is generated - to be used with OCIS_GRPC_CLIENT_TLS_MODE=insecure."`
 	Cert    string `yaml:"cert" env:"OCIS_GRPC_TLS_CERTIFICATE" desc:"Path/File name of the TLS server certificate (in PEM format) for the grpc services."`
 	Key     string `yaml:"key" env:"OCIS_GRPC_TLS_KEY" desc:"Path/File name for the TLS certificate key (in PEM format) for the server certificate to use for the grpc services."`
 }
 
 type HTTPServiceTLS struct {
-	Enabled bool   `yaml:"enabled" env:"OCIS_HTTP_TLS_ENABLED"`
-	Cert    string `yaml:"cert" env:"OCIS_HTTP_TLS_CERTIFICATE" desc:"Path/File name of the TLS server certificate (in PEM format) for the http services."`
-	Key     string `yaml:"key" env:"OCIS_HTTP_TLS_KEY" desc:"Path/File name for the TLS certificate key (in PEM format) for the server certificate to use for the http services."`
+	Enabled bool `yaml:"enabled" env:"OCIS_HTTP_TLS_ENABLED" desc:"Activates TLS for the http based services using the server certifcate and key configured via OCIS_HTTP_TLS_CERTIFICATE and OCIS_HTTP_TLS_KEY. If OCIS_HTTP_TLS_CERTIFICATE is not set a temporary server certificate is generated - to be used with PROXY_INSECURE_BACKEND=true."`
+
+	Cert string `yaml:"cert" env:"OCIS_HTTP_TLS_CERTIFICATE" desc:"Path/File name of the TLS server certificate (in PEM format) for the http services."`
+	Key  string `yaml:"key" env:"OCIS_HTTP_TLS_KEY" desc:"Path/File name for the TLS certificate key (in PEM format) for the server certificate to use for the http services."`
 }
 
 type CacheStore struct {
diff --git a/services/app-provider/pkg/config/config.go b/services/app-provider/pkg/config/config.go
index 15681f07edd..046453b80e1 100644
--- a/services/app-provider/pkg/config/config.go
+++ b/services/app-provider/pkg/config/config.go
@@ -69,7 +69,7 @@ type WOPIDriver struct {
 	AppInternalURL            string `yaml:"app_internal_url" env:"APP_PROVIDER_WOPI_APP_INTERNAL_URL" desc:"Internal URL to the app, like in your DMZ."`
 	AppName                   string `yaml:"app_name" env:"APP_PROVIDER_WOPI_APP_NAME" desc:"Human readable app name."`
 	AppURL                    string `yaml:"app_url" env:"APP_PROVIDER_WOPI_APP_URL" desc:"URL for end users to access the app."`
-	Insecure                  bool   `yaml:"insecure" env:"APP_PROVIDER_WOPI_INSECURE" desc:"Allow insecure connections to the app."`
+	Insecure                  bool   `yaml:"insecure" env:"APP_PROVIDER_WOPI_INSECURE" desc:"Disable TLS certificate validation for requests to the WOPI server and the web office application. Do not set this in production environments."`
 	IopSecret                 string `yaml:"wopi_server_iop_secret" env:"APP_PROVIDER_WOPI_WOPI_SERVER_IOP_SECRET" desc:"Shared secret of the CS3org WOPI server."`
 	WopiURL                   string `yaml:"wopi_server_external_url" env:"APP_PROVIDER_WOPI_WOPI_SERVER_EXTERNAL_URL" desc:"External url of the CS3org WOPI server."`
 	WopiFolderURLBaseURL      string `yaml:"wopi_folder_url_base_url" env:"OCIS_URL;APP_PROVIDER_WOPI_FOLDER_URL_BASE_URL" desc:"Base url to navigate back from the app the containing folder in the file list."`
diff --git a/services/audit/pkg/service/service.go b/services/audit/pkg/service/service.go
index 621eaf8ec80..2f12a7800ed 100644
--- a/services/audit/pkg/service/service.go
+++ b/services/audit/pkg/service/service.go
@@ -35,6 +35,8 @@ func AuditLoggerFromConfig(ctx context.Context, cfg config.Auditlog, ch <-chan i
 }
 
 // StartAuditLogger will block. run in separate go routine
+//
+//nolint:gocyclo
 func StartAuditLogger(ctx context.Context, ch <-chan interface{}, log log.Logger, marshaller Marshaller, logto ...Log) {
 	for {
 		select {
@@ -87,6 +89,12 @@ func StartAuditLogger(ctx context.Context, ch <-chan interface{}, log log.Logger
 				auditEvent = types.SpaceEnabled(ev)
 			case events.SpaceDeleted:
 				auditEvent = types.SpaceDeleted(ev)
+			case events.SpaceShared:
+				auditEvent = types.SpaceShared(ev)
+			case events.SpaceUnshared:
+				auditEvent = types.SpaceUnshared(ev)
+			case events.SpaceUpdated:
+				auditEvent = types.SpaceUpdated(ev)
 			case events.UserCreated:
 				auditEvent = types.UserCreated(ev)
 			case events.UserDeleted:
diff --git a/services/audit/pkg/types/constants.go b/services/audit/pkg/types/constants.go
index 29d91972429..e41f53ba6c6 100644
--- a/services/audit/pkg/types/constants.go
+++ b/services/audit/pkg/types/constants.go
@@ -36,6 +36,9 @@ const (
 	ActionSpaceDisabled = "space_disabled"
 	ActionSpaceEnabled  = "space_enabled"
 	ActionSpaceDeleted  = "space_deleted"
+	ActionSpaceShared   = "space_shared"
+	ActionSpaceUnshared = "space_unshared"
+	ActionSpaceUpdated  = "space_updated"
 
 	// Users
 	ActionUserCreated        = "user_created"
@@ -159,6 +162,22 @@ func MessageSpaceDeleted(executant, spaceID string) string {
 	return fmt.Sprintf("user '%s' deleted the space '%s'", executant, spaceID)
 }
 
+// MessageSpaceShared returns the human readable string that describes the action
+func MessageSpaceShared(executant, spaceID, grantee string) string {
+	return fmt.Sprintf("user '%s' shared the space '%s' with '%s'", executant, spaceID, grantee)
+}
+
+// MessageSpaceUnshared returns the human readable string that describes the action
+func MessageSpaceUnshared(executant, spaceID, grantee string) string {
+	return fmt.Sprintf("user '%s' unshared the space '%s' with '%s'", executant, spaceID, grantee)
+}
+
+// MessageSpaceUpdated returns the human readable string that describes the action
+func MessageSpaceUpdated(executant, spaceID, name string, quota uint64, opaque map[string]string) string {
+	return fmt.Sprintf("user '%s' updated space '%s'. name: '%s', quota: '%d', opaque: '%s'",
+		executant, spaceID, name, quota, opaque)
+}
+
 // MessageUserCreated returns the human readable string that describes the action
 func MessageUserCreated(executant, userID string) string {
 	return fmt.Sprintf("user '%s' created the user '%s'", executant, userID)
diff --git a/services/audit/pkg/types/conversion.go b/services/audit/pkg/types/conversion.go
index c98b2caf491..dc433d569d4 100644
--- a/services/audit/pkg/types/conversion.go
+++ b/services/audit/pkg/types/conversion.go
@@ -11,6 +11,7 @@ import (
 	user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
 	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
+	sdk "github.com/cs3org/reva/v2/pkg/sdk/common"
 )
 
 const _linktype = "link"
@@ -380,6 +381,59 @@ func SpaceDeleted(ev events.SpaceDeleted) AuditEventSpaceDeleted {
 	}
 }
 
+// SpaceShared converts a SpaceShared event to an AuditEventSpaceShared
+func SpaceShared(ev events.SpaceShared) AuditEventSpaceShared {
+	sse := AuditEventSpaceShared{}
+
+	sid := ev.ID.GetOpaqueId()
+	grantee := "N/A"
+	if ev.GranteeUserID != nil {
+		sse.GranteeUserID = ev.GranteeUserID.OpaqueId
+		grantee = "user:" + ev.GranteeUserID.OpaqueId
+	} else if ev.GranteeGroupID != nil {
+		sse.GranteeGroupID = ev.GranteeGroupID.OpaqueId
+		grantee = "group:" + ev.GranteeGroupID.OpaqueId
+	}
+	base := BasicAuditEvent("", "", MessageSpaceShared(ev.Executant.GetOpaqueId(), sid, grantee), ActionSpaceShared)
+	sse.AuditEventSpaces = SpacesAuditEvent(base, sid)
+
+	return sse
+}
+
+// SpaceUnshared converts a SpaceUnshared event to an AuditEventSpaceUnshared
+func SpaceUnshared(ev events.SpaceUnshared) AuditEventSpaceUnshared {
+	sue := AuditEventSpaceUnshared{}
+
+	sid := ev.ID.GetOpaqueId()
+	grantee := "N/A"
+	if ev.GranteeUserID != nil {
+		sue.GranteeUserID = ev.GranteeUserID.OpaqueId
+		grantee = "user:" + ev.GranteeUserID.OpaqueId
+	} else if ev.GranteeGroupID != nil {
+		sue.GranteeGroupID = ev.GranteeGroupID.OpaqueId
+		grantee = "group:" + ev.GranteeGroupID.OpaqueId
+	}
+	base := BasicAuditEvent("", "", MessageSpaceUnshared(ev.Executant.GetOpaqueId(), sid, grantee), ActionSpaceUnshared)
+	sue.AuditEventSpaces = SpacesAuditEvent(base, sid)
+
+	return sue
+}
+
+// SpaceUpdated converts a SpaceUpdated event to an AuditEventSpaceUpdated
+func SpaceUpdated(ev events.SpaceUpdated) AuditEventSpaceUpdated {
+	sid := ev.ID.GetOpaqueId()
+	opaqueMap := sdk.DecodeOpaqueMap(ev.Space.Opaque)
+	sue := AuditEventSpaceUpdated{
+		Name:   ev.Space.Name,
+		Opaque: opaqueMap,
+	}
+
+	base := BasicAuditEvent("", "", MessageSpaceUpdated(ev.Executant.GetOpaqueId(), sid, ev.Space.Name, ev.Space.Quota.QuotaMaxBytes, opaqueMap), ActionSpaceUpdated)
+	sue.AuditEventSpaces = SpacesAuditEvent(base, sid)
+
+	return sue
+}
+
 // UserCreated converts a UserCreated event to an AuditEventUserCreated
 func UserCreated(ev events.UserCreated) AuditEventUserCreated {
 	base := BasicAuditEvent("", "", MessageUserCreated(ev.Executant.GetOpaqueId(), ev.UserID), ActionUserCreated)
diff --git a/services/audit/pkg/types/events.go b/services/audit/pkg/types/events.go
index 02c6dfa6694..80c0663768d 100644
--- a/services/audit/pkg/types/events.go
+++ b/services/audit/pkg/types/events.go
@@ -30,6 +30,8 @@ func RegisteredEvents() []events.Unmarshaller {
 		events.SpaceDisabled{},
 		events.SpaceDeleted{},
 		events.SpaceShared{},
+		events.SpaceUnshared{},
+		events.SpaceUpdated{},
 		events.UserCreated{},
 		events.UserDeleted{},
 		events.UserFeatureChanged{},
diff --git a/services/audit/pkg/types/types.go b/services/audit/pkg/types/types.go
index c26b44a03f3..e3b96f87805 100644
--- a/services/audit/pkg/types/types.go
+++ b/services/audit/pkg/types/types.go
@@ -205,6 +205,31 @@ type AuditEventSpaceDeleted struct {
 	AuditEventSpaces
 }
 
+// AuditEventSpaceShared is the event logged when a space is shared
+type AuditEventSpaceShared struct {
+	AuditEventSpaces
+
+	GranteeUserID  string
+	GranteeGroupID string
+}
+
+// AuditEventSpaceUnshared is the event logged when a space is unshared
+type AuditEventSpaceUnshared struct {
+	AuditEventSpaces
+
+	GranteeUserID  string
+	GranteeGroupID string
+}
+
+// AuditEventSpaceUpdated is the event logged when a space is updated
+type AuditEventSpaceUpdated struct {
+	AuditEventSpaces
+
+	Name          string
+	Opaque        map[string]string
+	QuotaMaxBytes uint64
+}
+
 // AuditEventUserCreated is the event logged when a user is created
 type AuditEventUserCreated struct {
 	AuditEvent
diff --git a/services/auth-basic/pkg/config/config.go b/services/auth-basic/pkg/config/config.go
index d9d386f5ae3..a75997c99d6 100644
--- a/services/auth-basic/pkg/config/config.go
+++ b/services/auth-basic/pkg/config/config.go
@@ -69,7 +69,7 @@ type JSONProvider struct {
 
 type LDAPProvider struct {
 	URI              string          `yaml:"uri" env:"LDAP_URI;AUTH_BASIC_LDAP_URI" desc:"URI of the LDAP Server to connect to. Supported URI schemes are 'ldaps://' and 'ldap://'"`
-	CACert           string          `yaml:"ca_cert" env:"LDAP_CACERT;AUTH_BASIC_LDAP_CACERT" desc:"Path to a CA certificate file for validating the LDAP server's TLS certificate. If empty the system default CA bundle will be used."`
+	CACert           string          `yaml:"ca_cert" env:"LDAP_CACERT;AUTH_BASIC_LDAP_CACERT" desc:"Path/File name for the root CA certificate (in PEM format) used to validate TLS server certificates of the LDAP service."`
 	Insecure         bool            `yaml:"insecure" env:"LDAP_INSECURE;AUTH_BASIC_LDAP_INSECURE" desc:"Disable TLS certificate validation for the LDAP connections. Do not set this in production environments."`
 	BindDN           string          `yaml:"bind_dn" env:"LDAP_BIND_DN;AUTH_BASIC_LDAP_BIND_DN" desc:"LDAP DN to use for simple bind authentication with the target LDAP server."`
 	BindPassword     string          `yaml:"bind_password" env:"LDAP_BIND_PASSWORD;AUTH_BASIC_LDAP_BIND_PASSWORD" desc:"Password to use for authenticating the 'bind_dn'."`
diff --git a/services/graph/Makefile b/services/graph/Makefile
index d239f9c17be..070214eac39 100644
--- a/services/graph/Makefile
+++ b/services/graph/Makefile
@@ -29,6 +29,7 @@ ci-go-generate: $(MOCKERY) # CI runs ci-node-generate automatically before this
 	$(MOCKERY) --dir pkg/service/v0 --case underscore --name HTTPClient
 	$(MOCKERY) --dir pkg/service/v0 --case underscore --name Publisher
 	$(MOCKERY) --dir pkg/service/v0 --case underscore --name Permissions
+	$(MOCKERY) --dir pkg/identity --output pkg/identity/mocks --case underscore --name Backend
 	$(MOCKERY) --srcpkg github.com/go-ldap/ldap/v3 --case underscore --filename ldapclient.go --name Client
 
 
diff --git a/services/graph/pkg/config/config.go b/services/graph/pkg/config/config.go
index 4d213407f2b..012dd6163aa 100644
--- a/services/graph/pkg/config/config.go
+++ b/services/graph/pkg/config/config.go
@@ -39,7 +39,7 @@ type Spaces struct {
 
 type LDAP struct {
 	URI                string `yaml:"uri" env:"LDAP_URI;GRAPH_LDAP_URI" desc:"URI of the LDAP Server to connect to. Supported URI schemes are 'ldaps://' and 'ldap://'"`
-	CACert             string `yaml:"cacert" env:"LDAP_CACERT;GRAPH_LDAP_CACERT" desc:"The certificate to verify TLS connections."`
+	CACert             string `yaml:"cacert" env:"LDAP_CACERT;GRAPH_LDAP_CACERT" desc:"Path/File name for the root CA certificate (in PEM format) used to validate TLS server certificates of the LDAP service."`
 	Insecure           bool   `yaml:"insecure" env:"LDAP_INSECURE;GRAPH_LDAP_INSECURE" desc:"Disable TLS certificate validation for the LDAP connections. Do not set this in production environments."`
 	BindDN             string `yaml:"bind_dn" env:"LDAP_BIND_DN;GRAPH_LDAP_BIND_DN" desc:"LDAP DN to use for simple bind authentication with the target LDAP server."`
 	BindPassword       string `yaml:"bind_password" env:"LDAP_BIND_PASSWORD;GRAPH_LDAP_BIND_PASSWORD" desc:"Password to use for authenticating the 'bind_dn'."`
diff --git a/services/graph/pkg/identity/mocks/backend.go b/services/graph/pkg/identity/mocks/backend.go
new file mode 100644
index 00000000000..5c192ab9210
--- /dev/null
+++ b/services/graph/pkg/identity/mocks/backend.go
@@ -0,0 +1,273 @@
+// Code generated by mockery v2.14.1. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	libregraph "github.com/owncloud/libre-graph-api-go"
+
+	mock "github.com/stretchr/testify/mock"
+
+	url "net/url"
+)
+
+// Backend is an autogenerated mock type for the Backend type
+type Backend struct {
+	mock.Mock
+}
+
+// AddMembersToGroup provides a mock function with given fields: ctx, groupID, memberID
+func (_m *Backend) AddMembersToGroup(ctx context.Context, groupID string, memberID []string) error {
+	ret := _m.Called(ctx, groupID, memberID)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, []string) error); ok {
+		r0 = rf(ctx, groupID, memberID)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// CreateGroup provides a mock function with given fields: ctx, group
+func (_m *Backend) CreateGroup(ctx context.Context, group libregraph.Group) (*libregraph.Group, error) {
+	ret := _m.Called(ctx, group)
+
+	var r0 *libregraph.Group
+	if rf, ok := ret.Get(0).(func(context.Context, libregraph.Group) *libregraph.Group); ok {
+		r0 = rf(ctx, group)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*libregraph.Group)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, libregraph.Group) error); ok {
+		r1 = rf(ctx, group)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// CreateUser provides a mock function with given fields: ctx, user
+func (_m *Backend) CreateUser(ctx context.Context, user libregraph.User) (*libregraph.User, error) {
+	ret := _m.Called(ctx, user)
+
+	var r0 *libregraph.User
+	if rf, ok := ret.Get(0).(func(context.Context, libregraph.User) *libregraph.User); ok {
+		r0 = rf(ctx, user)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*libregraph.User)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, libregraph.User) error); ok {
+		r1 = rf(ctx, user)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// DeleteGroup provides a mock function with given fields: ctx, id
+func (_m *Backend) DeleteGroup(ctx context.Context, id string) error {
+	ret := _m.Called(ctx, id)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
+		r0 = rf(ctx, id)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// DeleteUser provides a mock function with given fields: ctx, nameOrID
+func (_m *Backend) DeleteUser(ctx context.Context, nameOrID string) error {
+	ret := _m.Called(ctx, nameOrID)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
+		r0 = rf(ctx, nameOrID)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// GetGroup provides a mock function with given fields: ctx, nameOrID, queryParam
+func (_m *Backend) GetGroup(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.Group, error) {
+	ret := _m.Called(ctx, nameOrID, queryParam)
+
+	var r0 *libregraph.Group
+	if rf, ok := ret.Get(0).(func(context.Context, string, url.Values) *libregraph.Group); ok {
+		r0 = rf(ctx, nameOrID, queryParam)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*libregraph.Group)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string, url.Values) error); ok {
+		r1 = rf(ctx, nameOrID, queryParam)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// GetGroupMembers provides a mock function with given fields: ctx, id
+func (_m *Backend) GetGroupMembers(ctx context.Context, id string) ([]*libregraph.User, error) {
+	ret := _m.Called(ctx, id)
+
+	var r0 []*libregraph.User
+	if rf, ok := ret.Get(0).(func(context.Context, string) []*libregraph.User); ok {
+		r0 = rf(ctx, id)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*libregraph.User)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
+		r1 = rf(ctx, id)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// GetGroups provides a mock function with given fields: ctx, queryParam
+func (_m *Backend) GetGroups(ctx context.Context, queryParam url.Values) ([]*libregraph.Group, error) {
+	ret := _m.Called(ctx, queryParam)
+
+	var r0 []*libregraph.Group
+	if rf, ok := ret.Get(0).(func(context.Context, url.Values) []*libregraph.Group); ok {
+		r0 = rf(ctx, queryParam)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*libregraph.Group)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, url.Values) error); ok {
+		r1 = rf(ctx, queryParam)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// GetUser provides a mock function with given fields: ctx, nameOrID, queryParam
+func (_m *Backend) GetUser(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.User, error) {
+	ret := _m.Called(ctx, nameOrID, queryParam)
+
+	var r0 *libregraph.User
+	if rf, ok := ret.Get(0).(func(context.Context, string, url.Values) *libregraph.User); ok {
+		r0 = rf(ctx, nameOrID, queryParam)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*libregraph.User)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string, url.Values) error); ok {
+		r1 = rf(ctx, nameOrID, queryParam)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// GetUsers provides a mock function with given fields: ctx, queryParam
+func (_m *Backend) GetUsers(ctx context.Context, queryParam url.Values) ([]*libregraph.User, error) {
+	ret := _m.Called(ctx, queryParam)
+
+	var r0 []*libregraph.User
+	if rf, ok := ret.Get(0).(func(context.Context, url.Values) []*libregraph.User); ok {
+		r0 = rf(ctx, queryParam)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*libregraph.User)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, url.Values) error); ok {
+		r1 = rf(ctx, queryParam)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// RemoveMemberFromGroup provides a mock function with given fields: ctx, groupID, memberID
+func (_m *Backend) RemoveMemberFromGroup(ctx context.Context, groupID string, memberID string) error {
+	ret := _m.Called(ctx, groupID, memberID)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
+		r0 = rf(ctx, groupID, memberID)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// UpdateUser provides a mock function with given fields: ctx, nameOrID, user
+func (_m *Backend) UpdateUser(ctx context.Context, nameOrID string, user libregraph.User) (*libregraph.User, error) {
+	ret := _m.Called(ctx, nameOrID, user)
+
+	var r0 *libregraph.User
+	if rf, ok := ret.Get(0).(func(context.Context, string, libregraph.User) *libregraph.User); ok {
+		r0 = rf(ctx, nameOrID, user)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*libregraph.User)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string, libregraph.User) error); ok {
+		r1 = rf(ctx, nameOrID, user)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+type mockConstructorTestingTNewBackend interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewBackend creates a new instance of Backend. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewBackend(t mockConstructorTestingTNewBackend) *Backend {
+	mock := &Backend{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/services/graph/pkg/service/v0/driveitems.go b/services/graph/pkg/service/v0/driveitems.go
index a789e1accdc..b1c5c825de4 100644
--- a/services/graph/pkg/service/v0/driveitems.go
+++ b/services/graph/pkg/service/v0/driveitems.go
@@ -74,7 +74,7 @@ func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) {
 	}
 
 	render.Status(r, http.StatusOK)
-	render.JSON(w, r, &listResponse{Value: files})
+	render.JSON(w, r, &ListResponse{Value: files})
 }
 
 func (g Graph) getDriveItem(ctx context.Context, root storageprovider.ResourceId) (*libregraph.DriveItem, error) {
diff --git a/services/graph/pkg/service/v0/drives.go b/services/graph/pkg/service/v0/drives.go
index f7974bae3c0..f1484d367ae 100644
--- a/services/graph/pkg/service/v0/drives.go
+++ b/services/graph/pkg/service/v0/drives.go
@@ -96,7 +96,7 @@ func (g Graph) getDrives(w http.ResponseWriter, r *http.Request, unrestricted bo
 		if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
 			// return an empty list
 			render.Status(r, http.StatusOK)
-			render.JSON(w, r, &listResponse{})
+			render.JSON(w, r, &ListResponse{})
 			return
 		}
 		logger.Debug().Str("message", res.GetStatus().GetMessage()).Msg("could not get drives: grpc error")
@@ -126,7 +126,7 @@ func (g Graph) getDrives(w http.ResponseWriter, r *http.Request, unrestricted bo
 	}
 
 	render.Status(r, http.StatusOK)
-	render.JSON(w, r, &listResponse{Value: spaces})
+	render.JSON(w, r, &ListResponse{Value: spaces})
 }
 
 // GetSingleDrive does a lookup of a single space by spaceId
diff --git a/services/graph/pkg/service/v0/graph.go b/services/graph/pkg/service/v0/graph.go
index af58f61d3e9..f1aa0d140c6 100644
--- a/services/graph/pkg/service/v0/graph.go
+++ b/services/graph/pkg/service/v0/graph.go
@@ -119,7 +119,7 @@ func (g Graph) getWebDavBaseURL() (*url.URL, error) {
 	return webDavBaseURL, nil
 }
 
-type listResponse struct {
+type ListResponse struct {
 	Value interface{} `json:"value,omitempty"`
 }
 
diff --git a/services/graph/pkg/service/v0/groups.go b/services/graph/pkg/service/v0/groups.go
index 48c657b73ed..3ca8c8313c1 100644
--- a/services/graph/pkg/service/v0/groups.go
+++ b/services/graph/pkg/service/v0/groups.go
@@ -42,6 +42,7 @@ func (g Graph) GetGroups(w http.ResponseWriter, r *http.Request) {
 		} else {
 			errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
 		}
+		return
 	}
 
 	groups, err = sortGroups(odataReq, groups)
@@ -51,7 +52,7 @@ func (g Graph) GetGroups(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	render.Status(r, http.StatusOK)
-	render.JSON(w, r, &listResponse{Value: groups})
+	render.JSON(w, r, &ListResponse{Value: groups})
 }
 
 // PostGroup implements the Service interface.
@@ -312,20 +313,20 @@ func (g Graph) PostGroupMember(w http.ResponseWriter, r *http.Request) {
 	memberRefURL, ok := memberRef.GetOdataIdOk()
 	if !ok {
 		logger.Debug().Msg("could not add group member: @odata.id reference is missing")
-		errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "@odata.id reference is missing")
+		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "@odata.id reference is missing")
 		return
 	}
 	memberType, id, err := g.parseMemberRef(*memberRefURL)
 	if err != nil {
 		logger.Debug().Err(err).Msg("could not add group member: error parsing @odata.id url")
-		errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "Error parsing @odata.id url")
+		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "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" {
 		logger.Debug().Str("type", memberType).Msg("could not add group member: Only users are allowed as group members")
-		errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "Only users are allowed as group members")
+		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "Only users are allowed as group members")
 		return
 	}
 
@@ -371,14 +372,14 @@ func (g Graph) DeleteGroupMember(w http.ResponseWriter, r *http.Request) {
 	memberID := chi.URLParam(r, "memberID")
 	memberID, err = url.PathUnescape(memberID)
 	if err != nil {
-		logger.Debug().Err(err).Str("id", memberID).Msg("could not delete group member: unescaping group id failed")
-		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping group id failed")
+		logger.Debug().Err(err).Str("id", memberID).Msg("could not delete group member: unescaping member id failed")
+		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping member id failed")
 		return
 	}
 
 	if memberID == "" {
-		logger.Debug().Msg("could not delete group member: missing group id")
-		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing group id")
+		logger.Debug().Msg("could not delete group member: missing member id")
+		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing member id")
 		return
 	}
 	logger.Debug().Str("groupID", groupID).Str("memberID", memberID).Msg("calling delete member on backend")
diff --git a/services/graph/pkg/service/v0/users.go b/services/graph/pkg/service/v0/users.go
index 82521085aa7..cb0e05f26bf 100644
--- a/services/graph/pkg/service/v0/users.go
+++ b/services/graph/pkg/service/v0/users.go
@@ -95,7 +95,7 @@ func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	render.Status(r, http.StatusOK)
-	render.JSON(w, r, &listResponse{Value: users})
+	render.JSON(w, r, &ListResponse{Value: users})
 }
 
 func (g Graph) PostUser(w http.ResponseWriter, r *http.Request) {
diff --git a/services/groups/pkg/config/config.go b/services/groups/pkg/config/config.go
index 7b4417e091e..b0f41c3930d 100644
--- a/services/groups/pkg/config/config.go
+++ b/services/groups/pkg/config/config.go
@@ -68,7 +68,7 @@ type Drivers struct {
 
 type LDAPDriver struct {
 	URI                      string          `yaml:"uri" env:"LDAP_URI;GROUPS_LDAP_URI" desc:"URI of the LDAP Server to connect to. Supported URI schemes are 'ldaps://' and 'ldap://'"`
-	CACert                   string          `yaml:"ca_cert" env:"LDAP_CACERT;GROUPS_LDAP_CACERT" desc:"Path to a CA certificate file for validating the LDAP server's TLS certificate. If empty, the system default CA bundle will be used."`
+	CACert                   string          `yaml:"ca_cert" env:"LDAP_CACERT;GROUPS_LDAP_CACERT" desc:"Path/File name for the root CA certificate (in PEM format) used to validate TLS server certificates of the LDAP service."`
 	Insecure                 bool            `yaml:"insecure" env:"LDAP_INSECURE;GROUPS_LDAP_INSECURE" desc:"Disable TLS certificate validation for the LDAP connections. Do not set this in production environments."`
 	BindDN                   string          `yaml:"bind_dn" env:"LDAP_BIND_DN;GROUPS_LDAP_BIND_DN" desc:"LDAP DN to use for simple bind authentication with the target LDAP server."`
 	BindPassword             string          `yaml:"bind_password" env:"LDAP_BIND_PASSWORD;GROUPS_LDAP_BIND_PASSWORD" desc:"Password to use for authenticating the 'bind_dn'."`
diff --git a/services/idp/pkg/config/config.go b/services/idp/pkg/config/config.go
index 67ea0c296ce..8bbfddb58cf 100644
--- a/services/idp/pkg/config/config.go
+++ b/services/idp/pkg/config/config.go
@@ -33,7 +33,7 @@ type Config struct {
 // Ldap defines the available LDAP configuration.
 type Ldap struct {
 	URI       string `yaml:"uri" env:"LDAP_URI;IDP_LDAP_URI" desc:"Url of the LDAP service to use as IDP."`
-	TLSCACert string `yaml:"cacert" env:"LDAP_CACERT;IDP_LDAP_TLS_CACERT" desc:"Path to the TLS cert for the LDAP service."`
+	TLSCACert string `yaml:"cacert" env:"LDAP_CACERT;IDP_LDAP_TLS_CACERT" desc:"Path/File name for the root CA certificate (in PEM format) used to validate TLS server certificates of the LDAP service."`
 
 	BindDN       string `yaml:"bind_dn" env:"LDAP_BIND_DN;IDP_LDAP_BIND_DN" desc:"LDAP DN to use for simple bind authentication with the target LDAP server."`
 	BindPassword string `yaml:"bind_password" env:"LDAP_BIND_PASSWORD;IDP_LDAP_BIND_PASSWORD" desc:"Password to use for authenticating the 'bind_dn'."`
@@ -82,7 +82,7 @@ type Settings struct {
 	AuthorizationEndpointURI string `yaml:"authorization_endpoint_uri" env:"IDP_ENDPOINT_URI" desc:"URL of the IDP endpoint."`
 	EndsessionEndpointURI    string `yaml:"-"` // unused, not supported by lico-idp
 
-	Insecure bool `yaml:"insecure" env:"LDAP_INSECURE;IDP_INSECURE" desc:"Allow insecure connections to the user backend like LDAP, CS3 api, ..."`
+	Insecure bool `yaml:"insecure" env:"LDAP_INSECURE;IDP_INSECURE" desc:"Disable TLS certificate validation for the LDAP connections. Do not set this in production environments."`
 
 	TrustedProxy []string `yaml:"trusted_proxy"` //TODO: how to configure this via env?
 
diff --git a/services/idp/pkg/config/http.go b/services/idp/pkg/config/http.go
index a178eda7f58..a1c093a0298 100644
--- a/services/idp/pkg/config/http.go
+++ b/services/idp/pkg/config/http.go
@@ -5,7 +5,7 @@ type HTTP struct {
 	Addr      string `yaml:"addr" env:"IDP_HTTP_ADDR" desc:"The bind address of the HTTP service."`
 	Root      string `yaml:"root" env:"IDP_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service."`
 	Namespace string `yaml:"-"`
-	TLSCert   string `yaml:"tls_cert" env:"IDP_TRANSPORT_TLS_CERT" desc:"File name of the TLS server certificate for the HTTPS server."`
-	TLSKey    string `yaml:"tls_key" env:"IDP_TRANSPORT_TLS_KEY" desc:"File name of the TLS server certificate key for the HTTPS server."`
-	TLS       bool   `yaml:"tls" env:"IDP_TLS" desc:"Use the HTTPS server instead of the HTTP server."`
+	TLSCert   string `yaml:"tls_cert" env:"IDP_TRANSPORT_TLS_CERT" desc:"Path/File name of the TLS server certificate (in PEM format) for the IDP service."`
+	TLSKey    string `yaml:"tls_key" env:"IDP_TRANSPORT_TLS_KEY" desc:"Path/File name for the TLS certificate key (in PEM format) for the server certificate to use for the IDP service."`
+	TLS       bool   `yaml:"tls" env:"IDP_TLS" desc:"Enable/Disable HTTPS for the IDP service."`
 }
diff --git a/services/nats/pkg/config/config.go b/services/nats/pkg/config/config.go
index 4c24e876f34..f84dc0c4af8 100644
--- a/services/nats/pkg/config/config.go
+++ b/services/nats/pkg/config/config.go
@@ -26,8 +26,8 @@ type Nats struct {
 	Port                    int    `yaml:"port" env:"NATS_NATS_PORT" desc:"Bind port."`
 	ClusterID               string `yaml:"clusterid" env:"NATS_NATS_CLUSTER_ID" desc:"ID of the NATS cluster."`
 	StoreDir                string `yaml:"store_dir" env:"NATS_NATS_STORE_DIR" desc:"The directory where the filesystem storage will store NATS JetStream data. If not definied, the root directory derives from $OCIS_BASE_DATA_PATH:/nats."`
-	TLSCert                 string `yaml:"tls_cert" env:"NATS_TLS_CERT" desc:"File name of the TLS server certificate for the nats listener."`
-	TLSKey                  string `yaml:"tls_key" env:"NATS_TLS_KEY" desc:"File name for the TLS certificate key for the server certificate."`
+	TLSCert                 string `yaml:"tls_cert" env:"NATS_TLS_CERT" desc:"Path/File name of the TLS server certificate (in PEM format) for the NATS listener."`
+	TLSKey                  string `yaml:"tls_key" env:"NATS_TLS_KEY" desc:"Path/File name for the TLS certificate key (in PEM format) for the NATS listener."`
 	TLSSkipVerifyClientCert bool   `yaml:"tls_skip_verify_client_cert" env:"OCIS_INSECURE;NATS_TLS_SKIP_VERIFY_CLIENT_CERT" desc:"Whether the NATS server should skip the client certificate verification during the TLS handshake."`
 	EnableTLS               bool   `yaml:"enable_tls" env:"OCIS_EVENTS_ENABLE_TLS;NATS_EVENTS_ENABLE_TLS" desc:"Enable TLS for the connection to the events broker. The events broker is the ocis service which receives and delivers events between the services.."`
 }
diff --git a/services/notifications/pkg/service/service.go b/services/notifications/pkg/service/service.go
index a419ef14520..b6deec8264d 100644
--- a/services/notifications/pkg/service/service.go
+++ b/services/notifications/pkg/service/service.go
@@ -153,7 +153,7 @@ func (s eventsNotifier) handleSpaceShared(e events.SpaceShared) {
 		return
 	}
 
-	shareLink, err := urlJoinPath(s.ocisURL, "f", storagespace.FormatResourceID(*e.ID))
+	shareLink, err := urlJoinPath(s.ocisURL, "f", e.ID.OpaqueId)
 
 	if err != nil {
 		s.logger.Error().
diff --git a/services/proxy/pkg/config/config.go b/services/proxy/pkg/config/config.go
index 26d3618e2d9..36c96c200db 100644
--- a/services/proxy/pkg/config/config.go
+++ b/services/proxy/pkg/config/config.go
@@ -33,7 +33,7 @@ type Config struct {
 	AutoprovisionAccounts bool            `yaml:"auto_provision_accounts" env:"PROXY_AUTOPROVISION_ACCOUNTS" desc:"Set this to 'true' to automatically provision users that do not yet exist in the users service on-demand upon first sign-in. To use this a write-enabled libregraph user backend needs to be setup an running."`
 	EnableBasicAuth       bool            `yaml:"enable_basic_auth" env:"PROXY_ENABLE_BASIC_AUTH" desc:"Set this to true to enable 'basic authentication' (username/password)."`
 	InsecureBackends      bool            `yaml:"insecure_backends" env:"PROXY_INSECURE_BACKENDS" desc:"Disable TLS certificate validation for all HTTP backend connections."`
-	BackendHTTPSCACert    string          `yaml:"backend_https_cacert" env:"PROXY_HTTPS_CACERT" desc:"The root CA certificate used to validate TLS server certificates of https enabled backend services."`
+	BackendHTTPSCACert    string          `yaml:"backend_https_cacert" env:"PROXY_HTTPS_CACERT" desc:"Path/File for the root CA certificate used to validate the server’s TLS certificate for https enabled backend services."`
 	AuthMiddleware        AuthMiddleware  `yaml:"auth_middleware"`
 
 	Context context.Context `yaml:"-" json:"-"`
diff --git a/services/proxy/pkg/config/http.go b/services/proxy/pkg/config/http.go
index 2ed22d9f944..793d67b4c56 100644
--- a/services/proxy/pkg/config/http.go
+++ b/services/proxy/pkg/config/http.go
@@ -5,7 +5,7 @@ type HTTP struct {
 	Addr      string `yaml:"addr" env:"PROXY_HTTP_ADDR" desc:"The bind address of the HTTP service."`
 	Root      string `yaml:"root" env:"PROXY_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service."`
 	Namespace string `yaml:"-"`
-	TLSCert   string `yaml:"tls_cert" env:"PROXY_TRANSPORT_TLS_CERT" desc:"File name of the TLS server certificate for the HTTPS server."`
-	TLSKey    string `yaml:"tls_key" env:"PROXY_TRANSPORT_TLS_KEY" desc:"File name of the TLS server certificate key for the HTTPS server."`
-	TLS       bool   `yaml:"tls" env:"PROXY_TLS" desc:"Use the HTTPS server instead of the HTTP server."`
+	TLSCert   string `yaml:"tls_cert" env:"PROXY_TRANSPORT_TLS_CERT" desc:"Path/File name of the TLS server certificate (in PEM format) for the external http services."`
+	TLSKey    string `yaml:"tls_key" env:"PROXY_TRANSPORT_TLS_KEY" desc:"Path/File name for the TLS certificate key (in PEM format) for the server certificate to use for the external http services."`
+	TLS       bool   `yaml:"tls" env:"PROXY_TLS" desc:"Enable/Disable HTTPS for the external HTTP services."`
 }
diff --git a/services/users/pkg/config/config.go b/services/users/pkg/config/config.go
index 8591124379a..5a77b303e83 100644
--- a/services/users/pkg/config/config.go
+++ b/services/users/pkg/config/config.go
@@ -71,7 +71,7 @@ type JSONDriver struct {
 }
 type LDAPDriver struct {
 	URI                     string          `yaml:"uri" env:"LDAP_URI;USERS_LDAP_URI" desc:"URI of the LDAP Server to connect to. Supported URI schemes are 'ldaps://' and 'ldap://'"`
-	CACert                  string          `yaml:"ca_cert" env:"LDAP_CACERT;USERS_LDAP_CACERT" desc:"Path to a CA certificate file for validating the LDAP server's TLS certificate. If empty, the system default CA bundle will be used."`
+	CACert                  string          `yaml:"ca_cert" env:"LDAP_CACERT;USERS_LDAP_CACERT" desc:"Path/File name for the root CA certificate (in PEM format) used to validate TLS server certificates of the LDAP service."`
 	Insecure                bool            `yaml:"insecure" env:"LDAP_INSECURE;USERS_LDAP_INSECURE" desc:"Disable TLS certificate validation for the LDAP connections. Do not set this in production environments."`
 	BindDN                  string          `yaml:"bind_dn" env:"LDAP_BIND_DN;USERS_LDAP_BIND_DN" desc:"LDAP DN to use for simple bind authentication with the target LDAP server."`
 	BindPassword            string          `yaml:"bind_password" env:"LDAP_BIND_PASSWORD;USERS_LDAP_BIND_PASSWORD" desc:"Password to use for authenticating the 'bind_dn'."`


Test patch:
diff --git a/services/graph/pkg/service/v0/groups_test.go b/services/graph/pkg/service/v0/groups_test.go
new file mode 100644
index 00000000000..84d5b5a8338
--- /dev/null
+++ b/services/graph/pkg/service/v0/groups_test.go
@@ -0,0 +1,486 @@
+package svc_test
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"errors"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+
+	userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
+	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
+	"github.com/go-chi/chi/v5"
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"github.com/test-go/testify/mock"
+
+	libregraph "github.com/owncloud/libre-graph-api-go"
+	ogrpc "github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
+	"github.com/owncloud/ocis/v2/ocis-pkg/shared"
+	"github.com/owncloud/ocis/v2/services/graph/mocks"
+	"github.com/owncloud/ocis/v2/services/graph/pkg/config"
+	"github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults"
+	identitymocks "github.com/owncloud/ocis/v2/services/graph/pkg/identity/mocks"
+	service "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0"
+	"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
+)
+
+type groupList struct {
+	Value []*libregraph.Group
+}
+
+var _ = Describe("Groups", func() {
+	var (
+		svc             service.Service
+		ctx             context.Context
+		cfg             *config.Config
+		gatewayClient   *mocks.GatewayClient
+		eventsPublisher mocks.Publisher
+		identityBackend *identitymocks.Backend
+
+		rr *httptest.ResponseRecorder
+
+		newGroup    *libregraph.Group
+		currentUser = &userv1beta1.User{
+			Id: &userv1beta1.UserId{
+				OpaqueId: "user",
+			},
+		}
+	)
+
+	BeforeEach(func() {
+		eventsPublisher.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+		identityBackend = &identitymocks.Backend{}
+		newGroup = libregraph.NewGroup()
+		newGroup.SetMembersodataBind([]string{"/users/user1"})
+		newGroup.SetId("group1")
+
+		rr = httptest.NewRecorder()
+		ctx = context.Background()
+
+		cfg = defaults.FullDefaultConfig()
+		cfg.Identity.LDAP.CACert = "" // skip the startup checks, we don't use LDAP at all in this tests
+		cfg.TokenManager.JWTSecret = "loremipsum"
+		cfg.Commons = &shared.Commons{}
+		cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
+
+		_ = ogrpc.Configure(ogrpc.GetClientOptions(cfg.GRPCClientTLS)...)
+		svc = service.NewService(
+			service.Config(cfg),
+			service.WithGatewayClient(gatewayClient),
+			service.EventsPublisher(&eventsPublisher),
+			service.WithIdentityBackend(identityBackend),
+		)
+	})
+
+	Describe("GetGroups", func() {
+		It("handles invalid ODATA parameters", func() {
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/groups?§foo=bar", nil)
+			svc.GetGroups(rr, r)
+
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+		})
+
+		It("handles invalid sorting queries", func() {
+			identityBackend.On("GetGroups", ctx, mock.Anything).Return([]*libregraph.Group{newGroup}, nil)
+
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/groups?$orderby=invalid", nil)
+			svc.GetGroups(rr, r)
+
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+			data, err := ioutil.ReadAll(rr.Body)
+			Expect(err).ToNot(HaveOccurred())
+
+			odataerr := libregraph.OdataError{}
+			err = json.Unmarshal(data, &odataerr)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(odataerr.Error.Code).To(Equal("invalidRequest"))
+		})
+
+		It("handles unknown backend errors", func() {
+			identityBackend.On("GetGroups", ctx, mock.Anything).Return(nil, errors.New("failed"))
+
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/groups", nil)
+			svc.GetGroups(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusInternalServerError))
+			data, err := ioutil.ReadAll(rr.Body)
+			Expect(err).ToNot(HaveOccurred())
+
+			odataerr := libregraph.OdataError{}
+			err = json.Unmarshal(data, &odataerr)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(odataerr.Error.Code).To(Equal("generalException"))
+		})
+
+		It("handles backend errors", func() {
+			identityBackend.On("GetGroups", ctx, mock.Anything).Return(nil, errorcode.New(errorcode.AccessDenied, "access denied"))
+
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/groups", nil)
+			svc.GetGroups(rr, r)
+
+			Expect(rr.Code).To(Equal(http.StatusInternalServerError))
+			data, err := ioutil.ReadAll(rr.Body)
+			Expect(err).ToNot(HaveOccurred())
+
+			odataerr := libregraph.OdataError{}
+			err = json.Unmarshal(data, &odataerr)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(odataerr.Error.Code).To(Equal("accessDenied"))
+		})
+
+		It("renders an empty list of groups", func() {
+			identityBackend.On("GetGroups", ctx, mock.Anything).Return([]*libregraph.Group{}, nil)
+
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/groups", nil)
+			svc.GetGroups(rr, r)
+
+			Expect(rr.Code).To(Equal(http.StatusOK))
+			data, err := ioutil.ReadAll(rr.Body)
+			Expect(err).ToNot(HaveOccurred())
+
+			res := service.ListResponse{}
+			err = json.Unmarshal(data, &res)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(res.Value).To(Equal([]interface{}{}))
+		})
+
+		It("renders a list of groups", func() {
+			identityBackend.On("GetGroups", ctx, mock.Anything).Return([]*libregraph.Group{newGroup}, nil)
+
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/groups", nil)
+			svc.GetGroups(rr, r)
+
+			Expect(rr.Code).To(Equal(http.StatusOK))
+			data, err := ioutil.ReadAll(rr.Body)
+			Expect(err).ToNot(HaveOccurred())
+
+			res := groupList{}
+			err = json.Unmarshal(data, &res)
+			Expect(err).ToNot(HaveOccurred())
+
+			Expect(len(res.Value)).To(Equal(1))
+			Expect(res.Value[0].GetId()).To(Equal("group1"))
+		})
+	})
+
+	Describe("GetGroup", func() {
+		It("handles missing or empty group id", func() {
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/groups", nil)
+			svc.GetGroup(rr, r)
+
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+
+			r = httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/groups", nil)
+			rctx := chi.NewRouteContext()
+			rctx.URLParams.Add("groupID", "")
+			r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, nil), chi.RouteCtxKey, rctx))
+			svc.GetGroup(rr, r)
+
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+		})
+
+		Context("with an existing group", func() {
+			BeforeEach(func() {
+				identityBackend.On("GetGroup", mock.Anything, mock.Anything, mock.Anything).Return(newGroup, nil)
+			})
+
+			It("gets the group", func() {
+				r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/groups/"+*newGroup.Id, nil)
+				rctx := chi.NewRouteContext()
+				rctx.URLParams.Add("groupID", *newGroup.Id)
+				r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, nil), chi.RouteCtxKey, rctx))
+
+				svc.GetGroup(rr, r)
+
+				Expect(rr.Code).To(Equal(http.StatusOK))
+			})
+		})
+	})
+
+	Describe("PostGroup", func() {
+		It("handles invalid body", func() {
+			r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/me/groups/", bytes.NewBufferString("{invalid"))
+
+			svc.PostGroup(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+		})
+
+		It("handles missing display name", func() {
+			newGroup = libregraph.NewGroup()
+			newGroup.SetId("disallowed")
+			newGroup.SetMembersodataBind([]string{"/non-users/user"})
+			newGroupJson, err := json.Marshal(newGroup)
+			Expect(err).ToNot(HaveOccurred())
+
+			r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/me/groups/", bytes.NewBuffer(newGroupJson))
+
+			svc.PostGroup(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+		})
+
+		It("disallows user create ids", func() {
+			newGroup = libregraph.NewGroup()
+			newGroup.SetId("disallowed")
+			newGroup.SetDisplayName("New Group")
+			newGroup.SetMembersodataBind([]string{"/non-users/user"})
+			newGroupJson, err := json.Marshal(newGroup)
+			Expect(err).ToNot(HaveOccurred())
+
+			r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/me/groups/", bytes.NewBuffer(newGroupJson))
+
+			svc.PostGroup(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+		})
+
+		It("creates the group", func() {
+			newGroup = libregraph.NewGroup()
+			newGroup.SetDisplayName("New Group")
+			newGroupJson, err := json.Marshal(newGroup)
+			Expect(err).ToNot(HaveOccurred())
+
+			identityBackend.On("CreateGroup", mock.Anything, mock.Anything).Return(newGroup, nil)
+
+			r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/me/groups/", bytes.NewBuffer(newGroupJson))
+
+			svc.PostGroup(rr, r)
+
+			Expect(rr.Code).To(Equal(http.StatusOK))
+		})
+	})
+	Describe("PatchGroup", func() {
+		It("handles invalid body", func() {
+			r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/me/groups/", bytes.NewBufferString("{invalid"))
+			rctx := chi.NewRouteContext()
+			rctx.URLParams.Add("groupID", *newGroup.Id)
+			r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
+			svc.PatchGroup(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+		})
+
+		It("handles missing or empty group id", func() {
+			r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/me/groups", nil)
+			svc.PatchGroup(rr, r)
+
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+
+			r = httptest.NewRequest(http.MethodPatch, "/graph/v1.0/me/groups", nil)
+			rctx := chi.NewRouteContext()
+			rctx.URLParams.Add("groupID", "")
+			r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
+			svc.PatchGroup(rr, r)
+
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+		})
+
+		Context("with an existing group", func() {
+			BeforeEach(func() {
+				identityBackend.On("GetGroup", mock.Anything, mock.Anything, mock.Anything).Return(newGroup, nil)
+			})
+
+			It("fails when the number of users is exceeded - spec says 20 max", func() {
+				updatedGroup := libregraph.NewGroup()
+				updatedGroup.SetDisplayName("group1 updated")
+				updatedGroup.SetMembersodataBind([]string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18",
+					"19", "20", "21"})
+				updatedGroupJson, err := json.Marshal(updatedGroup)
+				Expect(err).ToNot(HaveOccurred())
+
+				r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/me/groups", bytes.NewBuffer(updatedGroupJson))
+				rctx := chi.NewRouteContext()
+				rctx.URLParams.Add("groupID", *newGroup.Id)
+				r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
+				svc.PatchGroup(rr, r)
+
+				Expect(rr.Code).To(Equal(http.StatusBadRequest))
+			})
+
+			It("fails on invalid user refs", func() {
+				updatedGroup := libregraph.NewGroup()
+				updatedGroup.SetDisplayName("group1 updated")
+				updatedGroup.SetMembersodataBind([]string{"invalid"})
+				updatedGroupJson, err := json.Marshal(updatedGroup)
+				Expect(err).ToNot(HaveOccurred())
+
+				r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/me/groups", bytes.NewBuffer(updatedGroupJson))
+				rctx := chi.NewRouteContext()
+				rctx.URLParams.Add("groupID", *newGroup.Id)
+				r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
+				svc.PatchGroup(rr, r)
+
+				Expect(rr.Code).To(Equal(http.StatusBadRequest))
+			})
+
+			It("fails when the adding non-users users", func() {
+				updatedGroup := libregraph.NewGroup()
+				updatedGroup.SetDisplayName("group1 updated")
+				updatedGroup.SetMembersodataBind([]string{"/non-users/user1"})
+				updatedGroupJson, err := json.Marshal(updatedGroup)
+				Expect(err).ToNot(HaveOccurred())
+
+				r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/me/groups", bytes.NewBuffer(updatedGroupJson))
+				rctx := chi.NewRouteContext()
+				rctx.URLParams.Add("groupID", *newGroup.Id)
+				r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
+				svc.PatchGroup(rr, r)
+
+				Expect(rr.Code).To(Equal(http.StatusBadRequest))
+			})
+
+			It("adds members to the group", func() {
+				identityBackend.On("AddMembersToGroup", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+				updatedGroup := libregraph.NewGroup()
+				updatedGroup.SetDisplayName("group1 updated")
+				updatedGroup.SetMembersodataBind([]string{"/users/user1"})
+				updatedGroupJson, err := json.Marshal(updatedGroup)
+				Expect(err).ToNot(HaveOccurred())
+
+				r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/me/groups", bytes.NewBuffer(updatedGroupJson))
+				rctx := chi.NewRouteContext()
+				rctx.URLParams.Add("groupID", *newGroup.Id)
+				r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
+				svc.PatchGroup(rr, r)
+
+				Expect(rr.Code).To(Equal(http.StatusNoContent))
+				identityBackend.AssertNumberOfCalls(GinkgoT(), "AddMembersToGroup", 1)
+			})
+		})
+	})
+
+	Describe("DeleteGroup", func() {
+		Context("with an existing group", func() {
+			BeforeEach(func() {
+				identityBackend.On("GetGroup", mock.Anything, mock.Anything, mock.Anything).Return(newGroup, nil)
+			})
+		})
+
+		It("deletes the group", func() {
+			identityBackend.On("DeleteGroup", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+			r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/me/groups", nil)
+			rctx := chi.NewRouteContext()
+			rctx.URLParams.Add("groupID", *newGroup.Id)
+			r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
+			svc.DeleteGroup(rr, r)
+
+			Expect(rr.Code).To(Equal(http.StatusNoContent))
+			identityBackend.AssertNumberOfCalls(GinkgoT(), "DeleteGroup", 1)
+			eventsPublisher.AssertNumberOfCalls(GinkgoT(), "Publish", 1)
+		})
+	})
+
+	Describe("GetGroupMembers", func() {
+		It("gets the list of members", func() {
+			user := libregraph.NewUser()
+			user.SetId("user")
+			identityBackend.On("GetGroupMembers", mock.Anything, mock.Anything, mock.Anything).Return([]*libregraph.User{user}, nil)
+
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/groups/{groupID}/members", nil)
+			rctx := chi.NewRouteContext()
+			rctx.URLParams.Add("groupID", *newGroup.Id)
+			r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
+			svc.GetGroupMembers(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusOK))
+
+			data, err := ioutil.ReadAll(rr.Body)
+			Expect(err).ToNot(HaveOccurred())
+
+			var members []*libregraph.User
+			err = json.Unmarshal(data, &members)
+			Expect(err).ToNot(HaveOccurred())
+
+			Expect(len(members)).To(Equal(1))
+			Expect(members[0].GetId()).To(Equal("user"))
+		})
+	})
+
+	Describe("PostGroupMembers", func() {
+		It("fails on invalid body", func() {
+			r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/me/groups/{groupID}/members", bytes.NewBufferString("{invalid"))
+			rctx := chi.NewRouteContext()
+			rctx.URLParams.Add("groupID", *newGroup.Id)
+			r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
+			svc.PostGroupMember(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+		})
+
+		It("fails on missing member refs", func() {
+			member := libregraph.NewMemberReference()
+			data, err := json.Marshal(member)
+			Expect(err).ToNot(HaveOccurred())
+
+			r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/me/groups/{groupID}/members", bytes.NewBuffer(data))
+			rctx := chi.NewRouteContext()
+			rctx.URLParams.Add("groupID", *newGroup.Id)
+			r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
+			svc.PostGroupMember(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+		})
+
+		It("fails on invalid member refs", func() {
+			member := libregraph.NewMemberReference()
+			member.SetOdataId("/invalidtype/user")
+			data, err := json.Marshal(member)
+			Expect(err).ToNot(HaveOccurred())
+
+			r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/me/groups/{groupID}/members", bytes.NewBuffer(data))
+			rctx := chi.NewRouteContext()
+			rctx.URLParams.Add("groupID", *newGroup.Id)
+			r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
+			svc.PostGroupMember(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+		})
+
+		It("adds a new member", func() {
+			member := libregraph.NewMemberReference()
+			member.SetOdataId("/users/user")
+			data, err := json.Marshal(member)
+			Expect(err).ToNot(HaveOccurred())
+			identityBackend.On("AddMembersToGroup", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+			r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/me/groups/{groupID}/members", bytes.NewBuffer(data))
+			rctx := chi.NewRouteContext()
+			rctx.URLParams.Add("groupID", *newGroup.Id)
+			r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
+			svc.PostGroupMember(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusNoContent))
+
+			identityBackend.AssertNumberOfCalls(GinkgoT(), "AddMembersToGroup", 1)
+		})
+	})
+
+	Describe("DeleteGroupMembers", func() {
+		It("handles missing or empty member id", func() {
+			r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/me/groups/{groupID}/members/{memberID}/$ref", nil)
+			rctx := chi.NewRouteContext()
+			rctx.URLParams.Add("groupID", *newGroup.Id)
+			r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
+			svc.DeleteGroupMember(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+		})
+		It("handles missing or empty member id", func() {
+			r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/me/groups/{groupID}/members/{memberID}/$ref", nil)
+			rctx := chi.NewRouteContext()
+			rctx.URLParams.Add("memberID", "/users/user")
+			r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
+			svc.DeleteGroupMember(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+		})
+
+		It("deletes members", func() {
+			identityBackend.On("RemoveMemberFromGroup", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+			r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/me/groups/{groupID}/members/{memberID}/$ref", nil)
+			rctx := chi.NewRouteContext()
+			rctx.URLParams.Add("groupID", *newGroup.Id)
+			rctx.URLParams.Add("memberID", "/users/user1")
+			r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
+			svc.DeleteGroupMember(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusNoContent))
+
+			identityBackend.AssertNumberOfCalls(GinkgoT(), "RemoveMemberFromGroup", 1)
+		})
+	})
+})
diff --git a/tests/acceptance/expected-failures-API-on-OCIS-storage.md b/tests/acceptance/expected-failures-API-on-OCIS-storage.md
index c1f19b9a964..be0a3acaae9 100644
--- a/tests/acceptance/expected-failures-API-on-OCIS-storage.md
+++ b/tests/acceptance/expected-failures-API-on-OCIS-storage.md
@@ -33,10 +33,6 @@ _ocdav: double-check the webdav property parsing when custom namespaces are used
 - [apiWebdavProperties2/getFileProperties.feature:405](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties2/getFileProperties.feature#L405)
 - [apiWebdavProperties2/getFileProperties.feature:410](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties2/getFileProperties.feature#L410)
 
-#### [Downloading the older version of shared file gives 404](https://github.com/owncloud/ocis/issues/3868)
-
-- [apiVersions/fileVersionsSharingToShares.feature:306](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiVersions/fileVersionsSharingToShares.feature#L306)
-
 #### [file versions do not report the version author](https://github.com/owncloud/ocis/issues/2914)
 
 - [apiVersions/fileVersionAuthor.feature:14](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiVersions/fileVersionAuthor.feature#L14)
@@ -320,10 +316,6 @@ cannot share a folder with create permission
 - [apiWebdavMove2/moveShareOnOcis.feature:169](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavMove2/moveShareOnOcis.feature#L169)
 - [apiWebdavMove2/moveShareOnOcis.feature:170](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavMove2/moveShareOnOcis.feature#L170)
 
-#### [restoring an older version of a shared file deletes the share](https://github.com/owncloud/ocis/issues/765)
-
-- [apiShareManagementToShares/acceptShares.feature:579](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareManagementToShares/acceptShares.feature#L579)
-
 #### [Expiration date for shares is not implemented](https://github.com/owncloud/ocis/issues/1250)
 
 #### Expiration date of user shares
diff --git a/tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md b/tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md
index 79d3d1ec943..e27ee42f4e7 100644
--- a/tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md
+++ b/tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md
@@ -60,3 +60,11 @@ 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:62](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiGraph/deleteGroup.feature#L62)
+
+#### [Updating group displayName request seems OK but group is not being renamed](https://github.com/owncloud/ocis/issues/5099)
+- [apiGraph/editGroup.feature:20](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiGraph/editGroup.feature#L20)
+- [apiGraph/editGroup.feature:21](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiGraph/editGroup.feature#L21)
+- [apiGraph/editGroup.feature:22](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiGraph/editGroup.feature#L22)
+- [apiGraph/editGroup.feature:23](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiGraph/editGroup.feature#L23)
+- [apiGraph/editGroup.feature:24](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiGraph/editGroup.feature#L24)
+- [apiGraph/editGroup.feature:25](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiGraph/editGroup.feature#L25)
diff --git a/tests/acceptance/features/apiGraph/editGroup.feature b/tests/acceptance/features/apiGraph/editGroup.feature
new file mode 100644
index 00000000000..e188c04be21
--- /dev/null
+++ b/tests/acceptance/features/apiGraph/editGroup.feature
@@ -0,0 +1,25 @@
+@api @skipOnOcV10
+Feature: edit group name
+  As an admin
+  I want to be able to edit group name
+  So that I can manage group name
+
+  Background:
+    Given user "Alice" has been created with default attributes and without skeleton files
+    And the administrator has given "Alice" the role "Admin" using the settings api
+
+  @issue-5099
+  Scenario Outline: admin user renames a group
+    Given group "<old_group>" has been created
+    When user "Alice" renames group "<old_group>" to "<new_group>" using the Graph API
+    Then the HTTP status code should be "204"
+    And group "<old_group>" should not exist
+    And group "<new_group>" should exist
+    Examples:
+      | old_group | new_group     |
+      | grp1      | grp101        |
+      | grp1      | España§àôœ€   |
+      | grp1      | नेपाली          |
+      | grp1      | $x<=>[y*z^2]! |
+      | grp1      | staff?group   |
+      | grp1      | 50%pass       |
\ No newline at end of file
diff --git a/tests/acceptance/features/bootstrap/GraphContext.php b/tests/acceptance/features/bootstrap/GraphContext.php
index afb3aaa2ef1..32064126627 100644
--- a/tests/acceptance/features/bootstrap/GraphContext.php
+++ b/tests/acceptance/features/bootstrap/GraphContext.php
@@ -889,4 +889,41 @@ class GraphContext implements Context {
 			);
 		}
 	}
+
+	/**
+	 * rename group name
+	 *
+	 * @param string $oldGroup
+	 * @param string $newGroup
+	 * @param string $user
+	 *
+	 * @return ResponseInterface
+	 * @throws GuzzleException
+	 */
+	public function renameGroup(string $oldGroup, string $newGroup, ?string $user = null): ResponseInterface {
+		$credentials = $this->getAdminOrUserCredentials($user);
+		$groupId = $this->featureContext->getAttributeOfCreatedGroup($oldGroup, "id");
+
+		return GraphHelper::updateGroup(
+			$this->featureContext->getBaseUrl(),
+			$this->featureContext->getStepLineRef(),
+			$credentials['username'],
+			$credentials['password'],
+			$groupId,
+			$newGroup
+		);
+	}
+
+	/**
+	 * @When user :user renames group :oldGroup to :newGroup using the Graph API
+	 *
+	 * @param string $user
+	 * @param string $oldGroup
+	 * @param string $newGroup
+	 *
+	 * @return void
+	 */
+	public function userRenamesGroupUsingTheGraphApi(string $user, string $oldGroup, string $newGroup): void {
+		$this->featureContext->setResponse($this->renameGroup($oldGroup, $newGroup, $user));
+	}
 }