Repository: owncloud/ocis
Pull request #5784 (2023-03-09 15:07:29)

proxy: Assign user roles from OIDC claim
## Description
This adjusts the account resolver middleware to update a user's role assignment from a claim provided via the IDPs userinfo endpoint.

It's still work in progress and needs some adjustments before we can merge it:

- [x] Make the role claim name configurable. Currently it's hardcoded to `roles`
- [x] Make the role mapping (claim value to ocis role name) configurable. Currently the mapping is hardcoded to the roles deployed in the `ocis_keycloak` deployment example.
- [x] Allow to turn off the default role assignment that's currently happening in the graph API, which is somewhat annoying especially when AutoProvisioning is enabled (#5828).
- [x] Turn off creation for default role assigment for admin and demo users in settings service (#3432 )
- [x] Turn off creation of the admin user in idm, for this kind of setup (#5842)
- [x] Add some caching to avoid running ListRoles with every request
- [x] Restructure the code a bit to make UpdateRoleAssignmentsFromClaims() somewhat readable
- [ ] Update Readme
- [ ] Adapt keycloak deployment to skip admin user creating, and use oidc role assignment
- [ ] For later probably: Try to be a bit smarter about how often to check the role assignments. Currently this happens with every role request. Might make sense to only do that after receiving a new access_token (how?), or when the userinfo c
cache expired.

For testing purposes this can be used with the `ocis_keycloak` deployment, which already has some roles assigned to the demo users.

As ocis only allows one role per user. The code errors out if more than one role mapping is found for a user. We might need to change that behaviour somehow. See also: https://github.com/owncloud/ocis/pull/5750#pullrequestreview-1328586282

Currently this uses a very hacky way for obtaining a reva token with admin privileges (need for actually assigning the roles). We really need service accounts (#5550) for stuff like this.

Closes: #5669 


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

rhafer: @wkloucek @jfd Feel free to try this out.
rhafer: > That would be kind of a "OIDC Provider has the authority about user roles"-flag? So no more "SETTINGS_ADMIN_USER_ID" and editable roles in the WebUI!?

Yeah. I think so. If we get the roles from the IDP we should not allow overriding them, locally. This will just cause confusion (and also pretty difficult synchronization and conflict issues). Do you disagree?

@tbsbdr I guess this is probably also interesting for you.

@kulmann This also means we need some capability to tell the WebUI that roles cannot be re-assigned.
sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=5784)

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

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


ownclouders: :boom: Acceptance test [<strong>localApiTests-apiArchiver-ocis</strong>](https://drone.owncloud.com/owncloud/ocis/20392/47/1) failed. Further test are cancelled...
<!-- id: pr-5784 -->

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

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

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


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

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

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


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

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

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


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

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

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


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

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

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


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

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

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


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

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

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


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

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

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



Patch:
diff --git a/go.mod b/go.mod
index cf5a5a1a5d8..7c2e01d6862 100644
--- a/go.mod
+++ b/go.mod
@@ -86,7 +86,7 @@ require (
 	go.opentelemetry.io/otel/trace v1.14.0
 	golang.org/x/crypto v0.7.0
 	golang.org/x/exp v0.0.0-20221026004748-78e5e7837ae6
-	golang.org/x/image v0.5.0
+	golang.org/x/image v0.6.0
 	golang.org/x/net v0.8.0
 	golang.org/x/oauth2 v0.4.0
 	golang.org/x/term v0.6.0
diff --git a/go.sum b/go.sum
index e07a366e0c9..72d1cacad33 100644
--- a/go.sum
+++ b/go.sum
@@ -1156,8 +1156,6 @@ github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
 github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0=
 github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
-github.com/kobergj/gotext v0.0.0-20230309141732-b909eb0b8956 h1:b3MBTVgsfKXD/CdnAAqD7czu9Npx9QG+rHZycBIXJGE=
-github.com/kobergj/gotext v0.0.0-20230309141732-b909eb0b8956/go.mod h1:AT4NpQrOmyj1L/+hLja6aR0lk81yYYL4ePnj2kp7d6M=
 github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -1180,8 +1178,6 @@ github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c
 github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
 github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
 github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
-github.com/leonelquinteros/gotext v1.5.2 h1:T2y6ebHli+rMBCjcJlHTXyUrgXqsKBhl/ormgvt7lPo=
-github.com/leonelquinteros/gotext v1.5.2/go.mod h1:AT4NpQrOmyj1L/+hLja6aR0lk81yYYL4ePnj2kp7d6M=
 github.com/leonelquinteros/gotext v1.5.3-0.20230317130943-71a59c05b2c1 h1:k56sFOOJ0CYuQtGoRSeAMhP1R692+iNH+S1dC/CEz0w=
 github.com/leonelquinteros/gotext v1.5.3-0.20230317130943-71a59c05b2c1/go.mod h1:AT4NpQrOmyj1L/+hLja6aR0lk81yYYL4ePnj2kp7d6M=
 github.com/libregraph/idm v0.4.1-0.20230221143410-3503963047a5 h1:brLMXSjWoWhGXs8LpK+Lx+FQCtGLUa51Mq/ggHv9AV0=
@@ -1713,8 +1709,8 @@ golang.org/x/exp v0.0.0-20221026004748-78e5e7837ae6/go.mod h1:cyybsKvd6eL0RnXn6p
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
-golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
+golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4=
+golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -1740,6 +1736,7 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
 golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1813,6 +1810,7 @@ golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfS
 golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
 golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
 golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -1980,6 +1978,7 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
@@ -1988,6 +1987,7 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
 golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
 golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -2085,6 +2085,7 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
 golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/services/proxy/pkg/autoprovision/creator.go b/services/proxy/pkg/autoprovision/creator.go
new file mode 100644
index 00000000000..7714d78c19f
--- /dev/null
+++ b/services/proxy/pkg/autoprovision/creator.go
@@ -0,0 +1,97 @@
+package autoprovision
+
+import (
+	"context"
+	"encoding/json"
+
+	cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
+	types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
+	"github.com/cs3org/reva/v2/pkg/auth/scope"
+	"github.com/cs3org/reva/v2/pkg/token"
+	settingsService "github.com/owncloud/ocis/v2/services/settings/pkg/service/v0"
+)
+
+// Creator provides an interface to get a user or reva token with admin privileges
+type Creator interface {
+	// GetAutoProvisionAdmin returns a user with the Admin role assigned
+	GetAutoProvisionAdmin() (*cs3.User, error)
+	// GetAutoProvisionAdminToken returns a reva token with admin privileges
+	GetAutoProvisionAdminToken(ctx context.Context) (string, error)
+}
+
+// Options defines the available options for this package.
+type Options struct {
+	tokenManager token.Manager
+}
+
+// Option defines a single option function.
+type Option func(o *Options)
+
+// WithTokenManager sets the reva token manager
+func WithTokenManager(t token.Manager) Option {
+	return func(o *Options) {
+		o.tokenManager = t
+	}
+}
+
+type creator struct {
+	Options
+}
+
+// NewCreator returns a new Creator instance
+func NewCreator(opts ...Option) creator {
+	opt := Options{}
+	for _, o := range opts {
+		o(&opt)
+	}
+
+	return creator{
+		Options: opt,
+	}
+}
+
+// This returns an hardcoded internal User, that is privileged to create new User via
+// the Graph API. This user is needed for autoprovisioning of users from incoming OIDC
+// claims.
+func (c creator) GetAutoProvisionAdmin() (*cs3.User, error) {
+	roleIDsJSON, err := json.Marshal([]string{settingsService.BundleUUIDRoleAdmin})
+	if err != nil {
+		return nil, err
+	}
+
+	autoProvisionUserCreator := &cs3.User{
+		DisplayName: "Autoprovision User",
+		Username:    "autoprovisioner",
+		Id: &cs3.UserId{
+			Idp:      "internal",
+			OpaqueId: "autoprov-user-id00-0000-000000000000",
+		},
+		Opaque: &types.Opaque{
+			Map: map[string]*types.OpaqueEntry{
+				"roles": {
+					Decoder: "json",
+					Value:   roleIDsJSON,
+				},
+			},
+		},
+	}
+	return autoProvisionUserCreator, nil
+}
+
+func (c creator) GetAutoProvisionAdminToken(ctx context.Context) (string, error) {
+	userCreator, err := c.GetAutoProvisionAdmin()
+	if err != nil {
+		return "", err
+	}
+
+	s, err := scope.AddOwnerScope(nil)
+	if err != nil {
+		return "", err
+	}
+
+	token, err := c.tokenManager.MintToken(ctx, userCreator, s)
+	if err != nil {
+		return "", err
+	}
+	return token, nil
+}
diff --git a/services/proxy/pkg/command/server.go b/services/proxy/pkg/command/server.go
index 05a5b718ed8..d386ab49534 100644
--- a/services/proxy/pkg/command/server.go
+++ b/services/proxy/pkg/command/server.go
@@ -20,6 +20,7 @@ import (
 	"github.com/owncloud/ocis/v2/ocis-pkg/version"
 	settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
 	storesvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/store/v0"
+	"github.com/owncloud/ocis/v2/services/proxy/pkg/autoprovision"
 	"github.com/owncloud/ocis/v2/services/proxy/pkg/config"
 	"github.com/owncloud/ocis/v2/services/proxy/pkg/config/parser"
 	"github.com/owncloud/ocis/v2/services/proxy/pkg/logging"
@@ -31,6 +32,7 @@ import (
 	proxyHTTP "github.com/owncloud/ocis/v2/services/proxy/pkg/server/http"
 	"github.com/owncloud/ocis/v2/services/proxy/pkg/tracing"
 	"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend"
+	"github.com/owncloud/ocis/v2/services/proxy/pkg/userroles"
 	"github.com/urfave/cli/v2"
 	"golang.org/x/oauth2"
 )
@@ -136,22 +138,51 @@ func Server(cfg *config.Config) *cli.Command {
 func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config) alice.Chain {
 	rolesClient := settingssvc.NewRoleService("com.owncloud.api.settings", grpc.DefaultClient())
 	revaClient, err := pool.GetGatewayServiceClient(cfg.Reva.Address, cfg.Reva.GetRevaOptions()...)
+	if err != nil {
+		logger.Fatal().Err(err).Msg("Failed to get gateway client")
+	}
+	tokenManager, err := jwt.New(map[string]interface{}{
+		"secret": cfg.TokenManager.JWTSecret,
+	})
+	if err != nil {
+		logger.Fatal().Err(err).
+			Msg("Failed to create token manager")
+	}
+	autoProvsionCreator := autoprovision.NewCreator(autoprovision.WithTokenManager(tokenManager))
 	var userProvider backend.UserBackend
 	switch cfg.AccountBackend {
 	case "cs3":
-		tokenManager, err := jwt.New(map[string]interface{}{
-			"secret": cfg.TokenManager.JWTSecret,
-		})
-		if err != nil {
-			logger.Error().Err(err).
-				Msg("Failed to create token manager")
-		}
 
-		userProvider = backend.NewCS3UserBackend(rolesClient, revaClient, cfg.MachineAuthAPIKey, cfg.OIDC.Issuer, tokenManager, logger)
+		userProvider = backend.NewCS3UserBackend(
+			backend.WithLogger(logger),
+			backend.WithRevaAuthenticator(revaClient),
+			backend.WithMachineAuthAPIKey(cfg.MachineAuthAPIKey),
+			backend.WithOIDCissuer(cfg.OIDC.Issuer),
+			backend.WithAutoProvisonCreator(autoProvsionCreator),
+		)
 	default:
 		logger.Fatal().Msgf("Invalid accounts backend type '%s'", cfg.AccountBackend)
 	}
 
+	var roleAssigner userroles.UserRoleAssigner
+	switch cfg.RoleAssignment.Driver {
+	case "default":
+		roleAssigner = userroles.NewDefaultRoleAssigner(
+			userroles.WithRoleService(rolesClient),
+			userroles.WithLogger(logger),
+		)
+	case "oidc":
+		roleAssigner = userroles.NewOIDCRoleAssigner(
+			userroles.WithRoleService(rolesClient),
+			userroles.WithLogger(logger),
+			userroles.WithRolesClaim(cfg.RoleAssignment.OIDCRoleMapper.RoleClaim),
+			userroles.WithRoleMapping(cfg.RoleAssignment.OIDCRoleMapper.RoleMapping),
+			userroles.WithAutoProvisonCreator(autoProvsionCreator),
+		)
+	default:
+		logger.Fatal().Msgf("Invalid role assignment driver '%s'", cfg.RoleAssignment.Driver)
+	}
+
 	storeClient := storesvc.NewStoreService("com.owncloud.api.store", grpc.DefaultClient())
 	if err != nil {
 		logger.Error().Err(err).
@@ -203,6 +234,7 @@ func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config)
 		Logger:             logger,
 		PreSignedURLConfig: cfg.PreSignedURL,
 		UserProvider:       userProvider,
+		UserRoleAssigner:   roleAssigner,
 		Store:              storeClient,
 	})
 
@@ -229,21 +261,19 @@ func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config)
 		middleware.AccountResolver(
 			middleware.Logger(logger),
 			middleware.UserProvider(userProvider),
-			middleware.TokenManagerConfig(*cfg.TokenManager),
+			middleware.UserRoleAssigner(roleAssigner),
 			middleware.UserOIDCClaim(cfg.UserOIDCClaim),
 			middleware.UserCS3Claim(cfg.UserCS3Claim),
 			middleware.AutoprovisionAccounts(cfg.AutoprovisionAccounts),
 		),
 		middleware.SelectorCookie(
 			middleware.Logger(logger),
-			middleware.UserProvider(userProvider),
 			middleware.PolicySelectorConfig(*cfg.PolicySelector),
 		),
 		middleware.Policies(logger, cfg.PoliciesMiddleware.Query),
 		// finally, trigger home creation when a user logs in
 		middleware.CreateHome(
 			middleware.Logger(logger),
-			middleware.TokenManagerConfig(*cfg.TokenManager),
 			middleware.RevaGatewayClient(revaClient),
 			middleware.RoleQuotas(cfg.RoleQuotas),
 		),
diff --git a/services/proxy/pkg/config/config.go b/services/proxy/pkg/config/config.go
index 554caebcc2b..a0e5312601a 100644
--- a/services/proxy/pkg/config/config.go
+++ b/services/proxy/pkg/config/config.go
@@ -25,6 +25,7 @@ type Config struct {
 	Policies              []Policy           `yaml:"policies"`
 	OIDC                  OIDC               `yaml:"oidc"`
 	TokenManager          *TokenManager      `mask:"struct" yaml:"token_manager"`
+	RoleAssignment        RoleAssignment     `yaml:"role_assignment"`
 	PolicySelector        *PolicySelector    `yaml:"policy_selector"`
 	PreSignedURL          PreSignedURL       `yaml:"pre_signed_url"`
 	AccountBackend        string             `yaml:"account_backend" env:"PROXY_ACCOUNT_BACKEND_TYPE" desc:"Account backend the PROXY service should use. Currently only 'cs3' is possible here."`
@@ -121,6 +122,18 @@ type UserinfoCache struct {
 	TTL  int `yaml:"ttl" env:"PROXY_OIDC_USERINFO_CACHE_TTL" desc:"Max TTL in seconds for the OIDC user info cache."`
 }
 
+// RoleAssignment contains the configuration for how to assign roles to users during login
+type RoleAssignment struct {
+	Driver         string         `yaml:"driver" env:"PROXY_ROLE_ASSIGNMENT_DRIVER" desc:"The mechanism that should be used to assign roles to user upon login. Supported values: 'default' or 'oidc'. 'default' will assign the role 'user' to users which don't have a role assigned at the time they login. 'oidc' will assign the role based on the value of a claim (configured via PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM) from the users OIDC claims."`
+	OIDCRoleMapper OIDCRoleMapper `yaml:"oidc_role_mapper"`
+}
+
+// OIDCRoleMapper contains the configuration for the "oidc" role assignment driber
+type OIDCRoleMapper struct {
+	RoleClaim   string            `yaml:"role_claim" env:"PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM" desc:"The OIDC claim used to create the users role assignment."`
+	RoleMapping map[string]string `yaml:"role_mapping" desc:"A mapping of ocis role names to PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM claim values. This setting can only be configured in the configuration file and not via environment variables."`
+}
+
 // PolicySelector is the toplevel-configuration for different selectors
 type PolicySelector struct {
 	Static *StaticSelectorConf `yaml:"static"`
diff --git a/services/proxy/pkg/config/defaults/defaultconfig.go b/services/proxy/pkg/config/defaults/defaultconfig.go
index f1316cf9125..c7166fb9a07 100644
--- a/services/proxy/pkg/config/defaults/defaultconfig.go
+++ b/services/proxy/pkg/config/defaults/defaultconfig.go
@@ -52,7 +52,20 @@ func DefaultConfig() *config.Config {
 			},
 		},
 		PolicySelector: nil,
-		Reva:           shared.DefaultRevaConfig(),
+		RoleAssignment: config.RoleAssignment{
+			Driver: "default",
+			// this default is only relevant when Driver is set to "oidc"
+			OIDCRoleMapper: config.OIDCRoleMapper{
+				RoleClaim: "roles",
+				RoleMapping: map[string]string{
+					"admin":      "ocisAdmin",
+					"spaceadmin": "ocisSpaceAdmin",
+					"user":       "ocisUser",
+					"guest":      "ocisGuest",
+				},
+			},
+		},
+		Reva: shared.DefaultRevaConfig(),
 		PreSignedURL: config.PreSignedURL{
 			AllowedHTTPMethods: []string{"GET"},
 			Enabled:            true,
diff --git a/services/proxy/pkg/middleware/account_resolver.go b/services/proxy/pkg/middleware/account_resolver.go
index 6ccdf987d15..c8c16c9c7f7 100644
--- a/services/proxy/pkg/middleware/account_resolver.go
+++ b/services/proxy/pkg/middleware/account_resolver.go
@@ -5,6 +5,7 @@ import (
 	"net/http"
 
 	"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend"
+	"github.com/owncloud/ocis/v2/services/proxy/pkg/userroles"
 
 	revactx "github.com/cs3org/reva/v2/pkg/ctx"
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
@@ -24,6 +25,7 @@ func AccountResolver(optionSetters ...Option) func(next http.Handler) http.Handl
 			userProvider:          options.UserProvider,
 			userOIDCClaim:         options.UserOIDCClaim,
 			userCS3Claim:          options.UserCS3Claim,
+			userRoleAssigner:      options.UserRoleAssigner,
 			autoProvisionAccounts: options.AutoprovisionAccounts,
 		}
 	}
@@ -33,6 +35,7 @@ type accountResolver struct {
 	next                  http.Handler
 	logger                log.Logger
 	userProvider          backend.UserBackend
+	userRoleAssigner      userroles.UserRoleAssigner
 	autoProvisionAccounts bool
 	userOIDCClaim         string
 	userCS3Claim          string
@@ -62,7 +65,7 @@ func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 			return
 		}
 
-		user, token, err = m.userProvider.GetUserByClaims(req.Context(), m.userCS3Claim, value, true)
+		user, token, err = m.userProvider.GetUserByClaims(req.Context(), m.userCS3Claim, value)
 
 		if errors.Is(err, backend.ErrAccountNotFound) {
 			m.logger.Debug().Str("claim", m.userOIDCClaim).Str("value", value).Msg("User by claim not found")
@@ -76,7 +79,7 @@ func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 			if err != nil {
 				m.logger.Error().Err(err).Msg("Autoprovisioning user failed")
 			}
-			user, token, err = m.userProvider.GetUserByClaims(req.Context(), "userid", user.Id.OpaqueId, true)
+			user, token, err = m.userProvider.GetUserByClaims(req.Context(), "userid", user.Id.OpaqueId)
 			if err != nil {
 				m.logger.Error().Err(err).Str("userid", user.Id.OpaqueId).Msg("Error getting token for autoprovisioned user")
 			}
@@ -94,6 +97,14 @@ func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 			return
 		}
 
+		// resolve the user's roles
+		user, err = m.userRoleAssigner.UpdateUserRoleAssignment(ctx, user, claims)
+		if err != nil {
+			m.logger.Error().Err(err).Msg("Could not get user roles")
+			w.WriteHeader(http.StatusInternalServerError)
+			return
+		}
+
 		// add user to context for selectors
 		ctx = revactx.ContextSetUser(ctx, user)
 		req = req.WithContext(ctx)
@@ -101,7 +112,7 @@ func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 		m.logger.Debug().Interface("claims", claims).Interface("user", user).Msg("associated claims with user")
 	} else if user != nil {
 		var err error
-		_, token, err = m.userProvider.GetUserByClaims(req.Context(), "username", user.Username, true)
+		_, token, err = m.userProvider.GetUserByClaims(req.Context(), "username", user.Username)
 
 		if errors.Is(err, backend.ErrAccountDisabled) {
 			m.logger.Debug().Interface("user", user).Msg("Disabled")
diff --git a/services/proxy/pkg/middleware/options.go b/services/proxy/pkg/middleware/options.go
index c45bdd6682b..770d971ee30 100644
--- a/services/proxy/pkg/middleware/options.go
+++ b/services/proxy/pkg/middleware/options.go
@@ -5,6 +5,7 @@ import (
 	"time"
 
 	"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend"
+	"github.com/owncloud/ocis/v2/services/proxy/pkg/userroles"
 
 	settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
 	storesvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/store/v0"
@@ -27,8 +28,10 @@ type Options struct {
 	PolicySelector config.PolicySelector
 	// HTTPClient to use for communication with the oidcAuth provider
 	HTTPClient *http.Client
-	// UP
+	// UserProvider backend to use for resolving User
 	UserProvider backend.UserBackend
+	// UserRoleAssigner to user for assign a users default role
+	UserRoleAssigner userroles.UserRoleAssigner
 	// SettingsRoleService for the roles API in settings
 	SettingsRoleService settingssvc.RoleService
 	// OIDCProviderFunc to lazily initialize an oidc provider, must be set for the oidc_auth middleware
@@ -202,6 +205,13 @@ func UserProvider(up backend.UserBackend) Option {
 	}
 }
 
+// UserRoleAssigner sets the mechanism for assigning the default user roles
+func UserRoleAssigner(ra userroles.UserRoleAssigner) Option {
+	return func(o *Options) {
+		o.UserRoleAssigner = ra
+	}
+}
+
 // AccessTokenVerifyMethod set the mechanism for access token verification
 func AccessTokenVerifyMethod(method string) Option {
 	return func(o *Options) {
diff --git a/services/proxy/pkg/middleware/signed_url_auth.go b/services/proxy/pkg/middleware/signed_url_auth.go
index 793f6d8adca..a6bc3a703e3 100644
--- a/services/proxy/pkg/middleware/signed_url_auth.go
+++ b/services/proxy/pkg/middleware/signed_url_auth.go
@@ -17,6 +17,7 @@ import (
 	storesvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/store/v0"
 	"github.com/owncloud/ocis/v2/services/proxy/pkg/config"
 	"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend"
+	"github.com/owncloud/ocis/v2/services/proxy/pkg/userroles"
 	"golang.org/x/crypto/pbkdf2"
 )
 
@@ -43,6 +44,7 @@ type SignedURLAuthenticator struct {
 	Logger             log.Logger
 	PreSignedURLConfig config.PreSignedURL
 	UserProvider       backend.UserBackend
+	UserRoleAssigner   userroles.UserRoleAssigner
 	Store              storesvc.StoreService
 }
 
@@ -192,7 +194,17 @@ func (m SignedURLAuthenticator) Authenticate(r *http.Request) (*http.Request, bo
 		return nil, false
 	}
 
-	user, _, err := m.UserProvider.GetUserByClaims(r.Context(), "username", r.URL.Query().Get(_paramOCCredential), true)
+	user, _, err := m.UserProvider.GetUserByClaims(r.Context(), "username", r.URL.Query().Get(_paramOCCredential))
+	if err != nil {
+		m.Logger.Error().
+			Err(err).
+			Str("authenticator", "signed_url").
+			Str("path", r.URL.Path).
+			Msg("Could not get user by claim")
+		return nil, false
+	}
+
+	user, err = m.UserRoleAssigner.ApplyUserRole(r.Context(), user)
 	if err != nil {
 		m.Logger.Error().
 			Err(err).
diff --git a/services/proxy/pkg/user/backend/backend.go b/services/proxy/pkg/user/backend/backend.go
index b7aded6d3a6..efce21d55dc 100644
--- a/services/proxy/pkg/user/backend/backend.go
+++ b/services/proxy/pkg/user/backend/backend.go
@@ -2,13 +2,10 @@ package backend
 
 import (
 	"context"
-	"encoding/json"
 	"errors"
 
 	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
 	cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
-	types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
-	settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
 	"google.golang.org/grpc"
 )
 
@@ -21,46 +18,16 @@ var (
 	ErrNotSupported = errors.New("operation not supported")
 )
 
+//go:generate mockery --name=UserBackend
+
 // UserBackend allows the proxy to retrieve users from different user-backends (accounts-service, CS3)
 type UserBackend interface {
-	GetUserByClaims(ctx context.Context, claim, value string, withRoles bool) (*cs3.User, string, error)
+	GetUserByClaims(ctx context.Context, claim, value string) (*cs3.User, string, error)
 	Authenticate(ctx context.Context, username string, password string) (*cs3.User, string, error)
 	CreateUserFromClaims(ctx context.Context, claims map[string]interface{}) (*cs3.User, error)
-	GetUserGroups(ctx context.Context, userID string)
 }
 
 // RevaAuthenticator helper interface to mock auth-method from reva gateway-client.
 type RevaAuthenticator interface {
 	Authenticate(ctx context.Context, in *gateway.AuthenticateRequest, opts ...grpc.CallOption) (*gateway.AuthenticateResponse, error)
 }
-
-// loadRolesIDs returns the role-ids assigned to an user
-func loadRolesIDs(ctx context.Context, opaqueUserID string, rs settingssvc.RoleService) ([]string, error) {
-	req := &settingssvc.ListRoleAssignmentsRequest{AccountUuid: opaqueUserID}
-	assignmentResponse, err := rs.ListRoleAssignments(ctx, req)
-
-	if err != nil {
-		return nil, err
-	}
-
-	roleIDs := make([]string, 0)
-
-	for _, assignment := range assignmentResponse.Assignments {
-		roleIDs = append(roleIDs, assignment.RoleId)
-	}
-
-	return roleIDs, nil
-}
-
-// encodeRoleIDs encoded the given role id's in to reva-specific format to be able to mint a token from them
-func encodeRoleIDs(roleIDs []string) (*types.OpaqueEntry, error) {
-	roleIDsJSON, err := json.Marshal(roleIDs)
-	if err != nil {
-		return nil, err
-	}
-
-	return &types.OpaqueEntry{
-		Decoder: "json",
-		Value:   roleIDsJSON,
-	}, nil
-}
diff --git a/services/proxy/pkg/user/backend/cs3.go b/services/proxy/pkg/user/backend/cs3.go
index b37cd481336..59ef8dbaf9a 100644
--- a/services/proxy/pkg/user/backend/cs3.go
+++ b/services/proxy/pkg/user/backend/cs3.go
@@ -10,48 +10,87 @@ import (
 	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
 	cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
 	rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
-	types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
-	"github.com/cs3org/reva/v2/pkg/auth/scope"
 	revactx "github.com/cs3org/reva/v2/pkg/ctx"
-	"github.com/cs3org/reva/v2/pkg/token"
 	libregraph "github.com/owncloud/libre-graph-api-go"
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
-	"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
 	"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
 	"github.com/owncloud/ocis/v2/ocis-pkg/registry"
-	settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
 	"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
-	settingsService "github.com/owncloud/ocis/v2/services/settings/pkg/service/v0"
-	"go-micro.dev/v4/metadata"
+	"github.com/owncloud/ocis/v2/services/proxy/pkg/autoprovision"
 	"go-micro.dev/v4/selector"
 )
 
 type cs3backend struct {
-	graphSelector       selector.Selector
-	settingsRoleService settingssvc.RoleService
+	graphSelector selector.Selector
+	Options
+}
+
+// Option defines a single option function.
+type Option func(o *Options)
+
+// Options defines the available options for this package.
+type Options struct {
+	logger              log.Logger
 	authProvider        RevaAuthenticator
-	oidcISS             string
 	machineAuthAPIKey   string
-	tokenManager        token.Manager
-	logger              log.Logger
+	oidcISS             string
+	autoProvsionCreator autoprovision.Creator
+}
+
+// WithLogger sets the logger option
+func WithLogger(l log.Logger) Option {
+	return func(o *Options) {
+		o.logger = l
+	}
+}
+
+// WithRevaAuthenticator set the RevaAuthenticator option
+func WithRevaAuthenticator(ra RevaAuthenticator) Option {
+	return func(o *Options) {
+		o.authProvider = ra
+	}
+}
+
+// WithMachineAuthAPIKey configures the machine auth API key
+func WithMachineAuthAPIKey(ma string) Option {
+	return func(o *Options) {
+		o.machineAuthAPIKey = ma
+	}
+}
+
+// WithOIDCissuer set the OIDC issuer URL
+func WithOIDCissuer(oidcISS string) Option {
+	return func(o *Options) {
+		o.oidcISS = oidcISS
+	}
+}
+
+// WithAutoProvisonCreator configures the autoprovision creator to use
+func WithAutoProvisonCreator(c autoprovision.Creator) Option {
+	return func(o *Options) {
+		o.autoProvsionCreator = c
+	}
 }
 
 // NewCS3UserBackend creates a user-provider which fetches users from a CS3 UserBackend
-func NewCS3UserBackend(rs settingssvc.RoleService, ap RevaAuthenticator, machineAuthAPIKey string, oidcISS string, tokenManager token.Manager, logger log.Logger) UserBackend {
+func NewCS3UserBackend(opts ...Option) UserBackend {
+	opt := Options{}
+	for _, o := range opts {
+		o(&opt)
+	}
+
 	reg := registry.GetRegistry()
 	sel := selector.NewSelector(selector.Registry(reg))
-	return &cs3backend{
-		graphSelector:       sel,
-		settingsRoleService: rs,
-		authProvider:        ap,
-		oidcISS:             oidcISS,
-		machineAuthAPIKey:   machineAuthAPIKey,
-		tokenManager:        tokenManager,
-		logger:              logger,
+
+	b := cs3backend{
+		Options:       opt,
+		graphSelector: sel,
 	}
+
+	return &b
 }
 
-func (c *cs3backend) GetUserByClaims(ctx context.Context, claim, value string, withRoles bool) (*cs3.User, string, error) {
+func (c *cs3backend) GetUserByClaims(ctx context.Context, claim, value string) (*cs3.User, string, error) {
 	res, err := c.authProvider.Authenticate(ctx, &gateway.AuthenticateRequest{
 		Type:         "machine",
 		ClientId:     claim + ":" + value,
@@ -70,53 +109,6 @@ func (c *cs3backend) GetUserByClaims(ctx context.Context, claim, value string, w
 
 	user := res.User
 
-	if !withRoles {
-		return user, res.Token, nil
-	}
-
-	var roleIDs []string
-	if user.Id.Type != cs3.UserType_USER_TYPE_LIGHTWEIGHT {
-		roleIDs, err = loadRolesIDs(ctx, user.Id.OpaqueId, c.settingsRoleService)
-		if err != nil {
-			c.logger.Error().Err(err).Msgf("Could not load roles")
-			return nil, "", err
-		}
-
-		if len(roleIDs) == 0 {
-			// This user doesn't have a role assignment yet. Assign a
-			// default user role. At least until proper roles are provided. See
-			// 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")
-				ctx = metadata.Set(ctx, middleware.AccountID, user.Id.OpaqueId)
-				_, err := c.settingsRoleService.AssignRoleToUser(ctx, &settingssvc.AssignRoleToUserRequest{
-					AccountUuid: user.Id.OpaqueId,
-					RoleId:      settingsService.BundleUUIDRoleUser,
-				})
-				if err != nil {
-					c.logger.Error().Err(err).Msg("Could not add default role")
-					return nil, "", err
-				}
-				roleIDs = append(roleIDs, settingsService.BundleUUIDRoleUser)
-			}
-		}
-	}
-
-	enc, err := encodeRoleIDs(roleIDs)
-	if err != nil {
-		c.logger.Error().Err(err).Msg("Could not encode loaded roles")
-	}
-
-	if user.Opaque == nil {
-		user.Opaque = &types.Opaque{
-			Map: map[string]*types.OpaqueEntry{
-				"roles": enc,
-			},
-		}
-	} else {
-		user.Opaque.Map["roles"] = enc
-	}
-
 	return user, res.Token, nil
 }
 
@@ -143,7 +135,7 @@ func (c *cs3backend) Authenticate(ctx context.Context, username string, password
 // function will just return the existing user.
 func (c *cs3backend) CreateUserFromClaims(ctx context.Context, claims map[string]interface{}) (*cs3.User, error) {
 	newctx := context.Background()
-	token, err := c.generateAutoProvisionAdminToken(newctx)
+	token, err := c.autoProvsionCreator.GetAutoProvisionAdminToken(newctx)
 	if err != nil {
 		c.logger.Error().Err(err).Msg("Error generating token for autoprovisioning user.")
 		return nil, err
@@ -163,6 +155,8 @@ func (c *cs3backend) CreateUserFromClaims(ctx context.Context, claims map[string
 	req := lgClient.UsersApi.CreateUser(newctx).User(newUser)
 
 	created, resp, err := req.Execute()
+	defer resp.Body.Close()
+
 	var reread bool
 	if err != nil {
 		if resp == nil {
@@ -186,7 +180,8 @@ func (c *cs3backend) CreateUserFromClaims(ctx context.Context, claims map[string
 	if reread {
 		c.logger.Debug().Msg("User already exist, re-reading via libregraph")
 		gureq := lgClient.UserApi.GetUser(newctx, newUser.GetOnPremisesSamAccountName())
-		created, _, err = gureq.Execute()
+		created, resp, err = gureq.Execute()
+		defer resp.Body.Close()
 		if err != nil {
 			c.logger.Error().Err(err).Msg("Error trying to re-read user from graphAPI")
 			return nil, err
@@ -198,10 +193,6 @@ func (c *cs3backend) CreateUserFromClaims(ctx context.Context, claims map[string
 	return &cs3UserCreated, nil
 }
 
-func (c cs3backend) GetUserGroups(ctx context.Context, userID string) {
-	panic("implement me")
-}
-
 func (c cs3backend) setupLibregraphClient(ctx context.Context, cs3token string) (*libregraph.APIClient, error) {
 	// Use micro registry to resolve next graph service endpoint
 	next, err := c.graphSelector.Select("com.owncloud.graph.graph")
@@ -280,48 +271,3 @@ func (c cs3backend) cs3UserFromLibregraph(ctx context.Context, lu *libregraph.Us
 	cs3user.Mail = lu.GetMail()
 	return cs3user
 }
-
-// This returns an hardcoded internal User, that is privileged to create new User via
-// the Graph API. This user is needed for autoprovisioning of users from incoming OIDC
-// claims.
-func getAutoProvisionUserCreator() (*cs3.User, error) {
-	encRoleID, err := encodeRoleIDs([]string{settingsService.BundleUUIDRoleAdmin})
-	if err != nil {
-		return nil, err
-	}
-
-	autoProvisionUserCreator := &cs3.User{
-		DisplayName: "Autoprovision User",
-		Username:    "autoprovisioner",
-		Id: &cs3.UserId{
-			Idp:      "internal",
-			OpaqueId: "autoprov-user-id00-0000-000000000000",
-		},
-		Opaque: &types.Opaque{
-			Map: map[string]*types.OpaqueEntry{
-				"roles": encRoleID,
-			},
-		},
-	}
-	return autoProvisionUserCreator, nil
-}
-
-func (c cs3backend) generateAutoProvisionAdminToken(ctx context.Context) (string, error) {
-	userCreator, err := getAutoProvisionUserCreator()
-	if err != nil {
-		return "", err
-	}
-
-	s, err := scope.AddOwnerScope(nil)
-	if err != nil {
-		c.logger.Error().Err(err).Msg("could not get owner scope")
-		return "", err
-	}
-
-	token, err := c.tokenManager.MintToken(ctx, userCreator, s)
-	if err != nil {
-		c.logger.Error().Err(err).Msg("could not mint token")
-		return "", err
-	}
-	return token, nil
-}
diff --git a/services/proxy/pkg/user/backend/mocks/UserBackend.go b/services/proxy/pkg/user/backend/mocks/UserBackend.go
new file mode 100644
index 00000000000..76fe443c0cb
--- /dev/null
+++ b/services/proxy/pkg/user/backend/mocks/UserBackend.go
@@ -0,0 +1,113 @@
+// Code generated by mockery v2.14.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// UserBackend is an autogenerated mock type for the UserBackend type
+type UserBackend struct {
+	mock.Mock
+}
+
+// Authenticate provides a mock function with given fields: ctx, username, password
+func (_m *UserBackend) Authenticate(ctx context.Context, username string, password string) (*userv1beta1.User, string, error) {
+	ret := _m.Called(ctx, username, password)
+
+	var r0 *userv1beta1.User
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) *userv1beta1.User); ok {
+		r0 = rf(ctx, username, password)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*userv1beta1.User)
+		}
+	}
+
+	var r1 string
+	if rf, ok := ret.Get(1).(func(context.Context, string, string) string); ok {
+		r1 = rf(ctx, username, password)
+	} else {
+		r1 = ret.Get(1).(string)
+	}
+
+	var r2 error
+	if rf, ok := ret.Get(2).(func(context.Context, string, string) error); ok {
+		r2 = rf(ctx, username, password)
+	} else {
+		r2 = ret.Error(2)
+	}
+
+	return r0, r1, r2
+}
+
+// CreateUserFromClaims provides a mock function with given fields: ctx, claims
+func (_m *UserBackend) CreateUserFromClaims(ctx context.Context, claims map[string]interface{}) (*userv1beta1.User, error) {
+	ret := _m.Called(ctx, claims)
+
+	var r0 *userv1beta1.User
+	if rf, ok := ret.Get(0).(func(context.Context, map[string]interface{}) *userv1beta1.User); ok {
+		r0 = rf(ctx, claims)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*userv1beta1.User)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, map[string]interface{}) error); ok {
+		r1 = rf(ctx, claims)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// GetUserByClaims provides a mock function with given fields: ctx, claim, value
+func (_m *UserBackend) GetUserByClaims(ctx context.Context, claim string, value string) (*userv1beta1.User, string, error) {
+	ret := _m.Called(ctx, claim, value)
+
+	var r0 *userv1beta1.User
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) *userv1beta1.User); ok {
+		r0 = rf(ctx, claim, value)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*userv1beta1.User)
+		}
+	}
+
+	var r1 string
+	if rf, ok := ret.Get(1).(func(context.Context, string, string) string); ok {
+		r1 = rf(ctx, claim, value)
+	} else {
+		r1 = ret.Get(1).(string)
+	}
+
+	var r2 error
+	if rf, ok := ret.Get(2).(func(context.Context, string, string) error); ok {
+		r2 = rf(ctx, claim, value)
+	} else {
+		r2 = ret.Error(2)
+	}
+
+	return r0, r1, r2
+}
+
+type mockConstructorTestingTNewUserBackend interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewUserBackend creates a new instance of UserBackend. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewUserBackend(t mockConstructorTestingTNewUserBackend) *UserBackend {
+	mock := &UserBackend{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/services/proxy/pkg/userroles/defaultrole.go b/services/proxy/pkg/userroles/defaultrole.go
new file mode 100644
index 00000000000..ac5cc6e2b2a
--- /dev/null
+++ b/services/proxy/pkg/userroles/defaultrole.go
@@ -0,0 +1,77 @@
+package userroles
+
+import (
+	"context"
+
+	cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
+	"github.com/cs3org/reva/v2/pkg/utils"
+	"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
+	settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
+	settingsService "github.com/owncloud/ocis/v2/services/settings/pkg/service/v0"
+	"go-micro.dev/v4/metadata"
+)
+
+type defaultRoleAssigner struct {
+	Options
+}
+
+// NewDefaultRoleAssigner returns an implemenation of the UserRoleAssigner interface
+func NewDefaultRoleAssigner(opts ...Option) UserRoleAssigner {
+	opt := Options{}
+	for _, o := range opts {
+		o(&opt)
+	}
+
+	return defaultRoleAssigner{
+		Options: opt,
+	}
+}
+
+// UpdateUserRoleAssignment assigns the role "User" to the supplied user. Unless the user
+// already has a different role assigned.
+func (d defaultRoleAssigner) UpdateUserRoleAssignment(ctx context.Context, user *cs3.User, claims map[string]interface{}) (*cs3.User, error) {
+	var roleIDs []string
+	if user.Id.Type != cs3.UserType_USER_TYPE_LIGHTWEIGHT {
+		var err error
+		roleIDs, err = loadRolesIDs(ctx, user.Id.OpaqueId, d.roleService)
+		if err != nil {
+			d.logger.Error().Err(err).Msgf("Could not load roles")
+			return nil, err
+		}
+
+		if len(roleIDs) == 0 {
+			// This user doesn't have a role assignment yet. Assign a
+			// default user role. At least until proper roles are provided. See
+			// https://github.com/owncloud/ocis/issues/1825 for more context.
+			if user.Id.Type == cs3.UserType_USER_TYPE_PRIMARY {
+				d.logger.Info().Str("userid", user.Id.OpaqueId).Msg("user has no role assigned, assigning default user role")
+				ctx = metadata.Set(ctx, middleware.AccountID, user.Id.OpaqueId)
+				_, err := d.roleService.AssignRoleToUser(ctx, &settingssvc.AssignRoleToUserRequest{
+					AccountUuid: user.Id.OpaqueId,
+					RoleId:      settingsService.BundleUUIDRoleUser,
+				})
+				if err != nil {
+					d.logger.Error().Err(err).Msg("Could not add default role")
+					return nil, err
+				}
+				roleIDs = append(roleIDs, settingsService.BundleUUIDRoleUser)
+			}
+		}
+	}
+
+	user.Opaque = utils.AppendJSONToOpaque(user.Opaque, "roles", roleIDs)
+	return user, nil
+}
+
+// ApplyUserRole it looks up the user's role in the settings service and adds it
+// user's opaque data
+func (d defaultRoleAssigner) ApplyUserRole(ctx context.Context, user *cs3.User) (*cs3.User, error) {
+	roleIDs, err := loadRolesIDs(ctx, user.Id.OpaqueId, d.roleService)
+	if err != nil {
+		d.logger.Error().Err(err).Msgf("Could not load roles")
+		return nil, err
+	}
+
+	user.Opaque = utils.AppendJSONToOpaque(user.Opaque, "roles", roleIDs)
+	return user, nil
+}
diff --git a/services/proxy/pkg/userroles/mocks/UserRoleAssigner.go b/services/proxy/pkg/userroles/mocks/UserRoleAssigner.go
new file mode 100644
index 00000000000..39282c5b904
--- /dev/null
+++ b/services/proxy/pkg/userroles/mocks/UserRoleAssigner.go
@@ -0,0 +1,77 @@
+// Code generated by mockery v2.14.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	mock "github.com/stretchr/testify/mock"
+
+	userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
+)
+
+// UserRoleAssigner is an autogenerated mock type for the UserRoleAssigner type
+type UserRoleAssigner struct {
+	mock.Mock
+}
+
+// ApplyUserRole provides a mock function with given fields: ctx, user
+func (_m *UserRoleAssigner) ApplyUserRole(ctx context.Context, user *userv1beta1.User) (*userv1beta1.User, error) {
+	ret := _m.Called(ctx, user)
+
+	var r0 *userv1beta1.User
+	if rf, ok := ret.Get(0).(func(context.Context, *userv1beta1.User) *userv1beta1.User); ok {
+		r0 = rf(ctx, user)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*userv1beta1.User)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *userv1beta1.User) error); ok {
+		r1 = rf(ctx, user)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// UpdateUserRoleAssignment provides a mock function with given fields: ctx, user, claims
+func (_m *UserRoleAssigner) UpdateUserRoleAssignment(ctx context.Context, user *userv1beta1.User, claims map[string]interface{}) (*userv1beta1.User, error) {
+	ret := _m.Called(ctx, user, claims)
+
+	var r0 *userv1beta1.User
+	if rf, ok := ret.Get(0).(func(context.Context, *userv1beta1.User, map[string]interface{}) *userv1beta1.User); ok {
+		r0 = rf(ctx, user, claims)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*userv1beta1.User)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *userv1beta1.User, map[string]interface{}) error); ok {
+		r1 = rf(ctx, user, claims)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+type mockConstructorTestingTNewUserRoleAssigner interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewUserRoleAssigner creates a new instance of UserRoleAssigner. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewUserRoleAssigner(t mockConstructorTestingTNewUserRoleAssigner) *UserRoleAssigner {
+	mock := &UserRoleAssigner{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/services/proxy/pkg/userroles/oidcroles.go b/services/proxy/pkg/userroles/oidcroles.go
new file mode 100644
index 00000000000..0aee52d873b
--- /dev/null
+++ b/services/proxy/pkg/userroles/oidcroles.go
@@ -0,0 +1,198 @@
+package userroles
+
+import (
+	"context"
+	"errors"
+	"sync"
+	"time"
+
+	cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
+	revactx "github.com/cs3org/reva/v2/pkg/ctx"
+	"github.com/cs3org/reva/v2/pkg/utils"
+	"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
+	settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
+	"go-micro.dev/v4/metadata"
+)
+
+type oidcRoleAssigner struct {
+	Options
+}
+
+// NewOIDCRoleAssigner returns an implemenation of the UserRoleAssigner interface
+func NewOIDCRoleAssigner(opts ...Option) UserRoleAssigner {
+	opt := Options{}
+	for _, o := range opts {
+		o(&opt)
+	}
+
+	return oidcRoleAssigner{
+		Options: opt,
+	}
+}
+
+// UpdateUserRoleAssignment assigns the role "User" to the supplied user. Unless the user
+// already has a different role assigned.
+func (ra oidcRoleAssigner) UpdateUserRoleAssignment(ctx context.Context, user *cs3.User, claims map[string]interface{}) (*cs3.User, error) {
+	logger := ra.logger.SubloggerWithRequestID(ctx).With().Str("userid", user.GetId().GetOpaqueId()).Logger()
+	claimValueToRoleID, err := ra.oidcClaimvaluesToRoleIDs()
+	if err != nil {
+		logger.Error().Err(err).Msg("Error mapping claims to roles ids")
+		return nil, err
+	}
+
+	roleIDsFromClaim := make([]string, 0, 1)
+	claimRoles, ok := claims[ra.rolesClaim].([]interface{})
+	if !ok {
+		logger.Error().Err(err).Str("rolesClaim", ra.rolesClaim).Msg("No roles in user claims")
+		return nil, err
+	}
+	logger.Debug().Str("rolesClaim", ra.rolesClaim).Interface("rolesInClaim", claims[ra.rolesClaim]).Msg("got roles in claim")
+	for _, cri := range claimRoles {
+		cr, ok := cri.(string)
+		if !ok {
+			err := errors.New("invalid role in claims")
+			logger.Error().Err(err).Interface("claimValue", cri).Msg("Is not a valid string.")
+			return nil, err
+		}
+		id, ok := claimValueToRoleID[cr]
+		if !ok {
+			logger.Debug().Str("role", cr).Msg("No mapping for claim role. Skipped.")
+			continue
+		}
+		roleIDsFromClaim = append(roleIDsFromClaim, id)
+	}
+	logger.Debug().Interface("roleIDs", roleIDsFromClaim).Msg("Mapped claim roles to roleids")
+
+	switch len(roleIDsFromClaim) {
+	default:
+		err := errors.New("too many roles found in claims")
+		logger.Error().Err(err).Msg("Only one role per user is allowed.")
+		return nil, err
+	case 0:
+		err := errors.New("no role in claim, maps to a ocis role")
+		logger.Error().Err(err).Msg("")
+		return nil, err
+	case 1:
+		// exactly one mapping. This is right
+	}
+	assignedRoles, err := loadRolesIDs(ctx, user.GetId().GetOpaqueId(), ra.roleService)
+	if err != nil {
+		logger.Error().Err(err).Msg("Could not load roles")
+		return nil, err
+	}
+	if len(assignedRoles) > 1 {
+		err := errors.New("too many roles assigned")
+		logger.Error().Err(err).Msg("The user has too many roles assigned")
+		return nil, err
+	}
+	logger.Debug().Interface("assignedRoleIds", assignedRoles).Msg("Currently assigned roles")
+	if len(assignedRoles) == 0 || (assignedRoles[0] != roleIDsFromClaim[0]) {
+		logger.Debug().Interface("assignedRoleIds", assignedRoles).Interface("newRoleIds", roleIDsFromClaim).Msg("Updating role assignment for user")
+		newctx, err := ra.prepareAdminContext()
+		if err != nil {
+			logger.Error().Err(err).Msg("Error creating admin context")
+			return nil, err
+		}
+		if _, err = ra.roleService.AssignRoleToUser(newctx, &settingssvc.AssignRoleToUserRequest{
+			AccountUuid: user.GetId().GetOpaqueId(),
+			RoleId:      roleIDsFromClaim[0],
+		}); err != nil {
+			logger.Error().Err(err).Msg("Role assignment failed")
+			return nil, err
+		}
+	}
+
+	user.Opaque = utils.AppendJSONToOpaque(user.Opaque, "roles", roleIDsFromClaim)
+	return user, nil
+}
+
+// ApplyUserRole it looks up the user's role in the settings service and adds it
+// user's opaque data
+func (ra oidcRoleAssigner) ApplyUserRole(ctx context.Context, user *cs3.User) (*cs3.User, error) {
+	roleIDs, err := loadRolesIDs(ctx, user.Id.OpaqueId, ra.roleService)
+	if err != nil {
+		ra.logger.Error().Err(err).Msgf("Could not load roles")
+		return nil, err
+	}
+
+	user.Opaque = utils.AppendJSONToOpaque(user.Opaque, "roles", roleIDs)
+	return user, nil
+}
+
+func (ra oidcRoleAssigner) prepareAdminContext() (context.Context, error) {
+	newctx := context.Background()
+	autoProvisionUser, err := ra.autoProvsionCreator.GetAutoProvisionAdmin()
+	if err != nil {
+		return nil, err
+	}
+	token, err := ra.autoProvsionCreator.GetAutoProvisionAdminToken(newctx)
+	if err != nil {
+		ra.logger.Error().Err(err).Msg("Error generating token for provisioning role assignments.")
+		return nil, err
+	}
+	newctx = revactx.ContextSetToken(newctx, token)
+	newctx = metadata.Set(newctx, middleware.AccountID, autoProvisionUser.Id.OpaqueId)
+	newctx = metadata.Set(newctx, middleware.RoleIDs, string(autoProvisionUser.Opaque.Map["roles"].Value))
+	return newctx, nil
+}
+
+type roleClaimToIDCache struct {
+	roleClaimToID map[string]string
+	lastRead      time.Time
+	lock          sync.RWMutex
+}
+
+var roleClaimToID roleClaimToIDCache
+
+func (ra oidcRoleAssigner) oidcClaimvaluesToRoleIDs() (map[string]string, error) {
+	cacheTTL := 5 * time.Minute
+	roleClaimToID.lock.RLock()
+
+	if !roleClaimToID.lastRead.IsZero() && time.Since(roleClaimToID.lastRead) < cacheTTL {
+		defer roleClaimToID.lock.RUnlock()
+		return roleClaimToID.roleClaimToID, nil
+	}
+	ra.logger.Debug().Msg("refreshing roles ids")
+
+	// cache needs Refresh get a write lock
+	roleClaimToID.lock.RUnlock()
+	roleClaimToID.lock.Lock()
+	defer roleClaimToID.lock.Unlock()
+
+	// check again, another goroutine might have updated while we "upgraded" the lock
+	if !roleClaimToID.lastRead.IsZero() && time.Since(roleClaimToID.lastRead) < cacheTTL {
+		return roleClaimToID.roleClaimToID, nil
+	}
+
+	// Get all roles to find the role IDs.
+	// To list roles we need some elevated access to the settings service
+	// prepare a new request context for that until we have service accounts
+	ctx, err := ra.prepareAdminContext()
+	if err != nil {
+		ra.logger.Error().Err(err).Msg("Error creating admin context")
+		return nil, err
+	}
+
+	req := &settingssvc.ListBundlesRequest{}
+	res, err := ra.roleService.ListRoles(ctx, req)
+	if err != nil {
+		ra.logger.Error().Err(err).Msg("Failed to list all roles")
+		return map[string]string{}, err
+	}
+
+	newIDs := map[string]string{}
+	for _, role := range res.Bundles {
+		ra.logger.Debug().Str("role", role.Name).Str("id", role.Id).Msg("Got Role")
+		roleClaim, ok := ra.roleMapping[role.Name]
+		if !ok {
+			err := errors.New("Incomplete role mapping")
+			ra.logger.Error().Err(err).Str("role", role.Name).Msg("Role not mapped to a claim value")
+			return map[string]string{}, err
+		}
+		newIDs[roleClaim] = role.Id
+	}
+	ra.logger.Debug().Interface("roleMap", newIDs).Msg("Claim Role to role ID map")
+	roleClaimToID.roleClaimToID = newIDs
+	roleClaimToID.lastRead = time.Now()
+	return roleClaimToID.roleClaimToID, nil
+}
diff --git a/services/proxy/pkg/userroles/userroles.go b/services/proxy/pkg/userroles/userroles.go
new file mode 100644
index 00000000000..8c2dd431fe1
--- /dev/null
+++ b/services/proxy/pkg/userroles/userroles.go
@@ -0,0 +1,88 @@
+package userroles
+
+import (
+	"context"
+
+	cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
+	"github.com/owncloud/ocis/v2/ocis-pkg/log"
+	settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
+	"github.com/owncloud/ocis/v2/services/proxy/pkg/autoprovision"
+)
+
+//go:generate mockery --name=UserRoleAssigner
+
+// UserRoleAssigner allows to provide different implemenation for how users get their default roles
+// assigned by the proxy during authentication
+type UserRoleAssigner interface {
+	// UpdateUserRoleAssignment is called by the account resolver middleware. It updates the user's role assignment
+	// based on the user's (OIDC) claims. It adds the user's roles to the opaque data of the cs3.User struct
+	UpdateUserRoleAssignment(ctx context.Context, user *cs3.User, claims map[string]interface{}) (*cs3.User, error)
+	// ApplyUserRole can be called by proxy middlewares, it looks up the user's roles and adds them
+	// the users "roles" key in the user's opaque data
+	ApplyUserRole(ctx context.Context, user *cs3.User) (*cs3.User, error)
+}
+
+// Options defines the available options for this package.
+type Options struct {
+	roleService         settingssvc.RoleService
+	rolesClaim          string
+	roleMapping         map[string]string
+	autoProvsionCreator autoprovision.Creator
+	logger              log.Logger
+}
+
+// Option defines a single option function.
+type Option func(o *Options)
+
+// WithLogger configure the logger
+func WithLogger(l log.Logger) Option {
+	return func(o *Options) {
+		o.logger = l
+	}
+}
+
+// WithRoleService sets the roleservice instance to use
+func WithRoleService(rs settingssvc.RoleService) Option {
+	return func(o *Options) {
+		o.roleService = rs
+	}
+}
+
+// WithRolesClaim sets the OIDC claim for looking up role names
+func WithRolesClaim(claim string) Option {
+	return func(o *Options) {
+		o.rolesClaim = claim
+	}
+}
+
+// WithRoleMapping configures the map of ocis role names to claims values
+func WithRoleMapping(roleMap map[string]string) Option {
+	return func(o *Options) {
+		o.roleMapping = roleMap
+	}
+}
+
+// WithAutoProvisonCreator configures the autoprovision creator to use
+func WithAutoProvisonCreator(c autoprovision.Creator) Option {
+	return func(o *Options) {
+		o.autoProvsionCreator = c
+	}
+}
+
+// loadRolesIDs returns the role-ids assigned to an user
+func loadRolesIDs(ctx context.Context, opaqueUserID string, rs settingssvc.RoleService) ([]string, error) {
+	req := &settingssvc.ListRoleAssignmentsRequest{AccountUuid: opaqueUserID}
+	assignmentResponse, err := rs.ListRoleAssignments(ctx, req)
+
+	if err != nil {
+		return nil, err
+	}
+
+	roleIDs := make([]string, 0)
+
+	for _, assignment := range assignmentResponse.Assignments {
+		roleIDs = append(roleIDs, assignment.RoleId)
+	}
+
+	return roleIDs, nil
+}
diff --git a/services/settings/pkg/settings/mocks/Manager.go b/services/settings/pkg/settings/mocks/Manager.go
index ca216d97804..c5542f3ed37 100644
--- a/services/settings/pkg/settings/mocks/Manager.go
+++ b/services/settings/pkg/settings/mocks/Manager.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.10.4. DO NOT EDIT.
+// Code generated by mockery v2.14.0. DO NOT EDIT.
 
 package mocks
 
@@ -362,3 +362,18 @@ func (_m *Manager) WriteValue(value *v0.Value) (*v0.Value, error) {
 
 	return r0, r1
 }
+
+type mockConstructorTestingTNewManager interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewManager creates a new instance of Manager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewManager(t mockConstructorTestingTNewManager) *Manager {
+	mock := &Manager{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}


Test patch:
diff --git a/services/proxy/pkg/middleware/account_resolver_test.go b/services/proxy/pkg/middleware/account_resolver_test.go
index 14a74225cb0..e1b3b227d14 100644
--- a/services/proxy/pkg/middleware/account_resolver_test.go
+++ b/services/proxy/pkg/middleware/account_resolver_test.go
@@ -14,8 +14,10 @@ import (
 	"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
 	"github.com/owncloud/ocis/v2/services/proxy/pkg/config"
 	"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend"
-	"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend/test"
+	"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend/mocks"
+	userRoleMocks "github.com/owncloud/ocis/v2/services/proxy/pkg/userroles/mocks"
 	"github.com/stretchr/testify/assert"
+	"github.com/test-go/testify/mock"
 )
 
 func TestTokenIsAddedWithMailClaim(t *testing.T) {
@@ -119,15 +121,17 @@ func newMockAccountResolver(userBackendResult *userv1beta1.User, userBackendErr
 		token, _ = tokenManager.MintToken(context.Background(), userBackendResult, s)
 	}
 
-	mock := &test.UserBackendMock{
-		GetUserByClaimsFunc: func(ctx context.Context, claim string, value string, withRoles bool) (*userv1beta1.User, string, error) {
-			return userBackendResult, token, userBackendErr
-		},
-	}
+	ub := mocks.UserBackend{}
+	ub.On("GetUserByClaims", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(userBackendResult, token, userBackendErr)
+	ub.On("GetUserRoles", mock.Anything, mock.Anything).Return(userBackendResult, nil)
+
+	ra := userRoleMocks.UserRoleAssigner{}
+	ra.On("UpdateUserRoleAssignment", mock.Anything, mock.Anything, mock.Anything).Return(userBackendResult, nil)
 
 	return AccountResolver(
 		Logger(log.NewLogger()),
-		UserProvider(mock),
+		UserProvider(&ub),
+		UserRoleAssigner(&ra),
 		TokenManagerConfig(config.TokenManager{JWTSecret: "secret"}),
 		UserOIDCClaim(oidcclaim),
 		UserCS3Claim(cs3claim),
diff --git a/services/proxy/pkg/middleware/basic_auth_test.go b/services/proxy/pkg/middleware/basic_auth_test.go
index fcc2e6f3ce0..3e7660cf0dc 100644
--- a/services/proxy/pkg/middleware/basic_auth_test.go
+++ b/services/proxy/pkg/middleware/basic_auth_test.go
@@ -1,42 +1,41 @@
 package middleware
 
 import (
-	"context"
 	"net/http"
 	"net/http/httptest"
 
 	userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
 	. "github.com/onsi/ginkgo/v2"
+	"github.com/test-go/testify/mock"
 
 	. "github.com/onsi/gomega"
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
 	"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
 	"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend"
-	"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend/test"
+	"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend/mocks"
 )
 
 var _ = Describe("Authenticating requests", Label("BasicAuthenticator"), func() {
 	var authenticator Authenticator
+	ub := mocks.UserBackend{}
+	ub.On("Authenticate", mock.Anything, "testuser", "testpassword").Return(
+		&userv1beta1.User{
+			Id: &userv1beta1.UserId{
+				Idp:      "IdpId",
+				OpaqueId: "OpaqueId",
+			},
+			Username: "testuser",
+			Mail:     "testuser@example.com",
+		},
+		"",
+		nil,
+	)
+	ub.On("Authenticate", mock.Anything, mock.Anything, mock.Anything).Return(nil, "", backend.ErrAccountNotFound)
+
 	BeforeEach(func() {
 		authenticator = BasicAuthenticator{
-			Logger: log.NewLogger(),
-			UserProvider: &test.UserBackendMock{
-				AuthenticateFunc: func(ctx context.Context, username, password string) (*userv1beta1.User, string, error) {
-					var user *userv1beta1.User
-					if username == "testuser" && password == "testpassword" {
-						user = &userv1beta1.User{
-							Id: &userv1beta1.UserId{
-								Idp:      "IdpId",
-								OpaqueId: "OpaqueId",
-							},
-							Username: "testuser",
-							Mail:     "testuser@example.com",
-						}
-						return user, "", nil
-					}
-					return nil, "", backend.ErrAccountNotFound
-				},
-			},
+			Logger:       log.NewLogger(),
+			UserProvider: &ub,
 		}
 	})
 
diff --git a/services/proxy/pkg/user/backend/test/backend_mock.go b/services/proxy/pkg/user/backend/test/backend_mock.go
deleted file mode 100644
index 49c642a29ac..00000000000
--- a/services/proxy/pkg/user/backend/test/backend_mock.go
+++ /dev/null
@@ -1,248 +0,0 @@
-// Code generated by moq; DO NOT EDIT.
-// github.com/matryer/moq
-
-package test
-
-import (
-	"context"
-	"sync"
-
-	userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
-	"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend"
-)
-
-// Ensure, that UserBackendMock does implement UserBackend.
-// If this is not the case, regenerate this file with moq.
-var _ backend.UserBackend = &UserBackendMock{}
-
-// UserBackendMock is a mock implementation of UserBackend.
-//
-//     func TestSomethingThatUsesUserBackend(t *testing.T) {
-//
-//         // make and configure a mocked UserBackend
-//         mockedUserBackend := &UserBackendMock{
-//             AuthenticateFunc: func(ctx context.Context, username string, password string) (*userv1beta1.User, error) {
-// 	               panic("mock out the Authenticate method")
-//             },
-//             CreateUserFromClaimsFunc: func(ctx context.Context, claims *oidc.StandardClaims) (*userv1beta1.User, error) {
-// 	               panic("mock out the CreateUserFromClaims method")
-//             },
-//             GetUserByClaimsFunc: func(ctx context.Context, claim string, value string, withRoles bool) (*userv1beta1.User, error) {
-// 	               panic("mock out the GetUserByClaims method")
-//             },
-//             GetUserGroupsFunc: func(ctx context.Context, userID string)  {
-// 	               panic("mock out the GetUserGroups method")
-//             },
-//         }
-//
-//         // use mockedUserBackend in code that requires UserBackend
-//         // and then make assertions.
-//
-//     }
-type UserBackendMock struct {
-	// AuthenticateFunc mocks the Authenticate method.
-	AuthenticateFunc func(ctx context.Context, username string, password string) (*userv1beta1.User, string, error)
-
-	// CreateUserFromClaimsFunc mocks the CreateUserFromClaims method.
-	CreateUserFromClaimsFunc func(ctx context.Context, claims map[string]interface{}) (*userv1beta1.User, error)
-
-	// GetUserByClaimsFunc mocks the GetUserByClaims method.
-	GetUserByClaimsFunc func(ctx context.Context, claim string, value string, withRoles bool) (*userv1beta1.User, string, error)
-
-	// GetUserGroupsFunc mocks the GetUserGroups method.
-	GetUserGroupsFunc func(ctx context.Context, userID string)
-
-	// calls tracks calls to the methods.
-	calls struct {
-		// Authenticate holds details about calls to the Authenticate method.
-		Authenticate []struct {
-			// Ctx is the ctx argument value.
-			Ctx context.Context
-			// Username is the username argument value.
-			Username string
-			// Password is the password argument value.
-			Password string
-		}
-		// CreateUserFromClaims holds details about calls to the CreateUserFromClaims method.
-		CreateUserFromClaims []struct {
-			// Ctx is the ctx argument value.
-			Ctx context.Context
-			// Claims is the claims argument value.
-			Claims map[string]interface{}
-		}
-		// GetUserByClaims holds details about calls to the GetUserByClaims method.
-		GetUserByClaims []struct {
-			// Ctx is the ctx argument value.
-			Ctx context.Context
-			// Claim is the claim argument value.
-			Claim string
-			// Value is the value argument value.
-			Value string
-			// WithRoles is the withRoles argument value.
-			WithRoles bool
-		}
-		// GetUserGroups holds details about calls to the GetUserGroups method.
-		GetUserGroups []struct {
-			// Ctx is the ctx argument value.
-			Ctx context.Context
-			// UserID is the userID argument value.
-			UserID string
-		}
-	}
-	lockAuthenticate         sync.RWMutex
-	lockCreateUserFromClaims sync.RWMutex
-	lockGetUserByClaims      sync.RWMutex
-	lockGetUserGroups        sync.RWMutex
-}
-
-// Authenticate calls AuthenticateFunc.
-func (mock *UserBackendMock) Authenticate(ctx context.Context, username string, password string) (*userv1beta1.User, string, error) {
-	if mock.AuthenticateFunc == nil {
-		panic("UserBackendMock.AuthenticateFunc: method is nil but UserBackend.Authenticate was just called")
-	}
-	callInfo := struct {
-		Ctx      context.Context
-		Username string
-		Password string
-	}{
-		Ctx:      ctx,
-		Username: username,
-		Password: password,
-	}
-	mock.lockAuthenticate.Lock()
-	mock.calls.Authenticate = append(mock.calls.Authenticate, callInfo)
-	mock.lockAuthenticate.Unlock()
-	return mock.AuthenticateFunc(ctx, username, password)
-}
-
-// AuthenticateCalls gets all the calls that were made to Authenticate.
-// Check the length with:
-//     len(mockedUserBackend.AuthenticateCalls())
-func (mock *UserBackendMock) AuthenticateCalls() []struct {
-	Ctx      context.Context
-	Username string
-	Password string
-} {
-	var calls []struct {
-		Ctx      context.Context
-		Username string
-		Password string
-	}
-	mock.lockAuthenticate.RLock()
-	calls = mock.calls.Authenticate
-	mock.lockAuthenticate.RUnlock()
-	return calls
-}
-
-// CreateUserFromClaims calls CreateUserFromClaimsFunc.
-func (mock *UserBackendMock) CreateUserFromClaims(ctx context.Context, claims map[string]interface{}) (*userv1beta1.User, error) {
-	if mock.CreateUserFromClaimsFunc == nil {
-		panic("UserBackendMock.CreateUserFromClaimsFunc: method is nil but UserBackend.CreateUserFromClaims was just called")
-	}
-	callInfo := struct {
-		Ctx    context.Context
-		Claims map[string]interface{}
-	}{
-		Ctx:    ctx,
-		Claims: claims,
-	}
-	mock.lockCreateUserFromClaims.Lock()
-	mock.calls.CreateUserFromClaims = append(mock.calls.CreateUserFromClaims, callInfo)
-	mock.lockCreateUserFromClaims.Unlock()
-	return mock.CreateUserFromClaimsFunc(ctx, claims)
-}
-
-// CreateUserFromClaimsCalls gets all the calls that were made to CreateUserFromClaims.
-// Check the length with:
-//     len(mockedUserBackend.CreateUserFromClaimsCalls())
-func (mock *UserBackendMock) CreateUserFromClaimsCalls() []struct {
-	Ctx    context.Context
-	Claims map[string]interface{}
-} {
-	var calls []struct {
-		Ctx    context.Context
-		Claims map[string]interface{}
-	}
-	mock.lockCreateUserFromClaims.RLock()
-	calls = mock.calls.CreateUserFromClaims
-	mock.lockCreateUserFromClaims.RUnlock()
-	return calls
-}
-
-// GetUserByClaims calls GetUserByClaimsFunc.
-func (mock *UserBackendMock) GetUserByClaims(ctx context.Context, claim string, value string, withRoles bool) (*userv1beta1.User, string, error) {
-	if mock.GetUserByClaimsFunc == nil {
-		panic("UserBackendMock.GetUserByClaimsFunc: method is nil but UserBackend.GetUserByClaims was just called")
-	}
-	callInfo := struct {
-		Ctx       context.Context
-		Claim     string
-		Value     string
-		WithRoles bool
-	}{
-		Ctx:       ctx,
-		Claim:     claim,
-		Value:     value,
-		WithRoles: withRoles,
-	}
-	mock.lockGetUserByClaims.Lock()
-	mock.calls.GetUserByClaims = append(mock.calls.GetUserByClaims, callInfo)
-	mock.lockGetUserByClaims.Unlock()
-	return mock.GetUserByClaimsFunc(ctx, claim, value, withRoles)
-}
-
-// GetUserByClaimsCalls gets all the calls that were made to GetUserByClaims.
-// Check the length with:
-//     len(mockedUserBackend.GetUserByClaimsCalls())
-func (mock *UserBackendMock) GetUserByClaimsCalls() []struct {
-	Ctx       context.Context
-	Claim     string
-	Value     string
-	WithRoles bool
-} {
-	var calls []struct {
-		Ctx       context.Context
-		Claim     string
-		Value     string
-		WithRoles bool
-	}
-	mock.lockGetUserByClaims.RLock()
-	calls = mock.calls.GetUserByClaims
-	mock.lockGetUserByClaims.RUnlock()
-	return calls
-}
-
-// GetUserGroups calls GetUserGroupsFunc.
-func (mock *UserBackendMock) GetUserGroups(ctx context.Context, userID string) {
-	if mock.GetUserGroupsFunc == nil {
-		panic("UserBackendMock.GetUserGroupsFunc: method is nil but UserBackend.GetUserGroups was just called")
-	}
-	callInfo := struct {
-		Ctx    context.Context
-		UserID string
-	}{
-		Ctx:    ctx,
-		UserID: userID,
-	}
-	mock.lockGetUserGroups.Lock()
-	mock.calls.GetUserGroups = append(mock.calls.GetUserGroups, callInfo)
-	mock.lockGetUserGroups.Unlock()
-	mock.GetUserGroupsFunc(ctx, userID)
-}
-
-// GetUserGroupsCalls gets all the calls that were made to GetUserGroups.
-// Check the length with:
-//     len(mockedUserBackend.GetUserGroupsCalls())
-func (mock *UserBackendMock) GetUserGroupsCalls() []struct {
-	Ctx    context.Context
-	UserID string
-} {
-	var calls []struct {
-		Ctx    context.Context
-		UserID string
-	}
-	mock.lockGetUserGroups.RLock()
-	calls = mock.calls.GetUserGroups
-	mock.lockGetUserGroups.RUnlock()
-	return calls
-}