Repository: owncloud/ocis
Pull request #5070 (2022-11-17 06:08:56)

Bump github.com/go-micro/plugins/v4/events/natsjs from 1.1.0 to 1.2.0
Bumps [github.com/go-micro/plugins/v4/events/natsjs](https://github.com/go-micro/plugins) from 1.1.0 to 1.2.0.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a href="https://github.com/go-micro/plugins/compare/v3/auth/jwt/v1.1.0...v4/auth/jwt/v1.2.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/go-micro/plugins/v4/events/natsjs&package-manager=go_modules&previous-version=1.1.0&new-version=1.2.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/.drone.env b/.drone.env
index 751093365f1..2fed6d8c2d2 100644
--- a/.drone.env
+++ b/.drone.env
@@ -1,7 +1,7 @@
 # The test runner source for API tests
-CORE_COMMITID=ff3c509f6956ed6d1b51dab63176b122c2027cb0
+CORE_COMMITID=3ea9a1d44df397a504ca7ed8eb8c8c59b5a16852
 CORE_BRANCH=master
 
 # The test runner source for UI tests
-WEB_COMMITID=37e83b008f5baa762c6544e126e3836d44c11c10
-WEB_BRANCH=master
+WEB_COMMITID=1b489b1c198cc5759436b79c7f0021caa9a47f62
+WEB_BRANCH=stable-6.0
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fbc54469ee8..7f9aeff5e4f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,14 +6,24 @@ The following sections list the changes for unreleased.
 
 ## Summary
 
+* Bugfix - Lower IDP token lifespans: [#5077](https://github.com/owncloud/ocis/pull/5077)
 * Bugfix - Do not reindex a space twice at the same time: [#5001](https://github.com/owncloud/ocis/pull/5001)
 * Bugfix - Find spaces by their name: [#5044](https://github.com/owncloud/ocis/pull/5044)
+* 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)
 * 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)
 
 ## Details
 
+* Bugfix - Lower IDP token lifespans: [#5077](https://github.com/owncloud/ocis/pull/5077)
+
+   We've lowered the IDP token lifespans to more reasonable durations.
+
+   https://github.com/owncloud/ocis/pull/5077
+
 * Bugfix - Do not reindex a space twice at the same time: [#5001](https://github.com/owncloud/ocis/pull/5001)
 
    We fixed a problem where the search service reindexed a space while another reindex process was
@@ -28,6 +38,13 @@ The following sections list the changes for unreleased.
    https://github.com/owncloud/ocis/issues/4506
    https://github.com/owncloud/ocis/pull/5044
 
+* Bugfix - Adjust cache related configuration options: [#5087](https://github.com/owncloud/ocis/pull/5087)
+
+   We've adjusted cache related configuration options of the gateway and storage-users service
+   to the other services.
+
+   https://github.com/owncloud/ocis/pull/5087
+
 * Bugfix - Fix notifications Web UI url: [#4998](https://github.com/owncloud/ocis/pull/4998)
 
    We've fixed the configuration of the notification service's Web UI url that appears in emails.
@@ -37,6 +54,12 @@ The following sections list the changes for unreleased.
 
    https://github.com/owncloud/ocis/pull/4998
 
+* Bugfix - Initial role assingment with external IDM: [#5045](https://github.com/owncloud/ocis/issues/5045)
+
+   We've the initial user role assignment when using an external LDAP server.
+
+   https://github.com/owncloud/ocis/issues/5045
+
 * 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
@@ -50,6 +73,150 @@ The following sections list the changes for unreleased.
 
    https://github.com/owncloud/ocis/issues/5011
    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)
+
+   Tags: web
+
+   We updated ownCloud Web to v6.0.0-rc.6. Please refer to the changelog (linked) for details on
+   the web release.
+
+   ### Breaking changes * BREAKING CHANGE for users in
+   [owncloud/web#6648](https://github.com/owncloud/web/issues/6648): breaks existing
+   bookmarks - they won't resolve anymore. * BREAKING CHANGE for developers in
+   [owncloud/web#6648](https://github.com/owncloud/web/issues/6648): the appDefaults
+   composables from web-pkg now work with drive aliases, concatenated with relative item paths,
+   instead of webdav paths. If you use the appDefaults composables in your application it's
+   likely that your code needs to be adapted.
+
+   ### Changes * Bugfix [owncloud/web#7731](https://github.com/owncloud/web/pull/7731):
+   "Copy Quicklink"-translations * Bugfix
+   [owncloud/web#7419](https://github.com/owncloud/web/issues/7419): Add language
+   param opening external app * Bugfix
+   [owncloud/web#7830](https://github.com/owncloud/web/pull/7830): "Cut" and "Copy"
+   actions for current folder * Bugfix
+   [owncloud/web#7739](https://github.com/owncloud/web/pull/7739): Disable shares
+   loading on public and trash locations * Bugfix
+   [owncloud/web#7652](https://github.com/owncloud/web/pull/7652): Disable copy/move
+   overwrite on self * Bugfix
+   [owncloud/web#7740](https://github.com/owncloud/web/pull/7740): Disappearing
+   quicklink in sidebar * Bugfix
+   [owncloud/web#7936](https://github.com/owncloud/web/pull/7936): Editing text files
+   on public pages * Bugfix
+   [owncloud/web#7820](https://github.com/owncloud/web/pull/7820): Edit new created
+   user in user management * Bugfix
+   [owncloud/web#7861](https://github.com/owncloud/web/pull/7861): Handle non 2xx
+   external app responses * Bugfix
+   [owncloud/web#7975](https://github.com/owncloud/web/pull/7975): Prevent file upload
+   when folder creation failed * Bugfix
+   [owncloud/web#7734](https://github.com/owncloud/web/pull/7734): File name
+   reactivity * Bugfix [owncloud/web#7724](https://github.com/owncloud/web/pull/7724):
+   Folder conflict dialog * Bugfix
+   [owncloud/web#7889](https://github.com/owncloud/web/pull/7889): Hide share
+   indicators on public page * Bugfix
+   [owncloud/web#7603](https://github.com/owncloud/web/issues/7603): Hide search bar in
+   public link context * Bugfix
+   [owncloud/web#7903](https://github.com/owncloud/web/issues/7903): "Keep
+   both"-conflict option * Bugfix
+   [owncloud/web#7652](https://github.com/owncloud/web/pull/7652): Inhibit move files
+   between spaces * Bugfix
+   [owncloud/web#7697](https://github.com/owncloud/web/issues/7697): Link indicator on
+   "Shared with me"-page * Bugfix
+   [owncloud/web#7985](https://github.com/owncloud/web/pull/7985): Prevent retrying
+   uploads with status code 5xx * Bugfix
+   [owncloud/web#7941](https://github.com/owncloud/web/pull/7941): Add origin check to
+   Draw.io events * Bugfix
+   [owncloud/web#7811](https://github.com/owncloud/web/pull/7811): Do not load files
+   from cache in public links * Bugfix
+   [owncloud/web#7916](https://github.com/owncloud/web/pull/7916): Prefer alias links
+   over private links * Bugfix
+   [owncloud/web#7748](https://github.com/owncloud/web/pull/7748): Reload file list
+   after last share removal * Bugfix
+   [owncloud/web#7640](https://github.com/owncloud/web/pull/7640): "Private
+   link"-button alignment * Bugfix
+   [owncloud/web#7699](https://github.com/owncloud/web/issues/7699): Remove the "close
+   sidebar"-calls on delete * Bugfix
+   [owncloud/web#7504](https://github.com/owncloud/web/pull/7504): Resolve upload
+   existing folder * Bugfix
+   [owncloud/web#7771](https://github.com/owncloud/web/pull/7771): Routing for
+   re-shares * Bugfix [owncloud/web#7675](https://github.com/owncloud/web/pull/7675):
+   Search bar on small screens * Bugfix
+   [owncloud/web#7662](https://github.com/owncloud/web/pull/7662): Sidebar for
+   received shares in search file list * Bugfix
+   [owncloud/web#7873](https://github.com/owncloud/web/pull/7873): Share editing after
+   selecting a space * Bugfix
+   [owncloud/web#7657](https://github.com/owncloud/web/issues/7657): Share
+   permissions for re-shares * Bugfix
+   [owncloud/web#7506](https://github.com/owncloud/web/issues/7506): Shares loading *
+   Bugfix [owncloud/web#7632](https://github.com/owncloud/web/pull/7632): Sidebar
+   toggle icon * Bugfix
+   [owncloud/web#7781](https://github.com/owncloud/web/issues/7781): Sidebar without
+   highlighted resource * Bugfix
+   [owncloud/web#7756](https://github.com/owncloud/web/pull/7756): Try to obtain
+   refresh token before the error case * Bugfix
+   [owncloud/web#7768](https://github.com/owncloud/web/pull/7768): Hide actions in
+   space trash bins * Bugfix
+   [owncloud/web#7651](https://github.com/owncloud/web/pull/7651): Spaces on "Shared
+   via link"-page * Bugfix
+   [owncloud/web#7521](https://github.com/owncloud/web/issues/7521): Spaces
+   reactivity on update * Bugfix
+   [owncloud/web#7778](https://github.com/owncloud/web/issues/7778): Trash bin sidebar
+   * Bugfix [owncloud/web#7956](https://github.com/owncloud/web/issues/7956):
+   Introduce "upload finalizing"-state in upload overlay * Bugfix
+   [owncloud/web#7630](https://github.com/owncloud/web/pull/7630): Upload modify time *
+   Change [owncloud/web#6648](https://github.com/owncloud/web/issues/6648): Drive
+   aliases in URLs * Change
+   [owncloud/web#7935](https://github.com/owncloud/web/pull/7935): Remove mediaSource
+   and v-image-source * Enhancement
+   [owncloud/web#7635](https://github.com/owncloud/web/pull/7635): Add restore
+   conflict dialog * Enhancement
+   [owncloud/web#7901](https://github.com/owncloud/web/pull/7901): Add search field for
+   space members * Enhancement
+   [owncloud/web#4675](https://github.com/owncloud/web/issues/4675): Add
+   `X-Request-ID` header to all outgoing requests * Enhancement
+   [owncloud/web#7904](https://github.com/owncloud/web/pull/7904): Batch actions for
+   two or more items only * Enhancement
+   [owncloud/web#7892](https://github.com/owncloud/web/pull/7892): Respect the new
+   sharing denials capability (experimental) * Enhancement
+   [owncloud/web#7709](https://github.com/owncloud/web/pull/7709): Edit custom
+   permissions wording * Enhancement
+   [owncloud/web#7373](https://github.com/owncloud/web/issues/7373): Align dark mode
+   colors with given design * Enhancement
+   [owncloud/web#7190](https://github.com/owncloud/web/pull/7190): Deny subfolders
+   inside share * Enhancement
+   [owncloud/web#7684](https://github.com/owncloud/web/pull/7684): Design polishing *
+   Enhancement [owncloud/web#7865](https://github.com/owncloud/web/pull/7865):
+   Disable share renaming * Enhancement
+   [owncloud/web#7725](https://github.com/owncloud/web/pull/7725): Enable renaming on
+   received shares * Enhancement
+   [owncloud/web#7747](https://github.com/owncloud/web/pull/7747): Friendlier logout
+   screen * Enhancement
+   [owncloud/web#6247](https://github.com/owncloud/web/issues/6247): Id based routing *
+   Enhancement [owncloud/web#7803](https://github.com/owncloud/web/issues/7803):
+   Internal link on unaccepted share * Enhancement
+   [owncloud/web#7304](https://github.com/owncloud/web/issues/7304): Resolve internal
+   links * Enhancement [owncloud/web#7569](https://github.com/owncloud/web/pull/7569):
+   Make keybindings global * Enhancement
+   [owncloud/web#7894](https://github.com/owncloud/web/pull/7894): Optimize email
+   validation in the user management app * Enhancement
+   [owncloud/web#7707](https://github.com/owncloud/web/issues/7707): Resolve private
+   links * Enhancement
+   [owncloud/web#7234](https://github.com/owncloud/web/issues/7234): Auth context in
+   route meta props * Enhancement
+   [owncloud/web#7821](https://github.com/owncloud/web/pull/7821): Improve search
+   experience * Enhancement
+   [owncloud/web#7801](https://github.com/owncloud/web/pull/7801): Make search results
+   sortable * Enhancement
+   [owncloud/web#7915](https://github.com/owncloud/web/pull/7915): Update ODS to
+   v14.0.0-alpha.25 * Enhancement
+   [owncloud/web#7890](https://github.com/owncloud/web/pull/7890): Validate space
+   names * Enhancement [owncloud/web#7430](https://github.com/owncloud/web/pull/7430):
+   Webdav support in web-client package * Enhancement
+   [owncloud/web#7900](https://github.com/owncloud/web/issues/7900): XHR upload
+   timeout
+
+   https://github.com/owncloud/ocis/pull/5086
+   https://github.com/owncloud/web/releases/tag/v6.0.0-rc.7
 # Changelog for [2.0.0-rc.1] (2022-11-05)
 
 The following sections list the changes for 2.0.0-rc.1.
diff --git a/changelog/unreleased/change-lower-idp-token-lifespans.md b/changelog/unreleased/change-lower-idp-token-lifespans.md
new file mode 100644
index 00000000000..793041e20e7
--- /dev/null
+++ b/changelog/unreleased/change-lower-idp-token-lifespans.md
@@ -0,0 +1,5 @@
+Bugfix: Lower IDP token lifespans
+
+We've lowered the IDP token lifespans to more reasonable durations.
+
+https://github.com/owncloud/ocis/pull/5077
diff --git a/changelog/unreleased/fix-cache-store-env.md b/changelog/unreleased/fix-cache-store-env.md
new file mode 100644
index 00000000000..bccfe1d5722
--- /dev/null
+++ b/changelog/unreleased/fix-cache-store-env.md
@@ -0,0 +1,6 @@
+Bugfix: adjust cache related configuration options
+
+We've adjusted cache related configuration options of the
+gateway and storage-users service to the other services.
+
+https://github.com/owncloud/ocis/pull/5087
diff --git a/changelog/unreleased/initial-role-assignment.md b/changelog/unreleased/initial-role-assignment.md
new file mode 100644
index 00000000000..6ca708342cb
--- /dev/null
+++ b/changelog/unreleased/initial-role-assignment.md
@@ -0,0 +1,5 @@
+Bugfix: Initial role assingment with external IDM
+
+We've the initial user role assignment when using an external LDAP server.
+
+https://github.com/owncloud/ocis/issues/5045
diff --git a/changelog/unreleased/update-web-6.0.0-rc.7.md b/changelog/unreleased/update-web-6.0.0-rc.7.md
new file mode 100644
index 00000000000..c6477f125e8
--- /dev/null
+++ b/changelog/unreleased/update-web-6.0.0-rc.7.md
@@ -0,0 +1,81 @@
+Enhancement: Update ownCloud Web to v6.0.0-rc.6
+
+Tags: web
+
+We updated ownCloud Web to v6.0.0-rc.6. Please refer to the changelog (linked) for details on the web release.
+
+### Breaking changes
+* BREAKING CHANGE for users in [owncloud/web#6648](https://github.com/owncloud/web/issues/6648): breaks existing bookmarks - they won't resolve anymore.
+* BREAKING CHANGE for developers in [owncloud/web#6648](https://github.com/owncloud/web/issues/6648): the appDefaults composables from web-pkg now work with drive aliases, concatenated with relative item paths, instead of webdav paths. If you use the appDefaults composables in your application it's likely that your code needs to be adapted.
+
+### Changes
+* Bugfix [owncloud/web#7731](https://github.com/owncloud/web/pull/7731): "Copy Quicklink"-translations
+* Bugfix [owncloud/web#7419](https://github.com/owncloud/web/issues/7419): Add language param opening external app
+* Bugfix [owncloud/web#7830](https://github.com/owncloud/web/pull/7830): "Cut" and "Copy" actions for current folder
+* Bugfix [owncloud/web#7739](https://github.com/owncloud/web/pull/7739): Disable shares loading on public and trash locations
+* Bugfix [owncloud/web#7652](https://github.com/owncloud/web/pull/7652): Disable copy/move overwrite on self
+* Bugfix [owncloud/web#7740](https://github.com/owncloud/web/pull/7740): Disappearing quicklink in sidebar
+* Bugfix [owncloud/web#7936](https://github.com/owncloud/web/pull/7936): Editing text files on public pages
+* Bugfix [owncloud/web#7820](https://github.com/owncloud/web/pull/7820): Edit new created user in user management
+* Bugfix [owncloud/web#7861](https://github.com/owncloud/web/pull/7861): Handle non 2xx external app responses
+* Bugfix [owncloud/web#7975](https://github.com/owncloud/web/pull/7975): Prevent file upload when folder creation failed
+* Bugfix [owncloud/web#7734](https://github.com/owncloud/web/pull/7734): File name reactivity
+* Bugfix [owncloud/web#7724](https://github.com/owncloud/web/pull/7724): Folder conflict dialog
+* Bugfix [owncloud/web#7889](https://github.com/owncloud/web/pull/7889): Hide share indicators on public page
+* Bugfix [owncloud/web#7603](https://github.com/owncloud/web/issues/7603): Hide search bar in public link context
+* Bugfix [owncloud/web#7903](https://github.com/owncloud/web/issues/7903): "Keep both"-conflict option
+* Bugfix [owncloud/web#7652](https://github.com/owncloud/web/pull/7652): Inhibit move files between spaces
+* Bugfix [owncloud/web#7697](https://github.com/owncloud/web/issues/7697): Link indicator on "Shared with me"-page
+* Bugfix [owncloud/web#7985](https://github.com/owncloud/web/pull/7985): Prevent retrying uploads with status code 5xx
+* Bugfix [owncloud/web#7941](https://github.com/owncloud/web/pull/7941): Add origin check to Draw.io events
+* Bugfix [owncloud/web#7811](https://github.com/owncloud/web/pull/7811): Do not load files from cache in public links
+* Bugfix [owncloud/web#7916](https://github.com/owncloud/web/pull/7916): Prefer alias links over private links
+* Bugfix [owncloud/web#7748](https://github.com/owncloud/web/pull/7748): Reload file list after last share removal
+* Bugfix [owncloud/web#7640](https://github.com/owncloud/web/pull/7640): "Private link"-button alignment
+* Bugfix [owncloud/web#7699](https://github.com/owncloud/web/issues/7699): Remove the "close sidebar"-calls on delete
+* Bugfix [owncloud/web#7504](https://github.com/owncloud/web/pull/7504): Resolve upload existing folder
+* Bugfix [owncloud/web#7771](https://github.com/owncloud/web/pull/7771): Routing for re-shares
+* Bugfix [owncloud/web#7675](https://github.com/owncloud/web/pull/7675): Search bar on small screens
+* Bugfix [owncloud/web#7662](https://github.com/owncloud/web/pull/7662): Sidebar for received shares in search file list
+* Bugfix [owncloud/web#7873](https://github.com/owncloud/web/pull/7873): Share editing after selecting a space
+* Bugfix [owncloud/web#7657](https://github.com/owncloud/web/issues/7657): Share permissions for re-shares
+* Bugfix [owncloud/web#7506](https://github.com/owncloud/web/issues/7506): Shares loading
+* Bugfix [owncloud/web#7632](https://github.com/owncloud/web/pull/7632): Sidebar toggle icon
+* Bugfix [owncloud/web#7781](https://github.com/owncloud/web/issues/7781): Sidebar without highlighted resource
+* Bugfix [owncloud/web#7756](https://github.com/owncloud/web/pull/7756): Try to obtain refresh token before the error case
+* Bugfix [owncloud/web#7768](https://github.com/owncloud/web/pull/7768): Hide actions in space trash bins
+* Bugfix [owncloud/web#7651](https://github.com/owncloud/web/pull/7651): Spaces on "Shared via link"-page
+* Bugfix [owncloud/web#7521](https://github.com/owncloud/web/issues/7521): Spaces reactivity on update
+* Bugfix [owncloud/web#7778](https://github.com/owncloud/web/issues/7778): Trash bin sidebar
+* Bugfix [owncloud/web#7956](https://github.com/owncloud/web/issues/7956): Introduce "upload finalizing"-state in upload overlay
+* Bugfix [owncloud/web#7630](https://github.com/owncloud/web/pull/7630): Upload modify time
+* Change [owncloud/web#6648](https://github.com/owncloud/web/issues/6648): Drive aliases in URLs
+* Change [owncloud/web#7935](https://github.com/owncloud/web/pull/7935): Remove mediaSource and v-image-source
+* Enhancement [owncloud/web#7635](https://github.com/owncloud/web/pull/7635): Add restore conflict dialog
+* Enhancement [owncloud/web#7901](https://github.com/owncloud/web/pull/7901): Add search field for space members
+* Enhancement [owncloud/web#4675](https://github.com/owncloud/web/issues/4675): Add `X-Request-ID` header to all outgoing requests
+* Enhancement [owncloud/web#7904](https://github.com/owncloud/web/pull/7904): Batch actions for two or more items only
+* Enhancement [owncloud/web#7892](https://github.com/owncloud/web/pull/7892): Respect the new sharing denials capability (experimental)
+* Enhancement [owncloud/web#7709](https://github.com/owncloud/web/pull/7709): Edit custom permissions wording
+* Enhancement [owncloud/web#7373](https://github.com/owncloud/web/issues/7373): Align dark mode colors with given design
+* Enhancement [owncloud/web#7190](https://github.com/owncloud/web/pull/7190): Deny subfolders inside share
+* Enhancement [owncloud/web#7684](https://github.com/owncloud/web/pull/7684): Design polishing
+* Enhancement [owncloud/web#7865](https://github.com/owncloud/web/pull/7865): Disable share renaming
+* Enhancement [owncloud/web#7725](https://github.com/owncloud/web/pull/7725): Enable renaming on received shares
+* Enhancement [owncloud/web#7747](https://github.com/owncloud/web/pull/7747): Friendlier logout screen
+* Enhancement [owncloud/web#6247](https://github.com/owncloud/web/issues/6247): Id based routing
+* Enhancement [owncloud/web#7803](https://github.com/owncloud/web/issues/7803): Internal link on unaccepted share
+* Enhancement [owncloud/web#7304](https://github.com/owncloud/web/issues/7304): Resolve internal links
+* Enhancement [owncloud/web#7569](https://github.com/owncloud/web/pull/7569): Make keybindings global
+* Enhancement [owncloud/web#7894](https://github.com/owncloud/web/pull/7894): Optimize email validation in the user management app
+* Enhancement [owncloud/web#7707](https://github.com/owncloud/web/issues/7707): Resolve private links
+* Enhancement [owncloud/web#7234](https://github.com/owncloud/web/issues/7234): Auth context in route meta props
+* Enhancement [owncloud/web#7821](https://github.com/owncloud/web/pull/7821): Improve search experience
+* Enhancement [owncloud/web#7801](https://github.com/owncloud/web/pull/7801): Make search results sortable
+* Enhancement [owncloud/web#7915](https://github.com/owncloud/web/pull/7915): Update ODS to v14.0.0-alpha.25
+* Enhancement [owncloud/web#7890](https://github.com/owncloud/web/pull/7890): Validate space names
+* Enhancement [owncloud/web#7430](https://github.com/owncloud/web/pull/7430): Webdav support in web-client package
+* Enhancement [owncloud/web#7900](https://github.com/owncloud/web/issues/7900): XHR upload timeout
+
+https://github.com/owncloud/ocis/pull/5086
+https://github.com/owncloud/web/releases/tag/v6.0.0-rc.7
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..e8d17cf17ad 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.20221122203801-810f981c8d9b
 	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,7 +19,7 @@ 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/events/natsjs v1.2.0
 	github.com/go-micro/plugins/v4/logger/zerolog v1.1.0
 	github.com/go-micro/plugins/v4/registry/consul v1.2.0
 	github.com/go-micro/plugins/v4/registry/etcd v1.2.0
diff --git a/go.sum b/go.sum
index 969058a1354..00e21fe4655 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.20221122203801-810f981c8d9b h1:2/jP2wlUVwkAXSVllR/rwQEn460rmiUZXiuplcJ+T28=
+github.com/cs3org/reva/v2 v2.10.1-0.20221122203801-810f981c8d9b/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,8 +474,8 @@ 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/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.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/registry/consul v1.2.0 h1:nqrTzfWUTWKAy+M+i2FazbHnQn4m77EOtoch57kbCH4=
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/protogen/gen/ocis/messages/settings/v0/settings.pb.go b/protogen/gen/ocis/messages/settings/v0/settings.pb.go
index 70600cb9f19..ede907dba32 100644
--- a/protogen/gen/ocis/messages/settings/v0/settings.pb.go
+++ b/protogen/gen/ocis/messages/settings/v0/settings.pb.go
@@ -589,7 +589,6 @@ type Setting struct {
 	DisplayName string `protobuf:"bytes,3,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"`
 	Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"`
 	// Types that are assignable to Value:
-	//
 	//	*Setting_IntValue
 	//	*Setting_StringValue
 	//	*Setting_BoolValue
@@ -1194,7 +1193,6 @@ type Value struct {
 	AccountUuid string    `protobuf:"bytes,4,opt,name=account_uuid,json=accountUuid,proto3" json:"account_uuid,omitempty"`
 	Resource    *Resource `protobuf:"bytes,5,opt,name=resource,proto3" json:"resource,omitempty"`
 	// Types that are assignable to Value:
-	//
 	//	*Value_BoolValue
 	//	*Value_IntValue
 	//	*Value_StringValue
@@ -1385,7 +1383,6 @@ type ListOptionValue struct {
 	unknownFields protoimpl.UnknownFields
 
 	// Types that are assignable to Option:
-	//
 	//	*ListOptionValue_StringValue
 	//	*ListOptionValue_IntValue
 	Option isListOptionValue_Option `protobuf_oneof:"option"`
diff --git a/protogen/gen/ocis/services/thumbnails/v0/thumbnails.pb.go b/protogen/gen/ocis/services/thumbnails/v0/thumbnails.pb.go
index 25633f26e52..0329e98191c 100644
--- a/protogen/gen/ocis/services/thumbnails/v0/thumbnails.pb.go
+++ b/protogen/gen/ocis/services/thumbnails/v0/thumbnails.pb.go
@@ -37,7 +37,6 @@ type GetThumbnailRequest struct {
 	// The height of the thumbnail
 	Height int32 `protobuf:"varint,4,opt,name=height,proto3" json:"height,omitempty"`
 	// Types that are assignable to Source:
-	//
 	//	*GetThumbnailRequest_WebdavSource
 	//	*GetThumbnailRequest_Cs3Source
 	Source isGetThumbnailRequest_Source `protobuf_oneof:"source"`
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/gateway/pkg/config/config.go b/services/gateway/pkg/config/config.go
index 4b27f67b73f..8a9829df593 100644
--- a/services/gateway/pkg/config/config.go
+++ b/services/gateway/pkg/config/config.go
@@ -87,8 +87,8 @@ type StorageRegistry struct {
 
 // Cache holds cache config
 type Cache struct {
-	Store              string   `yaml:"store" env:"GATEWAY_CACHE_STORE" desc:"Store implementation for the cache. Valid values are \"memory\" (default), \"redis\", and \"etcd\"."`
-	Nodes              []string `yaml:"nodes" env:"GATEWAY_CACHE_NODES" desc:"Node addresses to use for the cache store."`
+	Store              string   `yaml:"store" env:"OCIS_CACHE_STORE_TYPE;GATEWAY_CACHE_STORE_TYPE;GATEWAY_CACHE_STORE" desc:"Store implementation for the cache. Valid values are \"memory\" (default), \"redis\", and \"etcd\"."`
+	Nodes              []string `yaml:"nodes" env:"OCIS_CACHE_STORE_ADDRESS;GATEWAY_CACHE_STORE_ADDRESS;GATEWAY_CACHE_NODES" desc:"Node addresses to use for the cache store."`
 	Database           string   `yaml:"database" env:"GATEWAY_CACHE_DATABASE" desc:"Database name of the cache."`
 	StatCacheTTL       int      `yaml:"stat_cache_ttl" env:"GATEWAY_STAT_CACHE_TTL" desc:"Max TTL in seconds for the gateway's stat cache."`
 	ProviderCacheTTL   int      `yaml:"provider_cache_ttl" env:"GATEWAY_PROVIDER_CACHE_TTL" desc:"Max TTL in seconds for the gateway's provider cache."`
diff --git a/services/graph/Makefile b/services/graph/Makefile
index d17a086a335..070214eac39 100644
--- a/services/graph/Makefile
+++ b/services/graph/Makefile
@@ -28,6 +28,8 @@ ci-go-generate: $(MOCKERY) # CI runs ci-node-generate automatically before this
 	$(MOCKERY) --dir pkg/service/v0 --case underscore --name GatewayClient
 	$(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/mocks/permissions.go b/services/graph/mocks/permissions.go
new file mode 100644
index 00000000000..78b47047059
--- /dev/null
+++ b/services/graph/mocks/permissions.go
@@ -0,0 +1,78 @@
+// Code generated by mockery v2.10.4. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	client "go-micro.dev/v4/client"
+
+	mock "github.com/stretchr/testify/mock"
+
+	v0 "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
+)
+
+// Permissions is an autogenerated mock type for the Permissions type
+type Permissions struct {
+	mock.Mock
+}
+
+// GetPermissionByID provides a mock function with given fields: ctx, request, opts
+func (_m *Permissions) GetPermissionByID(ctx context.Context, request *v0.GetPermissionByIDRequest, opts ...client.CallOption) (*v0.GetPermissionByIDResponse, error) {
+	_va := make([]interface{}, len(opts))
+	for _i := range opts {
+		_va[_i] = opts[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, request)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 *v0.GetPermissionByIDResponse
+	if rf, ok := ret.Get(0).(func(context.Context, *v0.GetPermissionByIDRequest, ...client.CallOption) *v0.GetPermissionByIDResponse); ok {
+		r0 = rf(ctx, request, opts...)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*v0.GetPermissionByIDResponse)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *v0.GetPermissionByIDRequest, ...client.CallOption) error); ok {
+		r1 = rf(ctx, request, opts...)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// ListPermissionsByResource provides a mock function with given fields: ctx, in, opts
+func (_m *Permissions) ListPermissionsByResource(ctx context.Context, in *v0.ListPermissionsByResourceRequest, opts ...client.CallOption) (*v0.ListPermissionsByResourceResponse, error) {
+	_va := make([]interface{}, len(opts))
+	for _i := range opts {
+		_va[_i] = opts[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, in)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 *v0.ListPermissionsByResourceResponse
+	if rf, ok := ret.Get(0).(func(context.Context, *v0.ListPermissionsByResourceRequest, ...client.CallOption) *v0.ListPermissionsByResourceResponse); ok {
+		r0 = rf(ctx, in, opts...)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*v0.ListPermissionsByResourceResponse)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *v0.ListPermissionsByResourceRequest, ...client.CallOption) error); ok {
+		r1 = rf(ctx, in, opts...)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
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 e75b09a86bf..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
@@ -200,10 +200,8 @@ func (g Graph) GetSingleDrive(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-func canCreateSpace(ctx context.Context, ownPersonalHome bool) bool {
-	s := settingssvc.NewPermissionService("com.owncloud.api.settings", grpc.DefaultClient())
-
-	pr, err := s.GetPermissionByID(ctx, &settingssvc.GetPermissionByIDRequest{
+func (g Graph) canCreateSpace(ctx context.Context, ownPersonalHome bool) bool {
+	pr, err := g.permissionsService.GetPermissionByID(ctx, &settingssvc.GetPermissionByIDRequest{
 		PermissionId: settingsServiceExt.CreateSpacePermissionID,
 	})
 	if err != nil || pr.Permission == nil {
@@ -228,7 +226,7 @@ func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
 	}
 
 	// TODO determine if the user tries to create his own personal space and pass that as a boolean
-	canCreateSpace := canCreateSpace(r.Context(), false)
+	canCreateSpace := g.canCreateSpace(r.Context(), false)
 	if !canCreateSpace {
 		logger.Debug().Bool("cancreatespace", canCreateSpace).Msg("could not create drive: insufficient permissions")
 		// if the permission is not existing for the user in context we can assume we don't have it. Return 401.
@@ -708,7 +706,7 @@ func (g Graph) getDriveQuota(ctx context.Context, space *storageprovider.Storage
 		return nil, nil
 	case res.GetStatus().GetCode() != cs3rpc.Code_CODE_OK:
 		logger.Debug().Str("grpc", res.GetStatus().GetMessage()).Msg("error sending get quota grpc request")
-		return nil, err
+		return nil, errors.New(res.GetStatus().GetMessage())
 	}
 
 	var remaining int64
diff --git a/services/graph/pkg/service/v0/graph.go b/services/graph/pkg/service/v0/graph.go
index 45f6ab68a78..f1aa0d140c6 100644
--- a/services/graph/pkg/service/v0/graph.go
+++ b/services/graph/pkg/service/v0/graph.go
@@ -15,6 +15,7 @@ import (
 	settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
 	"github.com/owncloud/ocis/v2/services/graph/pkg/config"
 	"github.com/owncloud/ocis/v2/services/graph/pkg/identity"
+	"go-micro.dev/v4/client"
 	mevents "go-micro.dev/v4/events"
 	"google.golang.org/grpc"
 )
@@ -62,6 +63,12 @@ type Publisher interface {
 	Publish(string, interface{}, ...mevents.PublishOption) error
 }
 
+// Permissions is the interface used to access the permissions service
+type Permissions interface {
+	GetPermissionByID(ctx context.Context, request *settingssvc.GetPermissionByIDRequest, opts ...client.CallOption) (*settingssvc.GetPermissionByIDResponse, error)
+	ListPermissionsByResource(ctx context.Context, in *settingssvc.ListPermissionsByResourceRequest, opts ...client.CallOption) (*settingssvc.ListPermissionsByResourceResponse, error)
+}
+
 // HTTPClient is the subset of the http.Client that is being used to interact with the download gateway
 type HTTPClient interface {
 	Do(req *http.Request) (*http.Response, error)
@@ -78,6 +85,7 @@ type Graph struct {
 	identityBackend      identity.Backend
 	gatewayClient        GatewayClient
 	roleService          settingssvc.RoleService
+	permissionsService   Permissions
 	spacePropertiesCache *ttlcache.Cache
 	eventsPublisher      events.Publisher
 }
@@ -111,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/instrument.go b/services/graph/pkg/service/v0/instrument.go
index 80503fe9be2..0b20788c26c 100644
--- a/services/graph/pkg/service/v0/instrument.go
+++ b/services/graph/pkg/service/v0/instrument.go
@@ -103,3 +103,13 @@ func (i instrument) DeleteGroupMember(w http.ResponseWriter, r *http.Request) {
 func (i instrument) GetDrives(w http.ResponseWriter, r *http.Request) {
 	i.next.GetDrives(w, r)
 }
+
+// GetAllDrives implements the Service interface.
+func (i instrument) GetAllDrives(w http.ResponseWriter, r *http.Request) {
+	i.next.GetAllDrives(w, r)
+}
+
+// CreateDrive implements the Service interface.
+func (i instrument) CreateDrive(w http.ResponseWriter, r *http.Request) {
+	i.next.CreateDrive(w, r)
+}
diff --git a/services/graph/pkg/service/v0/logging.go b/services/graph/pkg/service/v0/logging.go
index 51213475d7e..e9bd3aa12e1 100644
--- a/services/graph/pkg/service/v0/logging.go
+++ b/services/graph/pkg/service/v0/logging.go
@@ -103,3 +103,13 @@ func (l logging) DeleteGroupMember(w http.ResponseWriter, r *http.Request) {
 func (l logging) GetDrives(w http.ResponseWriter, r *http.Request) {
 	l.next.GetDrives(w, r)
 }
+
+// GetAllDrives implements the Service interface.
+func (l logging) GetAllDrives(w http.ResponseWriter, r *http.Request) {
+	l.next.GetAllDrives(w, r)
+}
+
+// CreateDrive implements the Service interface.
+func (l logging) CreateDrive(w http.ResponseWriter, r *http.Request) {
+	l.next.CreateDrive(w, r)
+}
diff --git a/services/graph/pkg/service/v0/option.go b/services/graph/pkg/service/v0/option.go
index 752c7de3c75..dcaa01a4228 100644
--- a/services/graph/pkg/service/v0/option.go
+++ b/services/graph/pkg/service/v0/option.go
@@ -16,14 +16,15 @@ type Option func(o *Options)
 
 // Options defines the available options for this package.
 type Options struct {
-	Logger          log.Logger
-	Config          *config.Config
-	Middleware      []func(http.Handler) http.Handler
-	GatewayClient   GatewayClient
-	IdentityBackend identity.Backend
-	RoleService     settingssvc.RoleService
-	RoleManager     *roles.Manager
-	EventsPublisher events.Publisher
+	Logger            log.Logger
+	Config            *config.Config
+	Middleware        []func(http.Handler) http.Handler
+	GatewayClient     GatewayClient
+	IdentityBackend   identity.Backend
+	RoleService       settingssvc.RoleService
+	PermissionService Permissions
+	RoleManager       *roles.Manager
+	EventsPublisher   events.Publisher
 }
 
 // newOptions initializes the available default options.
@@ -79,6 +80,13 @@ func RoleService(val settingssvc.RoleService) Option {
 	}
 }
 
+// PermissionService provides a function to set the PermissionService option.
+func PermissionService(val settingssvc.PermissionService) Option {
+	return func(o *Options) {
+		o.PermissionService = val
+	}
+}
+
 // RoleManager provides a function to set the RoleManager option.
 func RoleManager(val *roles.Manager) Option {
 	return func(o *Options) {
diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go
index 94be2c07b11..e3e6d76a09c 100644
--- a/services/graph/pkg/service/v0/service.go
+++ b/services/graph/pkg/service/v0/service.go
@@ -48,6 +48,8 @@ type Service interface {
 	DeleteGroupMember(http.ResponseWriter, *http.Request)
 
 	GetDrives(w http.ResponseWriter, r *http.Request)
+	GetAllDrives(w http.ResponseWriter, r *http.Request)
+	CreateDrive(w http.ResponseWriter, r *http.Request)
 }
 
 // NewService returns a service implementation for Service.
@@ -145,6 +147,12 @@ func NewService(opts ...Option) Service {
 		svc.roleService = options.RoleService
 	}
 
+	if options.PermissionService == nil {
+		svc.permissionsService = settingssvc.NewPermissionService("com.owncloud.api.settings", grpc.DefaultClient())
+	} else {
+		svc.permissionsService = options.PermissionService
+	}
+
 	roleManager := options.RoleManager
 	if roleManager == nil {
 		storeOptions := store.OcisStoreOptions{
diff --git a/services/graph/pkg/service/v0/tracing.go b/services/graph/pkg/service/v0/tracing.go
index d3a0167b463..0721dd63eb9 100644
--- a/services/graph/pkg/service/v0/tracing.go
+++ b/services/graph/pkg/service/v0/tracing.go
@@ -99,3 +99,13 @@ func (t tracing) DeleteGroupMember(w http.ResponseWriter, r *http.Request) {
 func (t tracing) GetDrives(w http.ResponseWriter, r *http.Request) {
 	t.next.GetDrives(w, r)
 }
+
+// GetAllDrives implements the Service interface.
+func (t tracing) GetAllDrives(w http.ResponseWriter, r *http.Request) {
+	t.next.GetAllDrives(w, r)
+}
+
+// CreateDrive implements the Service interface.
+func (t tracing) CreateDrive(w http.ResponseWriter, r *http.Request) {
+	t.next.CreateDrive(w, r)
+}
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 515b6970c3e..45a246c4af2 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'."`
@@ -111,8 +111,8 @@ type Settings struct {
 	CookieBackendURI string
 	CookieNames      []string
 
-	AccessTokenDurationSeconds        uint64 `yaml:"access_token_duration_seconds" env:"IDP_ACCESS_TOKEN_EXPIRATION" desc:"Expiration time in seconds for IDP access token."`
-	IDTokenDurationSeconds            uint64 `yaml:"id_token_duration_seconds" env:"IDP_ID_TOKEN_EXPIRATION" desc:"Expiration time in seconds for IDP ID tokens."`
-	RefreshTokenDurationSeconds       uint64 `yaml:"refresh_token_duration_seconds" env:"IDP_REFRESH_TOKEN_EXPIRATION" desc:"Expiration time in seconds for refresh tokens."`
-	DyamicClientSecretDurationSeconds uint64 `yaml:"dynamic_client_secret_duration_seconds" env:"IDP_DYNAMIC_CLIENT_SECRET_DURATION" desc:"Expiration time in seconds for dynamic clients."`
+	AccessTokenDurationSeconds        uint64 `yaml:"access_token_duration_seconds" env:"IDP_ACCESS_TOKEN_EXPIRATION" desc:"'Access token lifespan in seconds (time before an access token is expired).'"`
+	IDTokenDurationSeconds            uint64 `yaml:"id_token_duration_seconds" env:"IDP_ID_TOKEN_EXPIRATION" desc:"ID token lifespan in seconds (time before an ID token is expired)."`
+	RefreshTokenDurationSeconds       uint64 `yaml:"refresh_token_duration_seconds" env:"IDP_REFRESH_TOKEN_EXPIRATION" desc:"Refresh token lifespan in seconds (time before an refresh token is expired). This also limits the duration of an idle offline session."`
+	DyamicClientSecretDurationSeconds uint64 `yaml:"dynamic_client_secret_duration_seconds" env:"IDP_DYNAMIC_CLIENT_SECRET_DURATION" desc:"Lifespan in seconds of a dynamically registered OIDC client."`
 }
diff --git a/services/idp/pkg/config/defaults/defaultconfig.go b/services/idp/pkg/config/defaults/defaultconfig.go
index fbd928be42f..ab9c730e3c2 100644
--- a/services/idp/pkg/config/defaults/defaultconfig.go
+++ b/services/idp/pkg/config/defaults/defaultconfig.go
@@ -61,9 +61,9 @@ func DefaultConfig() *config.Config {
 			ValidationKeysPath:                "",
 			CookieBackendURI:                  "",
 			CookieNames:                       nil,
-			AccessTokenDurationSeconds:        60 * 60 * 24,           // 1 day
-			IDTokenDurationSeconds:            60 * 60,                // 1 hour
-			RefreshTokenDurationSeconds:       60 * 60 * 24 * 365 * 3, // 1 year
+			AccessTokenDurationSeconds:        60 * 5,            // 5 minutes
+			IDTokenDurationSeconds:            60 * 5,            // 5 minutes
+			RefreshTokenDurationSeconds:       60 * 60 * 24 * 30, // 30 days
 			DyamicClientSecretDurationSeconds: 0,
 		},
 		Clients: []config.Client{
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/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/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/proxy/pkg/user/backend/cs3.go b/services/proxy/pkg/user/backend/cs3.go
index 997ba33b925..bd848cc73ad 100644
--- a/services/proxy/pkg/user/backend/cs3.go
+++ b/services/proxy/pkg/user/backend/cs3.go
@@ -87,9 +87,7 @@ func (c *cs3backend) GetUserByClaims(ctx context.Context, claim, value string, w
 				// https://github.com/owncloud/ocis/v2/issues/1825 for more context.
 				if user.Id.Type == cs3.UserType_USER_TYPE_PRIMARY {
 					c.logger.Info().Str("userid", user.Id.OpaqueId).Msg("user has no role assigned, assigning default user role")
-					// Updating context to have the Account-ID field and suffixing with _init
-					// so that the safety check for setting users' own role doesn't fail
-					ctx = metadata.Set(ctx, middleware.AccountID, user.Id.OpaqueId+"_init")
+					ctx = metadata.Set(ctx, middleware.AccountID, user.Id.OpaqueId)
 					_, err := c.settingsRoleService.AssignRoleToUser(ctx, &settingssvc.AssignRoleToUserRequest{
 						AccountUuid: user.Id.OpaqueId,
 						RoleId:      settingsService.BundleUUIDRoleUser,
diff --git a/services/settings/pkg/service/v0/service.go b/services/settings/pkg/service/v0/service.go
index 33c3cadd085..0b9955381cd 100644
--- a/services/settings/pkg/service/v0/service.go
+++ b/services/settings/pkg/service/v0/service.go
@@ -15,6 +15,7 @@ import (
 	settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
 	"github.com/owncloud/ocis/v2/services/settings/pkg/config"
 	"github.com/owncloud/ocis/v2/services/settings/pkg/settings"
+	"github.com/owncloud/ocis/v2/services/settings/pkg/store/defaults"
 	filestore "github.com/owncloud/ocis/v2/services/settings/pkg/store/filesystem"
 	metastore "github.com/owncloud/ocis/v2/services/settings/pkg/store/metadata"
 	merrors "go-micro.dev/v4/errors"
@@ -392,10 +393,6 @@ func (g Service) ListRoleAssignments(ctx context.Context, req *settingssvc.ListR
 
 // AssignRoleToUser implements the RoleServiceHandler interface
 func (g Service) AssignRoleToUser(ctx context.Context, req *settingssvc.AssignRoleToUserRequest, res *settingssvc.AssignRoleToUserResponse) error {
-	if !g.canManageRoles(ctx) {
-		return merrors.Forbidden(g.id, "user has no role management permission")
-	}
-
 	req.AccountUuid = getValidatedAccountUUID(ctx, req.AccountUuid)
 	if validationError := validateAssignRoleToUser(req); validationError != nil {
 		return merrors.BadRequest(g.id, validationError.Error())
@@ -406,9 +403,21 @@ func (g Service) AssignRoleToUser(ctx context.Context, req *settingssvc.AssignRo
 		g.logger.Debug().Str("id", g.id).Msg("user not in context")
 		return merrors.InternalServerError(g.id, "user not in context")
 	}
-	if ownAccountUUID == req.AccountUuid {
-		g.logger.Debug().Str("id", g.id).Msg("Changing own role assignment forbidden")
-		return merrors.Forbidden(g.id, "Changing own role assignment forbidden")
+
+	switch {
+	case ownAccountUUID == req.AccountUuid:
+		// Allow users to assign themself to the user role
+		// deny any other attempt to change	the user's own assignment
+		if r, err := g.manager.ListRoleAssignments(req.AccountUuid); err == nil && len(r) > 0 {
+			return merrors.Forbidden(g.id, "Changing own role assignment forbidden")
+		}
+		if req.RoleId != defaults.BundleUUIDRoleUser {
+			return merrors.Forbidden(g.id, "Changing own role assignment forbidden")
+		}
+		g.logger.Debug().Str("userid", ownAccountUUID).Msg("Self-assignment for default 'user' role permitted")
+	case g.canManageRoles(ctx):
+	default:
+		return merrors.Forbidden(g.id, "user has no role management permission")
 	}
 
 	r, err := g.manager.WriteRoleAssignment(req.AccountUuid, req.RoleId)
diff --git a/services/storage-users/pkg/config/config.go b/services/storage-users/pkg/config/config.go
index 5fc019b95d1..ad77c9fb657 100644
--- a/services/storage-users/pkg/config/config.go
+++ b/services/storage-users/pkg/config/config.go
@@ -144,8 +144,8 @@ type Events struct {
 
 // Cache holds cache config
 type Cache struct {
-	Store    string   `yaml:"store" env:"STORAGE_USERS_CACHE_STORE" desc:"Store implementation for the cache. Valid values are \"memory\" (default), \"redis\", and \"etcd\"."`
-	Nodes    []string `yaml:"nodes" env:"STORAGE_USERS_CACHE_NODES" desc:"Node addresses to use for the cache store."`
+	Store    string   `yaml:"store" env:"OCIS_CACHE_STORE_TYPE;STORAGE_USERS_CACHE_STORE_TYPE;STORAGE_USERS_CACHE_STORE" desc:"Store implementation for the cache. Valid values are \"memory\" (default), \"redis\", and \"etcd\"."`
+	Nodes    []string `yaml:"nodes" env:"OCIS_CACHE_STORE_ADDRESS;STORAGE_USERS_CACHE_STORE_ADDRESS;STORAGE_USERS_CACHE_NODES" desc:"Node addresses to use for the cache store."`
 	Database string   `yaml:"database" env:"STORAGE_USERS_CACHE_DATABASE" desc:"Database name of the cache."`
 }
 
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'."`
diff --git a/services/web/Makefile b/services/web/Makefile
index 5cabdcaf469..734cad2e00e 100644
--- a/services/web/Makefile
+++ b/services/web/Makefile
@@ -1,6 +1,6 @@
 SHELL := bash
 NAME := web
-WEB_ASSETS_VERSION = v6.0.0-rc.6
+WEB_ASSETS_VERSION = v6.0.0-rc.7
 
 include ../../.make/recursion.mk
 
diff --git a/services/web/web.go b/services/web/web.go
index 97753be529d..6830e279e8c 100644
--- a/services/web/web.go
+++ b/services/web/web.go
@@ -5,5 +5,5 @@ import (
 )
 
 //go:generate make generate
-//go:embed assets/*
+//go:embed all:assets/*
 var Assets embed.FS
diff --git a/sonar-project.properties b/sonar-project.properties
index 9ae382cd61a..4382ec4e63b 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -37,5 +37,5 @@ sonar.go.golangci-lint.reportPaths=cache/checkstyle/app-provider_checkstyle.xml,
 
 # Exclude files
 sonar.exclusions=**/third_party,docs/**,changelog/**,*/pkg/assets/embed.go,idp/assets/identifier/**,**/package.json,**/rollup.config.js,CHANGELOG.md,**/pkg/proto/**/*.pb.*,deployments/**,tests/**,vendor-bin/**,README.md,**/mocks/
-sonar.coverage.exclusions=**/*_test.go
+sonar.coverage.exclusions=**/*_test.go,**mocks/**,/protogen/**
 sonar.cpd.exclusions=**/*_test.go


Test patch:
diff --git a/services/graph/pkg/service/v0/drives_test.go b/services/graph/pkg/service/v0/drives_test.go
index ac18991b473..2c13866e30a 100644
--- a/services/graph/pkg/service/v0/drives_test.go
+++ b/services/graph/pkg/service/v0/drives_test.go
@@ -153,4 +153,7 @@ func TestSpaceNameValidation(t *testing.T) {
 		err := validateSpaceName(tc.SpaceName)
 		require.Equal(t, tc.ExpectedError, err, tc.Alias)
 	}
+
+	// set max length back to protect other tests
+	_maxSpaceNameLength = 255
 }
diff --git a/services/graph/pkg/service/v0/graph_test.go b/services/graph/pkg/service/v0/graph_test.go
index 6eff1f2da10..201ea998e39 100644
--- a/services/graph/pkg/service/v0/graph_test.go
+++ b/services/graph/pkg/service/v0/graph_test.go
@@ -1,43 +1,49 @@
 package svc_test
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
 	"io"
 	"net/http"
 	"net/http/httptest"
-	"net/url"
 	"time"
 
 	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
+	userprovider "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
 	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
+	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
 	"github.com/cs3org/reva/v2/pkg/rgrpc/status"
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	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"
+	v0 "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0"
+	settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
 	"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"
 	service "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0"
 	"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
+	"github.com/pkg/errors"
 	"github.com/stretchr/testify/mock"
 )
 
 var _ = Describe("Graph", func() {
 	var (
-		svc             service.Service
-		gatewayClient   *mocks.GatewayClient
-		eventsPublisher mocks.Publisher
-		ctx             context.Context
-		cfg             *config.Config
+		svc               service.Service
+		gatewayClient     *mocks.GatewayClient
+		eventsPublisher   mocks.Publisher
+		permissionService mocks.Permissions
+		ctx               context.Context
+		cfg               *config.Config
 	)
 
 	JustBeforeEach(func() {
-		ctx = context.Background()
+		ctx = ctxpkg.ContextSetUser(context.Background(), &userprovider.User{Id: &userprovider.UserId{Type: userprovider.UserType_USER_TYPE_PRIMARY, OpaqueId: "testuser"}, Username: "testuser"})
 		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"
@@ -47,10 +53,12 @@ var _ = Describe("Graph", func() {
 		_ = ogrpc.Configure(ogrpc.GetClientOptions(cfg.GRPCClientTLS)...)
 		gatewayClient = &mocks.GatewayClient{}
 		eventsPublisher = mocks.Publisher{}
+		permissionService = mocks.Permissions{}
 		svc = service.NewService(
 			service.Config(cfg),
 			service.WithGatewayClient(gatewayClient),
 			service.EventsPublisher(&eventsPublisher),
+			service.PermissionService(&permissionService),
 		)
 	})
 
@@ -60,7 +68,7 @@ var _ = Describe("Graph", func() {
 		})
 	})
 
-	Describe("drive", func() {
+	Describe("List drives", func() {
 		It("can list an empty list of spaces", func() {
 			gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
 				Status:        status.NewOK(ctx),
@@ -72,9 +80,20 @@ var _ = Describe("Graph", func() {
 			svc.GetDrives(rr, r)
 			Expect(rr.Code).To(Equal(http.StatusOK))
 		})
+		It("can list an empty list of all spaces", func() {
+			gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Times(1).Return(&provider.ListStorageSpacesResponse{
+				Status:        status.NewOK(ctx),
+				StorageSpaces: []*provider.StorageSpace{},
+			}, nil)
+
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/drives", nil)
+			rr := httptest.NewRecorder()
+			svc.GetAllDrives(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusOK))
+		})
 
 		It("can list a space without owner", func() {
-			gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
+			gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Times(1).Return(&provider.ListStorageSpacesResponse{
 				Status: status.NewOK(ctx),
 				StorageSpaces: []*provider.StorageSpace{
 					{
@@ -256,12 +275,11 @@ var _ = Describe("Graph", func() {
 			Expect(err).ToNot(HaveOccurred())
 			Expect(len(response["value"])).To(Equal(1))
 			value := response["value"][0]
-			webdavURL, _ := url.PathUnescape(*value.Root.WebDavUrl)
 			Expect(*value.DriveAlias).To(Equal("mountpoint/new-folder"))
 			Expect(*value.DriveType).To(Equal("mountpoint"))
 			Expect(*value.Id).To(Equal("prID$aID!differentID"))
 			Expect(*value.Name).To(Equal("New Folder"))
-			Expect(webdavURL).To(Equal("https://localhost:9200/dav/spaces/prID$aID!differentID"))
+			Expect(*value.Root.WebDavUrl).To(Equal("https://localhost:9200/dav/spaces/prID$aID%21differentID"))
 			Expect(*value.Root.ETag).To(Equal("101112131415"))
 			Expect(*value.Root.Id).To(Equal("prID$aID!differentID"))
 			Expect(*value.Root.RemoteItem.ETag).To(Equal("123456789"))
@@ -296,27 +314,391 @@ var _ = Describe("Graph", func() {
 			Expect(libreError.Error.Code).To(Equal(errorcode.InvalidRequest.String()))
 		})
 		It("can list a spaces with invalid query parameter", func() {
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives?§orderby=owner%20asc", nil)
+			rr := httptest.NewRecorder()
+			svc.GetDrives(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+
+			body, _ := io.ReadAll(rr.Body)
+			var libreError libregraph.OdataError
+			err := json.Unmarshal(body, &libreError)
+			Expect(err).To(Not(HaveOccurred()))
+			Expect(libreError.Error.Message).To(Equal("Query parameter '§orderby' is not supported. Cause: Query parameter '§orderby' is not supported"))
+			Expect(libreError.Error.Code).To(Equal(errorcode.InvalidRequest.String()))
+		})
+		It("can list a spaces with an unsupported operand", func() {
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives?$filter=contains(driveType,personal)", nil)
+			rr := httptest.NewRecorder()
+			svc.GetDrives(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusNotImplemented))
+
+			body, _ := io.ReadAll(rr.Body)
+			var libreError libregraph.OdataError
+			err := json.Unmarshal(body, &libreError)
+			Expect(err).To(Not(HaveOccurred()))
+			Expect(libreError.Error.Message).To(Equal("unsupported filter operand: contains"))
+			Expect(libreError.Error.Code).To(Equal(errorcode.NotSupported.String()))
+		})
+		It("transport error", func() {
+			gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(nil, errors.New("transport error"))
+
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives)", nil)
+			rr := httptest.NewRecorder()
+			svc.GetDrives(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusInternalServerError))
+
+			body, _ := io.ReadAll(rr.Body)
+			var libreError libregraph.OdataError
+			err := json.Unmarshal(body, &libreError)
+			Expect(err).To(Not(HaveOccurred()))
+			Expect(libreError.Error.Message).To(Equal("transport error"))
+			Expect(libreError.Error.Code).To(Equal(errorcode.GeneralException.String()))
+		})
+		It("grpc error", func() {
 			gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
-				Status:        status.NewOK(ctx),
+				Status:        status.NewInternal(ctx, "internal error"),
+				StorageSpaces: []*provider.StorageSpace{}}, nil)
+
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives)", nil)
+			rr := httptest.NewRecorder()
+			svc.GetDrives(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusInternalServerError))
+
+			body, _ := io.ReadAll(rr.Body)
+			var libreError libregraph.OdataError
+			err := json.Unmarshal(body, &libreError)
+			Expect(err).To(Not(HaveOccurred()))
+			Expect(libreError.Error.Message).To(Equal("internal error"))
+			Expect(libreError.Error.Code).To(Equal(errorcode.GeneralException.String()))
+		})
+		It("grpc not found", func() {
+			gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
+				Status:        status.NewNotFound(ctx, "no spaces found"),
 				StorageSpaces: []*provider.StorageSpace{}}, nil)
+
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives)", nil)
+			rr := httptest.NewRecorder()
+			svc.GetDrives(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusOK))
+
+			body, _ := io.ReadAll(rr.Body)
+
+			var response map[string][]libregraph.Drive
+			err := json.Unmarshal(body, &response)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(len(response)).To(Equal(0))
+		})
+		It("quota error", func() {
+			gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
+				Status: status.NewOK(ctx),
+				StorageSpaces: []*provider.StorageSpace{
+					{
+						Id:        &provider.StorageSpaceId{OpaqueId: "sameID"},
+						SpaceType: "aspacetype",
+						Root: &provider.ResourceId{
+							StorageId: "pro-1",
+							SpaceId:   "sameID",
+							OpaqueId:  "sameID",
+						},
+						Name: "aspacename",
+					},
+				},
+			}, nil)
 			gatewayClient.On("InitiateFileDownload", mock.Anything, mock.Anything).Return(&gateway.InitiateFileDownloadResponse{
 				Status: status.NewNotFound(ctx, "not found"),
 			}, nil)
 			gatewayClient.On("GetQuota", mock.Anything, mock.Anything).Return(&provider.GetQuotaResponse{
-				Status: status.NewUnimplemented(ctx, fmt.Errorf("not supported"), "not supported"),
+				Status: status.NewInternal(ctx, "internal quota error"),
 			}, nil)
 
-			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives?§orderby=owner%20asc", nil)
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives", nil)
 			rr := httptest.NewRecorder()
 			svc.GetDrives(rr, r)
+
+			Expect(rr.Code).To(Equal(http.StatusInternalServerError))
+
+			body, _ := io.ReadAll(rr.Body)
+			var libreError libregraph.OdataError
+			err := json.Unmarshal(body, &libreError)
+			Expect(err).To(Not(HaveOccurred()))
+			Expect(libreError.Error.Message).To(Equal("internal quota error"))
+			Expect(libreError.Error.Code).To(Equal(errorcode.GeneralException.String()))
+		})
+	})
+	Describe("Create Drive", func() {
+		It("cannot create a space without valid user in context", func() {
+			jsonBody := []byte(`{"Name": "Test Space"}`)
+			r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/drives", bytes.NewBuffer(jsonBody))
+			rr := httptest.NewRecorder()
+			svc.CreateDrive(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusUnauthorized))
+
+			body, _ := io.ReadAll(rr.Body)
+			var libreError libregraph.OdataError
+			err := json.Unmarshal(body, &libreError)
+			Expect(err).To(Not(HaveOccurred()))
+			Expect(libreError.Error.Message).To(Equal("invalid user"))
+			Expect(libreError.Error.Code).To(Equal(errorcode.NotAllowed.String()))
+		})
+		It("cannot create a space without permission", func() {
+			permissionService.On("GetPermissionByID", mock.Anything, mock.Anything).Return(&settingssvc.GetPermissionByIDResponse{
+				Permission: &v0.Permission{
+					Operation:  v0.Permission_OPERATION_UNKNOWN,
+					Constraint: v0.Permission_CONSTRAINT_OWN,
+				},
+			}, nil)
+			jsonBody := []byte(`{"Name": "Test Space"}`)
+			r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/drives", bytes.NewBuffer(jsonBody)).WithContext(ctx)
+			rr := httptest.NewRecorder()
+			svc.CreateDrive(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusUnauthorized))
+
+			body, _ := io.ReadAll(rr.Body)
+			var libreError libregraph.OdataError
+			err := json.Unmarshal(body, &libreError)
+			Expect(err).To(Not(HaveOccurred()))
+			Expect(libreError.Error.Message).To(Equal("insufficient permissions to create a space."))
+			Expect(libreError.Error.Code).To(Equal(errorcode.NotAllowed.String()))
+		})
+		It("cannot create a space with wrong request body", func() {
+			permissionService.On("GetPermissionByID", mock.Anything, mock.Anything).Return(&settingssvc.GetPermissionByIDResponse{
+				Permission: &v0.Permission{
+					Operation:  v0.Permission_OPERATION_READWRITE,
+					Constraint: v0.Permission_CONSTRAINT_ALL,
+				},
+			}, nil)
+			jsonBody := []byte(`{"Name": "Test Space"`)
+			r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/drives", bytes.NewBuffer(jsonBody)).WithContext(ctx)
+			rr := httptest.NewRecorder()
+			svc.CreateDrive(rr, r)
 			Expect(rr.Code).To(Equal(http.StatusBadRequest))
 
 			body, _ := io.ReadAll(rr.Body)
 			var libreError libregraph.OdataError
 			err := json.Unmarshal(body, &libreError)
 			Expect(err).To(Not(HaveOccurred()))
-			Expect(libreError.Error.Message).To(Equal("Query parameter '§orderby' is not supported. Cause: Query parameter '§orderby' is not supported"))
+			Expect(libreError.Error.Message).To(Equal("invalid body schema definition"))
+			Expect(libreError.Error.Code).To(Equal(errorcode.InvalidRequest.String()))
+		})
+		It("transport error", func() {
+			permissionService.On("GetPermissionByID", mock.Anything, mock.Anything).Return(&settingssvc.GetPermissionByIDResponse{
+				Permission: &v0.Permission{
+					Operation:  v0.Permission_OPERATION_READWRITE,
+					Constraint: v0.Permission_CONSTRAINT_ALL,
+				},
+			}, nil)
+			gatewayClient.On("CreateStorageSpace", mock.Anything, mock.Anything).Return(&provider.CreateStorageSpaceResponse{}, errors.New("transport error"))
+			jsonBody := []byte(`{"Name": "Test Space"}`)
+			r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/drives", bytes.NewBuffer(jsonBody)).WithContext(ctx)
+			rr := httptest.NewRecorder()
+			svc.CreateDrive(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusInternalServerError))
+
+			body, _ := io.ReadAll(rr.Body)
+			var libreError libregraph.OdataError
+			err := json.Unmarshal(body, &libreError)
+			Expect(err).To(Not(HaveOccurred()))
+			Expect(libreError.Error.Message).To(Equal("transport error"))
+			Expect(libreError.Error.Code).To(Equal(errorcode.GeneralException.String()))
+		})
+		It("grpc permission denied error", func() {
+			permissionService.On("GetPermissionByID", mock.Anything, mock.Anything).Return(&settingssvc.GetPermissionByIDResponse{
+				Permission: &v0.Permission{
+					Operation:  v0.Permission_OPERATION_READWRITE,
+					Constraint: v0.Permission_CONSTRAINT_ALL,
+				},
+			}, nil)
+			gatewayClient.On("CreateStorageSpace", mock.Anything, mock.Anything).Return(&provider.CreateStorageSpaceResponse{
+				Status: status.NewPermissionDenied(ctx, nil, "grpc permission denied"),
+			}, nil)
+
+			jsonBody := []byte(`{"Name": "Test Space"}`)
+			r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/drives", bytes.NewBuffer(jsonBody)).WithContext(ctx)
+			rr := httptest.NewRecorder()
+			svc.CreateDrive(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusForbidden))
+
+			body, _ := io.ReadAll(rr.Body)
+			var libreError libregraph.OdataError
+			err := json.Unmarshal(body, &libreError)
+			Expect(err).To(Not(HaveOccurred()))
+			Expect(libreError.Error.Message).To(Equal("permission denied"))
+			Expect(libreError.Error.Code).To(Equal(errorcode.NotAllowed.String()))
+		})
+		It("grpc general error", func() {
+			permissionService.On("GetPermissionByID", mock.Anything, mock.Anything).Return(&settingssvc.GetPermissionByIDResponse{
+				Permission: &v0.Permission{
+					Operation:  v0.Permission_OPERATION_READWRITE,
+					Constraint: v0.Permission_CONSTRAINT_ALL,
+				},
+			}, nil)
+			gatewayClient.On("CreateStorageSpace", mock.Anything, mock.Anything).Return(&provider.CreateStorageSpaceResponse{
+				Status: status.NewInternal(ctx, "grpc error"),
+			}, nil)
+
+			jsonBody := []byte(`{"Name": "Test Space"}`)
+			r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/drives", bytes.NewBuffer(jsonBody)).WithContext(ctx)
+			rr := httptest.NewRecorder()
+			svc.CreateDrive(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusInternalServerError))
+
+			body, _ := io.ReadAll(rr.Body)
+			var libreError libregraph.OdataError
+			err := json.Unmarshal(body, &libreError)
+			Expect(err).To(Not(HaveOccurred()))
+			Expect(libreError.Error.Message).To(Equal("grpc error"))
+			Expect(libreError.Error.Code).To(Equal(errorcode.GeneralException.String()))
+		})
+		It("cannot create a space with empty name", func() {
+			permissionService.On("GetPermissionByID", mock.Anything, mock.Anything).Return(&settingssvc.GetPermissionByIDResponse{
+				Permission: &v0.Permission{
+					Operation:  v0.Permission_OPERATION_READWRITE,
+					Constraint: v0.Permission_CONSTRAINT_ALL,
+				},
+			}, nil)
+			jsonBody := []byte(`{"Name": ""}`)
+			r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/drives", bytes.NewBuffer(jsonBody)).WithContext(ctx)
+			rr := httptest.NewRecorder()
+			svc.CreateDrive(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+
+			body, _ := io.ReadAll(rr.Body)
+			var libreError libregraph.OdataError
+			err := json.Unmarshal(body, &libreError)
+			Expect(err).To(Not(HaveOccurred()))
+			Expect(libreError.Error.Message).To(Equal("invalid spacename: spacename must not be empty"))
+			Expect(libreError.Error.Code).To(Equal(errorcode.InvalidRequest.String()))
+		})
+		It("cannot create a space with a name that exceeds 255 chars", func() {
+			permissionService.On("GetPermissionByID", mock.Anything, mock.Anything).Return(&settingssvc.GetPermissionByIDResponse{
+				Permission: &v0.Permission{
+					Operation:  v0.Permission_OPERATION_READWRITE,
+					Constraint: v0.Permission_CONSTRAINT_ALL,
+				},
+			}, nil)
+			jsonBody := []byte(`{"Name": "uufZ2MEUjUMJa84RkPsjJ1zf4XXRTdVMxRsJGfevwHuUBojo5JEdNU22O1FGgzXXTi9tl5ZKWaluIef8pPmEAxn9lHGIjyDVYeRQPiX5PCAZ7rVszrpLJryY5x1p6fFGQ6WQsPpNaqnKnfMliJDsbkAwMf7rCpzo0GUuadgHY9s2mfoXHDnpxqEmDsheucqVAFcNlFZNbNHoZAebHfv78KYc8C0WnhWvqvSPGBkNPQbZUkFCOAIlqpQ2Q3MubgI2"}`)
+			r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/drives", bytes.NewBuffer(jsonBody)).WithContext(ctx)
+			rr := httptest.NewRecorder()
+			svc.CreateDrive(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+
+			body, _ := io.ReadAll(rr.Body)
+			var libreError libregraph.OdataError
+			err := json.Unmarshal(body, &libreError)
+			Expect(err).To(Not(HaveOccurred()))
+			Expect(libreError.Error.Message).To(Equal("invalid spacename: spacename must be smaller than 255"))
+			Expect(libreError.Error.Code).To(Equal(errorcode.InvalidRequest.String()))
+		})
+		It("cannot create a space with a wrong type", func() {
+			permissionService.On("GetPermissionByID", mock.Anything, mock.Anything).Return(&settingssvc.GetPermissionByIDResponse{
+				Permission: &v0.Permission{
+					Operation:  v0.Permission_OPERATION_READWRITE,
+					Constraint: v0.Permission_CONSTRAINT_ALL,
+				},
+			}, nil)
+			jsonBody := []byte(`{"Name": "Test", "DriveType": "media"}`)
+			r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/drives", bytes.NewBuffer(jsonBody)).WithContext(ctx)
+			rr := httptest.NewRecorder()
+			svc.CreateDrive(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+
+			body, _ := io.ReadAll(rr.Body)
+			var libreError libregraph.OdataError
+			err := json.Unmarshal(body, &libreError)
+			Expect(err).To(Not(HaveOccurred()))
+			Expect(libreError.Error.Message).To(Equal("drives of this type cannot be created via this api"))
 			Expect(libreError.Error.Code).To(Equal(errorcode.InvalidRequest.String()))
 		})
+		It("cannot create a space with a name that contains invalid chars", func() {
+			permissionService.On("GetPermissionByID", mock.Anything, mock.Anything).Return(&settingssvc.GetPermissionByIDResponse{
+				Permission: &v0.Permission{
+					Operation:  v0.Permission_OPERATION_READWRITE,
+					Constraint: v0.Permission_CONSTRAINT_ALL,
+				},
+			}, nil)
+			jsonBody := []byte(`{"Name": "Space / Name"}`)
+			r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/drives", bytes.NewBuffer(jsonBody)).WithContext(ctx)
+			rr := httptest.NewRecorder()
+			svc.CreateDrive(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+
+			body, _ := io.ReadAll(rr.Body)
+			var libreError libregraph.OdataError
+			err := json.Unmarshal(body, &libreError)
+			Expect(err).To(Not(HaveOccurred()))
+			Expect(libreError.Error.Message).To(Equal("invalid spacename: spacenames must not contain [/ \\ . : ? * \" > < |]"))
+			Expect(libreError.Error.Code).To(Equal(errorcode.InvalidRequest.String()))
+		})
+		It("can create a project space", func() {
+			gatewayClient.On("CreateStorageSpace", mock.Anything, mock.Anything).Return(&provider.CreateStorageSpaceResponse{
+				Status: status.NewOK(ctx),
+				StorageSpace: &provider.StorageSpace{
+					Id:        &provider.StorageSpaceId{OpaqueId: "newID"},
+					Name:      "Test Space",
+					SpaceType: "project",
+					Root: &provider.ResourceId{
+						StorageId: "pro-1",
+						SpaceId:   "newID",
+						OpaqueId:  "newID",
+					},
+					Opaque: &typesv1beta1.Opaque{
+						Map: map[string]*typesv1beta1.OpaqueEntry{
+							"description": {Decoder: "plain", Value: []byte("This space is for testing")},
+							"spaceAlias":  {Decoder: "plain", Value: []byte("project/testspace")},
+						},
+					},
+				},
+			}, nil)
+
+			permissionService.On("GetPermissionByID", mock.Anything, mock.Anything).Return(&settingssvc.GetPermissionByIDResponse{
+				Permission: &v0.Permission{
+					Operation:  v0.Permission_OPERATION_READWRITE,
+					Constraint: v0.Permission_CONSTRAINT_ALL,
+				},
+			}, nil)
+			jsonBody := []byte(`{"Name": "Test Space", "DriveType": "project", "Description": "This space is for testing", "DriveAlias": "project/testspace"}`)
+			r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/drives", bytes.NewBuffer(jsonBody)).WithContext(ctx)
+			rr := httptest.NewRecorder()
+			svc.CreateDrive(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusCreated))
+
+			body, _ := io.ReadAll(rr.Body)
+			var response libregraph.Drive
+			err := json.Unmarshal(body, &response)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(*response.Name).To(Equal("Test Space"))
+			Expect(*response.DriveType).To(Equal("project"))
+			Expect(*response.DriveAlias).To(Equal("project/testspace"))
+			Expect(*response.Description).To(Equal("This space is for testing"))
+		})
+		It("Incomplete space", func() {
+			gatewayClient.On("CreateStorageSpace", mock.Anything, mock.Anything).Return(&provider.CreateStorageSpaceResponse{
+				Status: status.NewOK(ctx),
+				StorageSpace: &provider.StorageSpace{
+					Id:        &provider.StorageSpaceId{OpaqueId: "newID"},
+					Name:      "Test Space",
+					SpaceType: "project",
+				},
+			}, nil)
+
+			permissionService.On("GetPermissionByID", mock.Anything, mock.Anything).Return(&settingssvc.GetPermissionByIDResponse{
+				Permission: &v0.Permission{
+					Operation:  v0.Permission_OPERATION_READWRITE,
+					Constraint: v0.Permission_CONSTRAINT_ALL,
+				},
+			}, nil)
+			jsonBody := []byte(`{"Name": "Test Space"}`)
+			r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/drives", bytes.NewBuffer(jsonBody)).WithContext(ctx)
+			rr := httptest.NewRecorder()
+			svc.CreateDrive(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusInternalServerError))
+
+			body, _ := io.ReadAll(rr.Body)
+			var libreError libregraph.OdataError
+			err := json.Unmarshal(body, &libreError)
+			Expect(err).To(Not(HaveOccurred()))
+			Expect(libreError.Error.Message).To(Equal("space has no root"))
+			Expect(libreError.Error.Code).To(Equal(errorcode.GeneralException.String()))
+		})
 	})
 })
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/services/settings/pkg/service/v0/service_test.go b/services/settings/pkg/service/v0/service_test.go
index d7536648086..86b4ec479f7 100644
--- a/services/settings/pkg/service/v0/service_test.go
+++ b/services/settings/pkg/service/v0/service_test.go
@@ -8,6 +8,7 @@ import (
 	settingsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0"
 	v0 "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
 	"github.com/owncloud/ocis/v2/services/settings/pkg/settings/mocks"
+	"github.com/owncloud/ocis/v2/services/settings/pkg/store/defaults"
 	"github.com/stretchr/testify/assert"
 	"github.com/test-go/testify/mock"
 	"go-micro.dev/v4/metadata"
@@ -66,7 +67,35 @@ func TestGetValidatedAccountUUID(t *testing.T) {
 
 func TestEditOwnRoleAssignment(t *testing.T) {
 	manager := &mocks.Manager{}
-	a := []*settingsmsg.UserRoleAssignment{
+	svc := Service{
+		manager: manager,
+	}
+	a := []*settingsmsg.UserRoleAssignment{}
+	manager.On("ListRoleAssignments", mock.Anything).Return(a, nil)
+	manager.On("WriteRoleAssignment", mock.Anything, mock.Anything).Return(nil, nil)
+	// Creating an initial self assignment is expected to succeed for UserRole when no assignment exists yet
+	req := v0.AssignRoleToUserRequest{
+		AccountUuid: "61445573-4dbe-4d56-88dc-88ab47aceba7",
+		RoleId:      defaults.BundleUUIDRoleUser,
+	}
+	res := v0.AssignRoleToUserResponse{}
+	err := svc.AssignRoleToUser(ctxWithUUID, &req, &res)
+	assert.Nil(t, err)
+
+	// Creating an initial self assignment is expected to fail for non UserRole when no assignment exists yet
+	req = v0.AssignRoleToUserRequest{
+		AccountUuid: "61445573-4dbe-4d56-88dc-88ab47aceba7",
+		RoleId:      defaults.BundleUUIDRoleAdmin,
+	}
+	res = v0.AssignRoleToUserResponse{}
+	err = svc.AssignRoleToUser(ctxWithUUID, &req, &res)
+	assert.NotNil(t, err)
+
+	manager = &mocks.Manager{}
+	svc = Service{
+		manager: manager,
+	}
+	a = []*settingsmsg.UserRoleAssignment{
 		{
 			Id:          "00000000-0000-0000-0000-000000000001",
 			AccountUuid: "61445573-4dbe-4d56-88dc-88ab47aceba7",
@@ -79,21 +108,18 @@ func TestEditOwnRoleAssignment(t *testing.T) {
 	}
 	manager.On("ListRoleAssignments", mock.Anything).Return(a, nil)
 	manager.On("ReadPermissionByID", mock.Anything, mock.Anything).Return(editRolePermission, nil)
-	svc := Service{
-		manager: manager,
-	}
 
-	// Creating an self assignment is expect to fail
-	req := v0.AssignRoleToUserRequest{
+	// Creating an self assignment is expect to fail if there is already an assingment
+	req = v0.AssignRoleToUserRequest{
 		AccountUuid: "61445573-4dbe-4d56-88dc-88ab47aceba7",
-		RoleId:      "aceb15b8-7486-479f-ae32-c91118e07a39",
+		RoleId:      defaults.BundleUUIDRoleUser,
 	}
-	res := v0.AssignRoleToUserResponse{}
-	err := svc.AssignRoleToUser(ctxWithUUID, &req, &res)
+	res = v0.AssignRoleToUserResponse{}
+	err = svc.AssignRoleToUser(ctxWithUUID, &req, &res)
 	assert.NotNil(t, err)
 
 	manager.On("WriteRoleAssignment", mock.Anything, mock.Anything).Return(nil, nil)
-	// Creating an self assignment is expect to fail
+	// Creating an assignment for somebody else is expected to succeed, give the right permissions
 	req = v0.AssignRoleToUserRequest{
 		AccountUuid: "00000000-0000-0000-0000-000000000000",
 		RoleId:      "aceb15b8-7486-479f-ae32-c91118e07a39",
diff --git a/tests/acceptance/config/behat.yml b/tests/acceptance/config/behat.yml
index 488b5d87d73..01c570a28bd 100644
--- a/tests/acceptance/config/behat.yml
+++ b/tests/acceptance/config/behat.yml
@@ -13,6 +13,7 @@ default:
           ldapGroupsOU: TestGroups
           ldapInitialUserFilePath: /../../config/ldap-users.ldif
       contexts:
+        - RoleAssignmentContext:
         - GraphContext:
         - SpacesContext:
         - OccContext:
@@ -36,6 +37,7 @@ default:
         - '%paths.base%/../features/apiSpaces'
       context: *common_ldap_suite_context
       contexts:
+        - RoleAssignmentContext:
         - SpacesContext:
         - OccContext:
         - FeatureContext: *common_feature_context_params
@@ -56,6 +58,7 @@ default:
         - '%paths.base%/../features/apiSpacesShares'
       context: *common_ldap_suite_context
       contexts:
+        - RoleAssignmentContext:
         - SpacesContext:
         - OccContext:
         - FeatureContext: *common_feature_context_params
@@ -76,6 +79,7 @@ default:
         - '%paths.base%/../features/apiContract'
       context: *common_ldap_suite_context
       contexts:
+        - RoleAssignmentContext:
         - SpacesContext:
         - OccContext:
         - FeatureContext: *common_feature_context_params
@@ -96,6 +100,7 @@ default:
         - '%paths.base%/../features/apiArchiver'
       context: *common_ldap_suite_context
       contexts:
+        - RoleAssignmentContext:
         - ArchiverContext:
         - SpacesContext:
         - OccContext:
@@ -114,6 +119,7 @@ default:
         - '%paths.base%/../features/apiGraph'
       context: *common_ldap_suite_context
       contexts:
+        - RoleAssignmentContext:
         - GraphContext:
         - SpacesContext:
         - OccContext:
diff --git a/tests/acceptance/expected-failures-API-on-OCIS-storage.md b/tests/acceptance/expected-failures-API-on-OCIS-storage.md
index 2f795baab7b..c1f19b9a964 100644
--- a/tests/acceptance/expected-failures-API-on-OCIS-storage.md
+++ b/tests/acceptance/expected-failures-API-on-OCIS-storage.md
@@ -268,7 +268,7 @@ cannot share a folder with create permission
 - [apiSharePublicLink3/updatePublicLinkShare.feature:45](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiSharePublicLink3/updatePublicLinkShare.feature#L45)
 - [apiSharePublicLink3/updatePublicLinkShare.feature:46](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiSharePublicLink3/updatePublicLinkShare.feature#L46)
 
-#### [Upload-only shares must not overwrite but create a separate file](https://github.com/owncloud/ocis-reva/issues/286)
+#### [Upload-only shares must not overwrite but create a separate file](https://github.com/owncloud/ocis/issues/1267)
 
 - [apiSharePublicLink3/uploadToPublicLinkShare.feature:24](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiSharePublicLink3/uploadToPublicLinkShare.feature#L24)
 - [apiSharePublicLink3/uploadToPublicLinkShare.feature:273](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiSharePublicLink3/uploadToPublicLinkShare.feature#L273)
diff --git a/tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md b/tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md
index 9980be49013..79d3d1ec943 100644
--- a/tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md
+++ b/tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md
@@ -44,3 +44,19 @@ The expected failures in this file are from features in the owncloud/ocis repo.
 
 ### [Creating group with empty name returns status code 200](https://github.com/owncloud/ocis/issues/5050)
 - [apiGraph/createGroup.feature:40](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiGraph/createGroup.feature#L40)
+
+### [Settings service user can list other peoples assignments](https://github.com/owncloud/ocis/issues/5032)
+- [apiAccountsHashDifficulty/assignRole.feature:27](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiAccountsHashDifficulty/assignRole.feature#L27)
+- [apiAccountsHashDifficulty/assignRole.feature:28](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiAccountsHashDifficulty/assignRole.feature#L28)
+
+### [Settings service user can see roles list](https://github.com/owncloud/ocis/issues/5079)
+- [apiAccountsHashDifficulty/assignRole.feature:15](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiAccountsHashDifficulty/assignRole.feature#L15)
+- [apiAccountsHashDifficulty/assignRole.feature:16](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiAccountsHashDifficulty/assignRole.feature#L16)
+
+### [Group having percentage (%) can be created but cannot be GET](https://github.com/owncloud/ocis/issues/5083)
+- [apiGraph/deleteGroup.feature:49](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiGraph/deleteGroup.feature#L49)
+- [apiGraph/deleteGroup.feature:50](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiGraph/deleteGroup.feature#L50)
+- [apiGraph/deleteGroup.feature:51](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiGraph/deleteGroup.feature#L51)
+
+#### [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)
diff --git a/tests/acceptance/features/apiAccountsHashDifficulty/assignRole.feature b/tests/acceptance/features/apiAccountsHashDifficulty/assignRole.feature
new file mode 100644
index 00000000000..4eaefb98276
--- /dev/null
+++ b/tests/acceptance/features/apiAccountsHashDifficulty/assignRole.feature
@@ -0,0 +1,64 @@
+@api
+Feature: assign role
+  As an admin, I want to assign roles to users.
+  I cannot change my own role.
+  Users without an admin role cannot get the list of roles, assignments list and assign roles to users
+
+  Scenario Outline: only admin user can see all existing roles
+    Given user "Alice" has been created with default attributes and without skeleton files
+    And the administrator has given "Alice" the role "<userRole>" using the settings api
+    When user "Alice" tries to get all existing roles
+    Then the HTTP status code should be "<statusCode>"
+    Examples:
+      | userRole    | statusCode |
+      | Admin       | 201        |
+      | Space Admin | 401        |
+      | User        | 401        |
+
+
+  Scenario Outline: only admin user can see assignments list
+    Given user "Alice" has been created with default attributes and without skeleton files
+    And the administrator has given "Alice" the role "<userRole>" using the settings api
+    When user "Alice" tries to get list of assignment
+    Then the HTTP status code should be "<statusCode>"
+    Examples:
+      | userRole    | statusCode |
+      | Admin       | 201        |
+      | Space Admin | 401        |
+      | User        | 401        |
+
+
+  Scenario Outline: a user cannot change own role
+    Given user "Alice" has been created with default attributes and without skeleton files
+    And the administrator has given "Alice" the role "<userRole>" using the settings api
+    When user "Alice" changes his own role to "<desiredRole>"
+    Then the HTTP status code should be "400"
+    And user "Alice" should have the role "<userRole>"
+    Examples:
+      | userRole    | desiredRole |
+      | Admin       | User        |
+      | Admin       | Space Admin |
+      | Space Admin | Admin       |
+      | Space Admin | Space Admin |
+      | User        | Admin       |
+      | User        | Space Admin |
+
+
+  Scenario Outline: only admin user can change the role for another user
+    Given these users have been created with default attributes and without skeleton files:
+      | username |
+      | Alice    |
+      | Brian    |
+    And the administrator has given "Alice" the role "<userRole>" using the settings api
+    When user "Alice" changes the role "<desiredRole>" for user "Brian"
+    Then the HTTP status code should be "<statusCode>"
+    And user "Brian" should have the role "<expectedRole>"
+    Examples:
+      | userRole    | desiredRole | statusCode | expectedRole |
+      | Admin       | User        | 201        | User         |
+      | Admin       | Space Admin | 201        | Space Admin  |
+      | Admin       | Admin       | 201        | Admin        |
+      | Space Admin | Admin       | 400        | User         |
+      | Space Admin | Space Admin | 400        | User         |
+      | User        | Admin       | 400        | User         |
+      | User        | Space Admin | 400        | User         |
diff --git a/tests/acceptance/features/apiGraph/addUserToGroup.feature b/tests/acceptance/features/apiGraph/addUserToGroup.feature
new file mode 100644
index 00000000000..ea91d1730a3
--- /dev/null
+++ b/tests/acceptance/features/apiGraph/addUserToGroup.feature
@@ -0,0 +1,151 @@
+@api @skipOnOcV10
+Feature: add users to group
+  As a admin
+  I want to be able to add users to a group
+  So that I can give a user access to the resources of the group
+
+  Background:
+    Given user "Alice" has been created with default attributes and without skeleton files
+
+
+  Scenario: adding a user to a group
+    Given these groups have been created:
+      | groupname   | comment                               |
+      | simplegroup | nothing special here                  |
+      | España§àôœ€ | special European and other characters |
+      | नेपाली        | Unicode group name                    |
+    When the administrator adds the following users to the following groups using the Graph API
+      | username | groupname   |
+      | Alice    | simplegroup |
+      | Alice    | España§àôœ€ |
+      | Alice    | नेपाली        |
+    Then the HTTP status code of responses on all endpoints should be "204"
+    And the following users should be listed in the following groups
+      | username | groupname   |
+      | Alice    | simplegroup |
+      | Alice    | España§àôœ€ |
+      | Alice    | नेपाली        |
+
+
+  Scenario: adding a user to a group with special character in its name
+    Given these groups have been created:
+      | groupname           | comment            |
+      | brand-new-group     | dash               |
+      | the.group           | dot                |
+      | left,right          | comma              |
+      | 0                   | The "false" group  |
+      | Finance (NP)        | Space and brackets |
+      | Admin&Finance       | Ampersand          |
+      | maint+eng           | Plus sign          |
+      | $x<=>[y*z^2]!       | Maths symbols      |
+      | 😁 😂               | emoji              |
+      | admin:Pokhara@Nepal | Colon and @        |
+    When the administrator adds the following users to the following groups using the Graph API
+      | username | groupname           |
+      | Alice    | brand-new-group     |
+      | Alice    | the.group           |
+      | Alice    | left,right          |
+      | Alice    | 0                   |
+      | Alice    | Finance (NP)        |
+      | Alice    | Admin&Finance       |
+      | Alice    | maint+eng           |
+      | Alice    | $x<=>[y*z^2]!       |
+      | Alice    | 😁 😂               |
+      | Alice    | admin:Pokhara@Nepal |
+    Then the HTTP status code of responses on all endpoints should be "204"
+    And the following users should be listed in the following groups
+      | username | groupname           |
+      | Alice    | brand-new-group     |
+      | Alice    | the.group           |
+      | Alice    | left,right          |
+      | Alice    | 0                   |
+      | Alice    | Finance (NP)        |
+      | Alice    | Admin&Finance       |
+      | Alice    | maint+eng           |
+      | Alice    | $x<=>[y*z^2]!       |
+      | Alice    | 😁 😂               |
+      | Alice    | admin:Pokhara@Nepal |
+
+
+  Scenario: adding a user to a group with % and # in its name
+    Given these groups have been created:
+      | groupname           | comment                                 |
+      | maintenance#123     | Hash sign                               |
+      | 50%pass             | Percent sign (special escaping happens) |
+      | 50%25=0             | %25 literal looks like an escaped "%"   |
+      | 50%2Eagle           | %2E literal looks like an escaped "."   |
+      | 50%2Fix             | %2F literal looks like an escaped slash |
+      | Mgmt\Middle         | Backslash                               |
+      | staff?group         | Question mark                           |
+    When the administrator adds the following users to the following groups using the Graph API
+      | username | groupname       |
+      | Alice    | maintenance#123 |
+      | Alice    | 50%pass         |
+      | Alice    | 50%25=0         |
+      | Alice    | 50%2Eagle       |
+      | Alice    | 50%2Fix         |
+      | Alice    | Mgmt\Middle     |
+      | Alice    | staff?group     |
+    Then the HTTP status code of responses on all endpoints should be "204"
+    And the following users should be listed in the following groups
+      | username | groupname       |
+      | Alice    | maintenance#123 |
+      | Alice    | 50%pass         |
+      | Alice    | 50%25=0         |
+      | Alice    | 50%2Eagle       |
+      | Alice    | 50%2Fix         |
+      | Alice    | Mgmt\Middle     |
+      | Alice    | staff?group     |
+
+
+  Scenario: adding a user to a group that has a forward-slash in the group name
+    Given these groups have been created:
+      | groupname        | comment                            |
+      | Mgmt/Sydney      | Slash (special escaping happens)   |
+      | Mgmt//NSW/Sydney | Multiple slash                     |
+      | priv/subadmins/1 | Subadmins mentioned not at the end |
+      | var/../etc       | using slash-dot-dot                |
+    When the administrator adds the following users to the following groups using the Graph API
+      | username | groupname        |
+      | Alice    | Mgmt/Sydney      |
+      | Alice    | Mgmt//NSW/Sydney |
+      | Alice    | priv/subadmins/1 |
+      | Alice    | var/../etc       |
+    Then the HTTP status code of responses on all endpoints should be "204"
+    And the following users should be listed in the following groups
+      | username | groupname        |
+      | Alice    | Mgmt/Sydney      |
+      | Alice    | Mgmt//NSW/Sydney |
+      | Alice    | priv/subadmins/1 |
+      | Alice    | var/../etc       |
+
+
+  Scenario: normal user tries to add himself to a group
+    Given group "groupA" has been created
+    When user "Alice" tries to add himself to group "groupA" using the Graph API
+    Then the HTTP status code should be "401"
+    And the last response should be an unauthorized response
+
+
+  Scenario: normal user tries to other user to a group
+    Given user "Brian" has been created with default attributes and without skeleton files
+    And group "groupA" has been created
+    When user "Alice" tries to add user "Brian" to group "groupA" using the Graph API
+    Then the HTTP status code should be "401"
+    And the last response should be an unauthorized response
+
+
+  Scenario: admin tries to add user to a non-existing group
+    When the administrator tries to add user "Alice" to group "nonexistentgroup" using the Graph API
+    Then the HTTP status code should be "404"
+
+
+  Scenario: admin tries to add a non-existing user to a group
+    Given group "groupA" has been created
+    When the administrator tries to add user "nonexistentuser" to group "groupA" using the provisioning API
+    Then the HTTP status code should be "405"
+
+
+  Scenario: admin tries to add user to a group without sending the group
+    When the administrator tries to add user "Alice" to group "" using the Graph API
+    Then the HTTP status code should be "404"
diff --git a/tests/acceptance/features/apiGraph/deleteGroup.feature b/tests/acceptance/features/apiGraph/deleteGroup.feature
new file mode 100644
index 00000000000..0fb377347ca
--- /dev/null
+++ b/tests/acceptance/features/apiGraph/deleteGroup.feature
@@ -0,0 +1,84 @@
+@api @skipOnOcV10
+Feature: delete groups
+  As an admin
+  I want to be able to delete groups
+  So that I can remove unnecessary groups
+
+  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
+
+
+  Scenario Outline: admin user deletes a group
+    Given group "<group_id>" has been created
+    When user "Alice" deletes group "<group_id>" using the Graph API
+    Then the HTTP status code should be "204"
+    And group "<group_id>" should not exist
+    Examples:
+      | group_id            | comment                               |
+      | simplegroup         | nothing special here                  |
+      | España§àôœ€         | special European and other characters |
+      | नेपाली                | Unicode group name                    |
+      | brand-new-group     | dash                                  |
+      | the.group           | dot                                   |
+      | left,right          | comma                                 |
+      | 0                   | The "false" group                     |
+      | Finance (NP)        | Space and brackets                    |
+      | Admin&Finance       | Ampersand                             |
+      | admin:Pokhara@Nepal | Colon and @                           |
+      | maint+eng           | Plus sign                             |
+      | $x<=>[y*z^2]!       | Maths symbols                         |
+      | Mgmt\Middle         | Backslash                             |
+      | 😁 😂               | emoji                                 |
+      | maintenance#123     | Hash sign                             |
+      | 50%25=0             | %25 literal looks like an escaped "%" |
+      | staff?group         | Question mark                         |
+      | Mgmt/Sydney         | Slash (special escaping happens)      |
+      | Mgmt//NSW/Sydney    | Multiple slash                        |
+      | priv/subadmins/1    | Subadmins mentioned not at the end    |
+      | var/../etc          | using slash-dot-dot                   |
+
+  @issue-5083
+  Scenario Outline: admin user deletes a group having % (as only special char) in its name
+    Given group "<group_id>" has been created
+    When user "Alice" deletes group "<group_id>" using the Graph API
+    Then the HTTP status code should be "204"
+    And group "<group_id>" should not exist
+    Examples:
+      | group_id            | comment                                 |
+      | 50%pass             | Percent sign (special escaping happens) |
+      | 50%2Eagle           | %2E literal looks like an escaped "."   |
+      | 50%2Fix             | %2F literal looks like an escaped slash |
+
+
+  Scenario: normal user tries to delete a group
+    Given user "Brian" has been created with default attributes and without skeleton files
+    And group "new-group" has been created
+    When user "Brian" tries to delete group "new-group" using the Graph API
+    Then the HTTP status code should be "401"
+    And group "new-group" should exist
+
+  @issue-903
+  Scenario: deleted group should not be listed in the sharees list
+    Given group "grp1" has been created
+    And group "grp2" has been created
+    And user "Alice" has uploaded file with content "sample text" to "lorem.txt"
+    And user "Alice" has shared file "lorem.txt" with group "grp1"
+    And user "Alice" has shared file "lorem.txt" with group "grp2"
+    And group "grp1" has been deleted
+    When user "Alice" gets all the shares from the file "lorem.txt" using the sharing API
+    Then the HTTP status code should be "200"
+    And group "grp2" should be included in the response
+    But group "grp1" should not be included in the response
+
+
+  Scenario: user should not see share received via deleted group
+    Given user "Alice" has uploaded file with content "sample text" to "lorem.txt"
+    And user "Brian" has been created with default attributes and without skeleton files
+    And group "grp1" has been created
+    And user "Brian" has been added to group "grp1"
+    And user "Alice" has shared file "lorem.txt" with group "grp1"
+    And user "Brian" has accepted share "/lorem.txt" offered by user "Alice"
+    When user "Alice" deletes group "grp1" using the Graph API
+    Then the HTTP status code should be "204"
+    And user "Brian" should not have any received shares
diff --git a/tests/acceptance/features/apiSpacesShares/shareSpaces.feature b/tests/acceptance/features/apiSpacesShares/shareSpaces.feature
index 83282e70f62..e81cf71fa31 100644
--- a/tests/acceptance/features/apiSpacesShares/shareSpaces.feature
+++ b/tests/acceptance/features/apiSpacesShares/shareSpaces.feature
@@ -136,3 +136,34 @@ Feature: Share spaces
       | viewer | manager  |
       | viewer | editor   |
       | viewer | viewer   |
+
+
+  Scenario Outline: space manager can change the role of space members
+    Given user "Alice" has shared a space "share space" to user "Brian" with role "<role>"
+    When user "Alice" updates the space "share space" for user "Brian" changing the role to "<new_role>"
+    Then the HTTP status code should be "200"
+    And the OCS status code should be "200"
+    And the user "Alice" should have a space called "share space" granted to "Brian" with role "<new_role>"
+    Examples:
+      | role    | new_role |
+      | editor  | manager  |
+      | editor  | viewer   |
+      | viewer  | manager  |
+      | viewer  | editor   |
+      | manager | editor   |
+      | manager | viewer   |
+
+
+  Scenario Outline: user without manager role cannot change the role of space members
+    Given user "Alice" has shared a space "share space" to user "Brian" with role "<role>"
+    And user "Alice" has shared a space "share space" to user "Bob" with role "viewer"
+    When user "Brian" updates the space "share space" for user "Bob" changing the role to "<new_role>"
+    Then the HTTP status code should be "404"
+    And the OCS status code should be "404"
+    And the user "Alice" should have a space called "share space" granted to "Bob" with role "viewer"
+    Examples:
+      | role   | new_role |
+      | editor | manager  |
+      | editor | viewer   |
+      | viewer | manager  |
+      | viewer | editor   |
diff --git a/tests/acceptance/features/apiSpacesShares/shareSubItemOfSpaceViaPublicLink.feature b/tests/acceptance/features/apiSpacesShares/shareSubItemOfSpaceViaPublicLink.feature
index ca027bcf380..42fc6734ae6 100644
--- a/tests/acceptance/features/apiSpacesShares/shareSubItemOfSpaceViaPublicLink.feature
+++ b/tests/acceptance/features/apiSpacesShares/shareSubItemOfSpaceViaPublicLink.feature
@@ -5,15 +5,17 @@ Feature: Share a file or folder that is inside a space via public link
 
       folder permissions:
       | role        | permissions |
+      | internal    | 0           |
       | viewer      | 1           |
       | uploader    | 4           |
       | contributor | 5           |
       | editor      | 15          |
 
       file permissions:
-      | role   | permissions |
-      | viewer | 1           |
-      | editor | 3           |
+      | role     | permissions |
+      | internal | 0           |
+      | viewer   | 1           |
+      | editor   | 3           |
 
   Note - this feature is run in CI with ACCOUNTS_HASH_DIFFICULTY set to the default for production
   See https://github.com/owncloud/ocis/issues/1542 and https://github.com/owncloud/ocis/pull/839
@@ -45,10 +47,12 @@ Feature: Share a file or folder that is inside a space via public link
       | path | <path> |
     Examples:
       | entity          | path     | permissions | password | name | expireDate               |
+      | folder          | /folder   |0           |          | link |                          |
       | folder          | /folder   |1           | 123      | link | 2042-03-25T23:59:59+0100 |
       | folder          | /folder   |4           |          |      |                          |
       | folder          | /folder   |5           | 200      |      | 2042-03-25T23:59:59+0100 |
       | folder          | /folder   |15          |          | link |                          |
+      | folder/file.txt | /file.txt |0           | 123      | link | 2042-03-25T23:59:59+0100 |
       | folder/file.txt | /file.txt |1           | 123      | link | 2042-03-25T23:59:59+0100 |
       | folder/file.txt | /file.txt |3           | 123      | link | 2042-03-25T23:59:59+0100 |
 
diff --git a/tests/acceptance/features/bootstrap/GraphContext.php b/tests/acceptance/features/bootstrap/GraphContext.php
index 35d6a0e199b..afb3aaa2ef1 100644
--- a/tests/acceptance/features/bootstrap/GraphContext.php
+++ b/tests/acceptance/features/bootstrap/GraphContext.php
@@ -139,6 +139,28 @@ class GraphContext implements Context {
 		$this->featureContext->thenTheHTTPStatusCodeShouldBe(200);
 	}
 
+	/**
+	 * @param string $groupId
+	 * @param string|null $user
+	 *
+	 * @return ResponseInterface
+	 * @throws GuzzleException
+	 */
+	public function userDeletesGroupWithGroupId(
+		string $groupId,
+		?string $user = null
+	): ResponseInterface {
+		$credentials = $this->getAdminOrUserCredentials($user);
+
+		return GraphHelper::deleteGroup(
+			$this->featureContext->getBaseUrl(),
+			$this->featureContext->getStepLineRef(),
+			$credentials["username"],
+			$credentials["password"],
+			$groupId
+		);
+	}
+
 	/**
 	 * @param string $groupId
 	 * @param bool $checkResult
@@ -151,13 +173,7 @@ class GraphContext implements Context {
 		bool $checkResult = false
 	): void {
 		$this->featureContext->setResponse(
-			GraphHelper::deleteGroup(
-				$this->featureContext->getBaseUrl(),
-				$this->featureContext->getStepLineRef(),
-				$this->featureContext->getAdminUsername(),
-				$this->featureContext->getAdminPassword(),
-				$groupId
-			)
+			$this->userDeletesGroupWithGroupId($groupId)
 		);
 		if ($checkResult) {
 			$this->featureContext->thenTheHTTPStatusCodeShouldBe(204);
@@ -538,6 +554,37 @@ class GraphContext implements Context {
 	/**
 	 * adds a user to a group
 	 *
+	 * @param string $group
+	 * @param string $user
+	 * @param string|null $byUser
+	 *
+	 * @return ResponseInterface
+	 * @throws GuzzleException
+	 */
+	public function addUserToGroup(string $group, string $user, ?string $byUser = null): ResponseInterface {
+		$credentials = $this->getAdminOrUserCredentials($byUser);
+		try {
+			$groupId = $this->featureContext->getAttributeOfCreatedGroup($group, "id");
+		} catch (Exception $e) {
+			$groupId = WebDavHelper::generateUUIDv4();
+		}
+		try {
+			$userId = $this->featureContext->getAttributeOfCreatedUser($user, "id");
+		} catch (Exception $e) {
+			$userId = WebDavHelper::generateUUIDv4();
+		}
+
+		return GraphHelper::addUserToGroup(
+			$this->featureContext->getBaseUrl(),
+			$this->featureContext->getStepLineRef(),
+			$credentials['username'],
+			$credentials['password'],
+			$userId,
+			$groupId
+		);
+	}
+
+	/**
 	 * @Given /^the administrator has added a user "([^"]*)" to the group "([^"]*)" using GraphApi$/
 	 *
 	 * @param string $user
@@ -553,21 +600,66 @@ class GraphContext implements Context {
 		string $group,
 		bool $checkResult = true
 	): void {
-		$groupId = $this->featureContext->getAttributeOfCreatedGroup($group, "id");
-		$userId = $this->featureContext->getAttributeOfCreatedUser($user, "id");
-		$result = GraphHelper::addUserToGroup(
-			$this->featureContext->getBaseUrl(),
-			$this->featureContext->getStepLineRef(),
-			$this->featureContext->getAdminUsername(),
-			$this->featureContext->getAdminPassword(),
-			$userId,
-			$groupId
-		);
+		$result = $this->addUserToGroup($group, $user);
 		if ($checkResult && ($result->getStatusCode() !== 204)) {
 			$this->throwHttpException($result, "Could not add user '$user' to group '$group'.");
 		}
 	}
 
+	/**
+	 * @When the administrator adds the following users to the following groups using the Graph API
+	 *
+	 * @param TableNode $table
+	 *
+	 * @return void
+	 */
+	public function theAdministratorAddsTheFollowingUsersToTheFollowingGroupsUsingTheGraphAPI(TableNode $table): void {
+		$this->featureContext->verifyTableNodeColumns($table, ['username', 'groupname']);
+		$userGroupList = $table->getColumnsHash();
+
+		foreach ($userGroupList as $userGroup) {
+			$this->featureContext->setResponse($this->addUserToGroup($userGroup['groupname'], $userGroup['username']));
+			$this->featureContext->pushToLastHttpStatusCodesArray();
+		}
+	}
+
+	/**
+	 * @When the administrator tries to add user :user to group :group using the Graph API
+	 *
+	 * @param string $user
+	 * @param string $group
+	 *
+	 * @return void
+	 */
+	public function theAdministratorTriesToAddUserToGroupUsingTheGraphAPI(string $user, string $group): void {
+		$this->featureContext->setResponse($this->addUserToGroup($group, $user));
+	}
+ 
+	/**
+	 * @When user :user tries to add himself/herself to group :group using the Graph API
+	 *
+	 * @param string $user
+	 * @param string $group
+	 *
+	 * @return void
+	 */
+	public function theUserTriesToAddHimselfToGroupUsingTheGraphAPI(string $user, string $group): void {
+		$this->featureContext->setResponse($this->addUserToGroup($group, $user, $user));
+	}
+
+	/**
+	 * @When user :byUser tries to add user :user to group :group using the Graph API
+	 *
+	 * @param string $byUser
+	 * @param string $user
+	 * @param string $group
+	 *
+	 * @return void
+	 */
+	public function theUserTriesToAddAnotherUserToGroupUsingTheGraphAPI(string $byUser, string $user, string $group): void {
+		$this->featureContext->setResponse($this->addUserToGroup($group, $byUser, $user));
+	}
+
 	/**
 	 *
 	 * @param string $group
@@ -751,4 +843,50 @@ class GraphContext implements Context {
 			. "\nExpected unauthorized message but got '" . $errorText . "'"
 		);
 	}
+
+	/**
+	 * @When user :user deletes group :group using the Graph API
+	 * @When the administrator deletes group :group using the Graph API
+	 * @When user :user tries to delete group :group using the Graph API
+	 *
+	 * @param string $group
+	 * @param string|null $user
+	 *
+	 * @return void
+	 */
+	public function userDeletesGroupUsingTheGraphApi(string $group, ?string $user): void {
+		$groupId = $this->featureContext->getAttributeOfCreatedGroup($group, "id");
+		$response = $this->userDeletesGroupWithGroupId($groupId, $user);
+		$this->featureContext->setResponse($response);
+	}
+  
+	/**
+	 * @Then the following users should be listed in the following groups
+	 *
+	 * @param TableNode $table
+	 *
+	 * @return void
+	 * @throws Exception
+	 */
+	public function theFollowingUsersShouldBeListedInFollowingGroups(TableNode $table): void {
+		$this->featureContext->verifyTableNodeColumns($table, ['username', 'groupname']);
+		$usersGroups = $table->getColumnsHash();
+		foreach ($usersGroups as $userGroup) {
+			$members = $this->listGroupMembers($userGroup['groupname']);
+			$members = $this->featureContext->getJsonDecodedResponse($members);
+
+			$exists = false;
+			foreach ($members as $member) {
+				if ($member['onPremisesSamAccountName'] === $userGroup['username']) {
+					$exists = true;
+					break;
+				}
+			}
+			Assert::assertTrue(
+				$exists,
+				__METHOD__
+				. "\nExpected user '" . $userGroup['username'] . "' to be in group '" . $userGroup['groupname'] . "'. But not found."
+			);
+		}
+	}
 }
diff --git a/tests/acceptance/features/bootstrap/RoleAssignmentContext.php b/tests/acceptance/features/bootstrap/RoleAssignmentContext.php
new file mode 100644
index 00000000000..b89791df4fd
--- /dev/null
+++ b/tests/acceptance/features/bootstrap/RoleAssignmentContext.php
@@ -0,0 +1,258 @@
+<?php
+declare(strict_types=1);
+
+/**
+ * ownCloud
+ *
+ * @author Viktor Scharf <v.scharf@owncloud.com>
+ * @copyright Copyright (c) 2022 Viktor Scharf v.scharf@owncloud.com
+ */
+
+use Behat\Behat\Context\Context;
+use GuzzleHttp\Exception\GuzzleException;
+use Behat\Behat\Hook\Scope\BeforeScenarioScope;
+use PHPUnit\Framework\Assert;
+
+require_once 'bootstrap.php';
+
+/**
+ * Context for the TUS-specific steps using the Graph API
+ */
+class RoleAssignmentContext implements Context {
+
+	/**
+	 * @var FeatureContext
+	 */
+	private FeatureContext $featureContext;
+
+	/**
+	 * @var SpacesContext
+	 */
+	private SpacesContext $spacesContext;
+
+	/**
+	 * @var string
+	 */
+	private string $baseUrl;
+
+	/**
+	 * @var string
+	 */
+	private string $setttingsUrl = '/api/v0/settings/';
+
+	/**
+	 * This will run before EVERY scenario.
+	 * It will set the properties for this object.
+	 *
+	 * @BeforeScenario
+	 *
+	 * @param BeforeScenarioScope $scope
+	 *
+	 * @return void
+	 */
+	public function before(BeforeScenarioScope $scope): void {
+		// Get the environment
+		$environment = $scope->getEnvironment();
+		// Get all the contexts you need in this context from here
+		$this->featureContext = $environment->getContext('FeatureContext');
+		$this->spacesContext = $environment->getContext('SpacesContext');
+		$this->baseUrl = \trim($this->featureContext->getBaseUrl(), "/");
+	}
+
+	/**
+	 * @When /^user "([^"]*)" tries to get all existing roles$/
+	 *
+	 * @param string $user
+	 *
+	 * @return void
+	 *
+	 * @throws GuzzleException
+	 * @throws Exception
+	 */
+	public function getAllExistingRoles(string $user): void {
+		$fullUrl = $this->baseUrl . $this->setttingsUrl . "roles-list";
+		$this->featureContext->setResponse(
+			$this->spacesContext->sendPostRequestToUrl($fullUrl, $user, $this->featureContext->getPasswordForUser($user), "{}")
+		);
+	}
+
+	/**
+	 * @param string $user
+	 * @param string $userId
+	 * @param string $roleId
+	 *
+	 * @return void
+	 *
+	 * @throws GuzzleException
+	 * @throws Exception
+	 */
+	public function sendRequestToAssignRoleToUser(string $user, string $userId, string $roleId): void {
+		$fullUrl = $this->baseUrl . $this->setttingsUrl . "assignments-add";
+		$body = json_encode(["account_uuid" => $userId, "role_id" => $roleId], JSON_THROW_ON_ERROR);
+
+		$this->featureContext->setResponse(
+			$this->spacesContext->sendPostRequestToUrl($fullUrl, $user, $this->featureContext->getPasswordForUser($user), $body)
+		);
+	}
+
+	/**
+	 * @param string $user
+	 * @param string $userId
+	 *
+	 * @return void
+	 *
+	 * @throws GuzzleException
+	 * @throws Exception
+	 */
+	public function sendRequestAssignmentsList(string $user, string $userId): void {
+		$fullUrl = $this->baseUrl . $this->setttingsUrl . "assignments-list";
+		$body = json_encode(["account_uuid" => $userId], JSON_THROW_ON_ERROR);
+
+		$this->featureContext->setResponse(
+			$this->spacesContext->sendPostRequestToUrl($fullUrl, $user, $this->featureContext->getPasswordForUser($user), $body)
+		);
+	}
+
+	/**
+	 * @When /^the administrator has given "([^"]*)" the role "([^"]*)" using the settings api$/
+	 *
+	 * @param string $user
+	 * @param string $role
+	 *
+	 * @return void
+	 *
+	 * @throws GuzzleException
+	 * @throws Exception
+	 */
+	public function theAdministratorHasGivenUserTheRole(string $user, string $role): void {
+		$admin = $this->featureContext->getAdminUserName();
+		$roleId = $this->userGetRoleIdByRoleName($admin, $role);
+		$userId = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
+		$this->setRoleToUser($admin, $userId, $roleId);
+	}
+
+	/**
+	 * @param string $user
+	 * @param string $role
+	 *
+	 * @return string
+	 */
+	public function userGetRoleIdByRoleName($user, $role): string {
+		$this->getAllExistingRoles($user);
+
+		if ($this->featureContext->getResponse()) {
+			$rawBody =  $this->featureContext->getResponse()->getBody()->getContents();
+			$decodedBody = \json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
+			Assert::assertArrayHasKey(
+				'bundles',
+				$decodedBody,
+				__METHOD__ . " could not find bundles in body"
+			);
+			$bundles = $decodedBody["bundles"];
+		}
+
+		$roleToAssign = "";
+		foreach ($bundles as $value) {
+			// find the selected role
+			if ($value["displayName"] === $role) {
+				$roleToAssign = $value;
+				break;
+			}
+		}
+		Assert::assertNotEmpty($roleToAssign, "The selected role $role could not be found");
+		return $roleToAssign["id"];
+	}
+
+	/**
+	 * @param string $user
+	 * @param string $userId
+	 * @param string $roleId
+	 *
+	 * @return void
+	 * @throws Exception
+	 */
+	public function setRoleToUser($user, $userId, $roleId): void {
+		$this->sendRequestToAssignRoleToUser($user, $userId, $roleId);
+
+		if ($this->featureContext->getResponse()) {
+			$rawBody = $this->featureContext->getResponse()->getBody()->getContents();
+			$decodedBody = \json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
+			Assert::assertArrayHasKey(
+				'assignment',
+				$decodedBody,
+				__METHOD__ . " could not find assignment in body"
+			);
+			$assignment = $decodedBody["assignment"];
+		}
+
+		Assert::assertEquals($userId, $assignment["accountUuid"]);
+		Assert::assertEquals($roleId, $assignment["roleId"]);
+	}
+
+	/**
+	 * @When /^user "([^"]*)" changes his own role to "([^"]*)"$/
+	 *
+	 * @param string $user
+	 * @param string $role
+	 *
+	 * @return void
+	 * @throws GuzzleException
+	 */
+	public function userChangeOwnRole(string $user, string $role): void {
+		// we assume that the user knows uuid role.
+		$roleId = $this->userGetRoleIdByRoleName($this->featureContext->getAdminUserName(), $role);
+		$userId = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
+		$this->sendRequestToAssignRoleToUser($user, $userId, $roleId);
+	}
+
+	/**
+	 * @When /^user "([^"]*)" changes the role "([^"]*)" for user "([^"]*)"$/
+	 *
+	 * @param string $user
+	 * @param string $role
+	 * @param string $assignedUser
+	 *
+	 * @return void
+	 * @throws GuzzleException
+	 */
+	public function userChangeRoleAnotherUser(string $user, string $role, string $assignedUser): void {
+		// we assume that the user knows uuid role.
+		$roleId = $this->userGetRoleIdByRoleName($this->featureContext->getAdminUserName(), $role);
+		$userId = $this->featureContext->getAttributeOfCreatedUser($assignedUser, 'id');
+		$this->sendRequestToAssignRoleToUser($user, $userId, $roleId);
+	}
+
+	/**
+	 * @When /^user "([^"]*)" tries to get list of assignment$/
+	 *
+	 * @param string $user
+	 *
+	 * @return void
+	 *
+	 * @throws GuzzleException
+	 * @throws Exception
+	 */
+	public function userGetAssignmentsList(string $user): void {
+		$userId = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
+		$this->sendRequestAssignmentsList($user, $userId);
+	}
+
+	/**
+	 * @When /^user "([^"]*)" should have the role "([^"]*)"$/
+	 *
+	 * @param string $user
+	 * @param string $role
+	 *
+	 * @return void
+	 *
+	 * @throws GuzzleException
+	 * @throws Exception
+	 */
+	public function userShouldHaveRole(string $user, string $role): void {
+		$userId = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
+		$this->sendRequestAssignmentsList($this->featureContext->getAdminUserName(), $userId);
+		$rawBody =  $this->featureContext->getResponse()->getBody()->getContents();
+		$assignmentRoleId = \json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR)["assignments"][0]["roleId"];
+		Assert::assertEquals($this->userGetRoleIdByRoleName($this->featureContext->getAdminUserName(), $role), $assignmentRoleId, "user $user has no role $role");
+	}
+}
diff --git a/tests/acceptance/features/bootstrap/SpacesContext.php b/tests/acceptance/features/bootstrap/SpacesContext.php
index 9dfafcadb21..12110fa9949 100644
--- a/tests/acceptance/features/bootstrap/SpacesContext.php
+++ b/tests/acceptance/features/bootstrap/SpacesContext.php
@@ -687,76 +687,6 @@ class SpacesContext implements Context {
 		$this->setSpaceCreator($spaceName, $user);
 	}
 
-	/**
-	 * @When /^the administrator has given "([^"]*)" the role "([^"]*)" using the settings api$/
-	 *
-	 * @param string $user
-	 * @param string $role
-	 *
-	 * @return void
-	 *
-	 * @throws GuzzleException
-	 * @throws Exception
-	 */
-	public function theAdministratorGivesUserTheRole(string $user, string $role): void {
-		$admin = $this->featureContext->getAdminUsername();
-		$password = $this->featureContext->getAdminPassword();
-		$bundles = [];
-		$assignment = [];
-
-		// get the roles list first
-		$fullUrl = $this->baseUrl . "/api/v0/settings/roles-list";
-		$this->featureContext->setResponse($this->sendPostRequestToUrl($fullUrl, $admin, $password, "{}"));
-
-		if ($this->featureContext->getResponse()) {
-			$rawBody =  $this->featureContext->getResponse()->getBody()->getContents();
-			if (isset(\json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR)["bundles"])) {
-				$bundles = \json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR)["bundles"];
-			}
-		}
-		$roleToAssign = "";
-		foreach ($bundles as $value) {
-			// find the selected role
-			if ($value["displayName"] === $role) {
-				$roleToAssign = $value;
-			}
-		}
-		Assert::assertNotEmpty($roleToAssign, "The selected role $role could not be found");
-
-		$this->featureContext->setResponse(
-			GraphHelper::getUser(
-				$this->featureContext->getBaseUrl(),
-				$this->featureContext->getStepLineRef(),
-				$this->featureContext->getAdminUsername(),
-				$this->featureContext->getAdminPassword(),
-				$user
-			)
-		);
-		if ($this->featureContext->getResponse()) {
-			$rawBody = $this->featureContext->getResponse()->getBody()->getContents();
-			if (isset(\json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR)["id"])) {
-				$accountToChange = \json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
-			}
-		}
-
-		Assert::assertNotEmpty($accountToChange, "The selected account $user does not exist");
-
-		// set the new role
-		$fullUrl = $this->baseUrl . "/api/v0/settings/assignments-add";
-		$body = json_encode(["account_uuid" => $accountToChange["id"], "role_id" => $roleToAssign["id"]], JSON_THROW_ON_ERROR);
-
-		$this->featureContext->setResponse($this->sendPostRequestToUrl($fullUrl, $admin, $password, $body));
-		if ($this->featureContext->getResponse()) {
-			$rawBody = $this->featureContext->getResponse()->getBody()->getContents();
-			if (isset(\json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR)["assignment"])) {
-				$assignment = \json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR)["assignment"];
-			}
-		}
-
-		Assert::assertEquals($accountToChange["id"], $assignment["accountUuid"]);
-		Assert::assertEquals($roleToAssign["id"], $assignment["roleId"]);
-	}
-
 	/**
 	 * Remember the available Spaces
 	 *
@@ -1752,8 +1682,7 @@ class SpacesContext implements Context {
 		string $fileName,
 		string $spaceName
 	):void {
-		$space = $this->getSpaceByName($user, $spaceName);
-		$fullUrl = $space["root"]["webDavUrl"] . '/' . ltrim($fileName, "/");
+		$this->setSpaceIDByName($user, $spaceName);
 		$this->featureContext->downloadFileAsUserUsingPassword($user, $fileName, $this->featureContext->getPasswordForUser($user));
 		Assert::assertGreaterThanOrEqual(
 			400,
@@ -1860,6 +1789,7 @@ class SpacesContext implements Context {
 
 	/**
 	 * @When /^user "([^"]*)" shares a space "([^"]*)" to user "([^"]*)" with role "([^"]*)"$/
+	 * @When /^user "([^"]*)" updates the space "([^"]*)" for user "([^"]*)" changing the role to "([^"]*)"$/
 	 *
 	 * @param  string $user
 	 * @param  string $spaceName