Repository: owncloud/ocis
Pull request #5500 (2023-02-01 15:17:48)

Space Trash-bin expiration cli
## Description
Introduction of a new cli command to purge old trash-bin items.
The command is part of the `storage-users` service and can be used as follows:

`ocis storage-users trash-bin purge-expired`.

The `purge-expired` command configuration is done in the `ocis`configuration or as usual by using environment variables.

ENV `STORAGE_USERS_PURGE_TRASH_BIN_USER_ID` is used to obtain space trash-bin information and takes the system admin user as the default `OCIS_ADMIN_USER_ID`.
It should be noted, that this is only set by default in the single binary. The command only considers spaces to which the user has access and delete permission.

ENV `STORAGE_USERS_PURGE_TRASH_BIN_PERSONAL_DELETE_BEFORE` has a default value of `30 days`, which means the command will delete all files older than `30 days`.
The value is human-readable, valid values are `24h`, `60m`, `60s` etc. `0` is equivalent to disable and prevents the deletion of `personal space` trash-bin files.

ENV `STORAGE_USERS_PURGE_TRASH_BIN_PROJECT_DELETE_BEFORE` has a default value of `30 days`, which means the command will delete all files older than `30 days`.
The value is human-readable, valid values are `24h`, `60m`, `60s` etc. `0` is equivalent to disable and prevents the deletion of `project space` trash-bin files.

Likewise, only spaces of the type `project` and `personal` are taken into account.
Spaces of type `virtual`, for example, are ignored.

## Related Issue
- Fixes https://github.com/owncloud/ocis/issues/5499

## Motivation and Context
storage space is expensive, therefor we need a way to decide which files can be deleted and cleanup the trash-bin.

## Todos:
- [X] add events service
- [X] use events to trigger the cleanup task (cli)
- [X] get sufficient executant user
- [X] error handling
- [X] changelog
- [X] unit tests
- [X] add `expiry` configuration 
- [ ] talk to doc-team and provide information

## How Has This Been Tested?
- unit tests

## Screenshots (if appropriate):

## Types of changes
- [ ] Bug fix (non-breaking change which fixes an issue)
- [X] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Technical debt
- [ ] Tests only (no source changes)

## Checklist:
- [X] Code changes
- [X] Unit tests added
- [ ] Acceptance tests added
- [X] Documentation ticket raised:


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

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

[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/0-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&metric=new_coverage&view=list) [0.0% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&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=5500&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&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=5500)

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

[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/0-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&metric=new_coverage&view=list) [0.0% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&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=5500&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&metric=new_duplicated_lines_density&view=list)


ownclouders: :boom: Acceptance test [<strong>wopiValidatorTests-ocis</strong>](https://drone.owncloud.com/owncloud/ocis/18846/40/1) failed. Further test are cancelled...
<!-- id: pr-5500 -->

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

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

[![46.5%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/40-16px.png '46.5%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&metric=new_coverage&view=list) [46.5% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&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=5500&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&metric=new_duplicated_lines_density&view=list)


fschade: @mmattel i could use a little `doc magic` help 😘 
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=5500)

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

[![46.5%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/40-16px.png '46.5%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&metric=new_coverage&view=list) [46.5% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&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=5500&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&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=5500)

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

[![51.1%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/50-16px.png '51.1%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&metric=new_coverage&view=list) [51.1% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&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=5500&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&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=5500)

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

[![51.1%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/50-16px.png '51.1%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&metric=new_coverage&view=list) [51.1% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&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=5500&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&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=5500)

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

[![51.1%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/50-16px.png '51.1%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&metric=new_coverage&view=list) [51.1% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&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=5500&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&metric=new_duplicated_lines_density&view=list)


mmattel: As todo and as discussed yesterday, we need to agree on a way how to describe command line calls. Currently we do have two commands which are:
`ocis storage-users uploads`
`ocis search index --space $SPACE_ID --user $USER_ID`
You can find both of them in the corresponding service readme/description.
fschade: > As todo and as discussed yesterday, we need to agree on a way how to describe command line calls. Currently we do have two commands which are: `ocis storage-users uploads` `ocis search index --space $SPACE_ID --user $USER_ID` Note that both start with `ocis` You can find both of them in the corresponding service readme/description.
> 
> My proposal for this is the following:
> 
> * As we currently and only ship the single binary containing all services, this binary is accessed with `ocis` and then the service name - this is what is currently reflected in the examples above and also in the readme's and admin docs.
>   `As result we should put "ocis" infront of the example command in the changelog`.
> * I will drop a note in the admin docs about what it means when we do a command example
> * If we change and not only provide a single but also service binaries, we have to:
>   Publish them accordingly
>   Change a lot of doc related stuff in the code, in ocis.dev and the admin docs

@mmattel thanks for your input, yes we discussed it in the scope of this issue, but the topic is broader and should not stop merging this pr. I see this as an `thought anchor` ;)
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=5500)

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

[![48.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/40-16px.png '48.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&metric=new_coverage&view=list) [48.0% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&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=5500&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5500&metric=new_duplicated_lines_density&view=list)


mmattel: @fschade you also have to update the `storage-users readme.md` with the text of the new cli stuff accordingly. We will then update the admin docs based on that content.
kobergj: @mmattel `storage-users` has no `README.md` yet. This is also not an easy task as it needs to explain what a `reva` is and also the differences to `storage-publiclink` , `storage-shares` and `storage-system`. Maybe better if that is done in a subsequent PR.
mmattel: > Maybe better if that is done in a subsequent PR.

Fine with that, but needs to be done. We now have the case where a readme needs to be created as discussed in our (big) doc meeting 😄 @fschade I am so in sorry 🤣 

Patch:
diff --git a/changelog/unreleased/space-trashbin-purge-cli.md b/changelog/unreleased/space-trashbin-purge-cli.md
new file mode 100644
index 00000000000..86d30bc4601
--- /dev/null
+++ b/changelog/unreleased/space-trashbin-purge-cli.md
@@ -0,0 +1,23 @@
+Enhancement: Cli to purge expired trash-bin items
+
+Introduction of a new cli command to purge old trash-bin items.
+The command is part of the `storage-users` service and can be used as follows:
+
+`ocis storage-users trash-bin purge-expired`.
+
+The `purge-expired` command configuration is done in the `ocis`configuration or as usual by using environment variables.
+
+ENV `STORAGE_USERS_PURGE_TRASH_BIN_USER_ID` is used to obtain space trash-bin information and takes the system admin user as the default `OCIS_ADMIN_USER_ID`.
+It should be noted, that this is only set by default in the single binary. The command only considers spaces to which the user has access and delete permission.
+
+ENV `STORAGE_USERS_PURGE_TRASH_BIN_PERSONAL_DELETE_BEFORE` has a default value of `30 days`, which means the command will delete all files older than `30 days`.
+The value is human-readable, valid values are `24h`, `60m`, `60s` etc. `0` is equivalent to disable and prevents the deletion of `personal space` trash-bin files.
+
+ENV `STORAGE_USERS_PURGE_TRASH_BIN_PROJECT_DELETE_BEFORE` has a default value of `30 days`, which means the command will delete all files older than `30 days`.
+The value is human-readable, valid values are `24h`, `60m`, `60s` etc. `0` is equivalent to disable and prevents the deletion of `project space` trash-bin files.
+
+Likewise, only spaces of the type `project` and `personal` are taken into account.
+Spaces of type `virtual`, for example, are ignored.
+
+https://github.com/owncloud/ocis/pull/5500
+https://github.com/owncloud/ocis/issues/5499
diff --git a/services/proxy/pkg/config/config.go b/services/proxy/pkg/config/config.go
index 36c96c200db..234ca828ec7 100644
--- a/services/proxy/pkg/config/config.go
+++ b/services/proxy/pkg/config/config.go
@@ -27,7 +27,7 @@ type Config struct {
 	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."`
-	UserOIDCClaim         string          `yaml:"user_oidc_claim" env:"PROXY_USER_OIDC_CLAIM" desc:"The name of an OpenID Connect claim that should be used for resolving users with the account backend. Currently defaults to 'email'."`
+	UserOIDCClaim         string          `yaml:"user_oidc_claim" env:"PROXY_USER_OIDC_CLAIM" desc:"The name of an OpenID Connect claim that is used for resolving users with the account backend. The value of the claim must hold a per user unique, stable and non re-assignable identifier. The availability of claims depends on your Identity Provider. There are common claims available for most Identity providers like 'email' or 'preferred_user' but you can also add your own claim."`
 	UserCS3Claim          string          `yaml:"user_cs3_claim" env:"PROXY_USER_CS3_CLAIM" desc:"The name of a CS3 user attribute (claim) that should be mapped to the 'user_oidc_claim'. Supported values are 'username', 'mail' and 'userid'."`
 	MachineAuthAPIKey     string          `mask:"password" yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY;PROXY_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary to access resources from other services."`
 	AutoprovisionAccounts bool            `yaml:"auto_provision_accounts" env:"PROXY_AUTOPROVISION_ACCOUNTS" desc:"Set this to 'true' to automatically provision users that do not yet exist in the users service on-demand upon first sign-in. To use this a write-enabled libregraph user backend needs to be setup an running."`
diff --git a/services/storage-users/pkg/command/root.go b/services/storage-users/pkg/command/root.go
index 21c3089b6cd..848e866c7da 100644
--- a/services/storage-users/pkg/command/root.go
+++ b/services/storage-users/pkg/command/root.go
@@ -19,6 +19,7 @@ func GetCommands(cfg *config.Config) cli.Commands {
 
 		// interaction with this service
 		Uploads(cfg),
+		TrashBin(cfg),
 
 		// infos about this service
 		Health(cfg),
diff --git a/services/storage-users/pkg/command/server.go b/services/storage-users/pkg/command/server.go
index 8c976eb376b..ffff3afe21b 100644
--- a/services/storage-users/pkg/command/server.go
+++ b/services/storage-users/pkg/command/server.go
@@ -7,6 +7,7 @@ import (
 	"path"
 
 	"github.com/cs3org/reva/v2/cmd/revad/runtime"
+	"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
 	"github.com/gofrs/uuid"
 	"github.com/oklog/run"
 	"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
@@ -15,6 +16,7 @@ import (
 	"github.com/owncloud/ocis/v2/ocis-pkg/version"
 	"github.com/owncloud/ocis/v2/services/storage-users/pkg/config"
 	"github.com/owncloud/ocis/v2/services/storage-users/pkg/config/parser"
+	"github.com/owncloud/ocis/v2/services/storage-users/pkg/event"
 	"github.com/owncloud/ocis/v2/services/storage-users/pkg/logging"
 	"github.com/owncloud/ocis/v2/services/storage-users/pkg/revaconfig"
 	"github.com/owncloud/ocis/v2/services/storage-users/pkg/server/debug"
@@ -88,6 +90,27 @@ func Server(cfg *config.Config) *cli.Command {
 				logger.Fatal().Err(err).Msg("failed to register the grpc endpoint")
 			}
 
+			{
+				stream, err := event.NewStream(cfg.Events)
+				if err != nil {
+					logger.Fatal().Err(err).Msg("can't connect to nats")
+				}
+
+				gw, err := pool.GetGatewayServiceClient(cfg.Reva.Address)
+				if err != nil {
+					return err
+				}
+
+				eventSVC, err := event.NewService(gw, stream, logger, *cfg)
+				if err != nil {
+					logger.Fatal().Err(err).Msg("can't create event service")
+				}
+
+				gr.Add(eventSVC.Run, func(_ error) {
+					cancel()
+				})
+			}
+
 			return gr.Run()
 		},
 	}
diff --git a/services/storage-users/pkg/command/trash_bin.go b/services/storage-users/pkg/command/trash_bin.go
new file mode 100644
index 00000000000..fdff7e1d59f
--- /dev/null
+++ b/services/storage-users/pkg/command/trash_bin.go
@@ -0,0 +1,55 @@
+package command
+
+import (
+	"time"
+
+	"github.com/cs3org/reva/v2/pkg/events"
+	"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
+	"github.com/owncloud/ocis/v2/services/storage-users/pkg/config"
+	"github.com/owncloud/ocis/v2/services/storage-users/pkg/config/parser"
+	"github.com/owncloud/ocis/v2/services/storage-users/pkg/event"
+	"github.com/urfave/cli/v2"
+)
+
+// TrashBin wraps trash-bin related sub-commands.
+func TrashBin(cfg *config.Config) *cli.Command {
+	return &cli.Command{
+		Name:  "trash-bin",
+		Usage: "manage trash-bin's",
+		Subcommands: []*cli.Command{
+			PurgeExpiredResources(cfg),
+		},
+	}
+}
+
+// PurgeExpiredResources cli command removes old trash-bin items.
+func PurgeExpiredResources(cfg *config.Config) *cli.Command {
+	return &cli.Command{
+		Name:  "purge-expired",
+		Usage: "Purge expired trash-bin items",
+		Flags: []cli.Flag{},
+		Before: func(c *cli.Context) error {
+			return configlog.ReturnFatal(parser.ParseConfig(cfg))
+		},
+		Action: func(c *cli.Context) error {
+			stream, err := event.NewStream(cfg.Events)
+			if err != nil {
+				return err
+			}
+
+			if err := events.Publish(stream, event.PurgeTrashBin{ExecutionTime: time.Now()}); err != nil {
+				return err
+			}
+
+			// go-micro nats implementation uses async publishing,
+			// therefore we need to manually wait.
+			//
+			// FIXME: upstream pr
+			//
+			// https://github.com/go-micro/plugins/blob/3e77393890683be4bacfb613bc5751867d584692/v4/events/natsjs/nats.go#L115
+			time.Sleep(5 * time.Second)
+
+			return nil
+		},
+	}
+}
diff --git a/services/storage-users/pkg/config/config.go b/services/storage-users/pkg/config/config.go
index 7c636af7254..30e60057aa0 100644
--- a/services/storage-users/pkg/config/config.go
+++ b/services/storage-users/pkg/config/config.go
@@ -2,6 +2,7 @@ package config
 
 import (
 	"context"
+	"time"
 
 	"github.com/owncloud/ocis/v2/ocis-pkg/shared"
 )
@@ -32,6 +33,7 @@ type Config struct {
 	ExposeDataServer bool    `yaml:"expose_data_server" env:"STORAGE_USERS_EXPOSE_DATA_SERVER" desc:"Exposes the data server directly to users and bypasses the data gateway. Ensure that the data server address is reachable by users."`
 	ReadOnly         bool    `yaml:"readonly" env:"STORAGE_USERS_READ_ONLY" desc:"Set this storage to be read-only."`
 	UploadExpiration int64   `yaml:"upload_expiration" env:"STORAGE_USERS_UPLOAD_EXPIRATION" desc:"Duration in seconds after which uploads will expire."`
+	Tasks            Tasks   `yaml:"tasks"`
 
 	Supervised bool            `yaml:"-"`
 	Context    context.Context `yaml:"-"`
@@ -218,3 +220,15 @@ type LocalDriver struct {
 	ShareFolder string `yaml:"share_folder"`
 	UserLayout  string `yaml:"user_layout"`
 }
+
+// Tasks wraps task configurations
+type Tasks struct {
+	PurgeTrashBin PurgeTrashBin `yaml:"purge_trash_bin"`
+}
+
+// PurgeTrashBin contains all necessary configurations to clean up the respective trash cans
+type PurgeTrashBin struct {
+	UserID               string        `yaml:"user_id" env:"OCIS_ADMIN_USER_ID;STORAGE_USERS_PURGE_TRASH_BIN_USER_ID" desc:"ID of the user who collects all necessary information for deletion."`
+	PersonalDeleteBefore time.Duration `yaml:"personal_delete_before" env:"STORAGE_USERS_PURGE_TRASH_BIN_PERSONAL_DELETE_BEFORE" desc:"Specifies the period of time in which items that have been in the personal trash-bin for longer than this value should be deleted. A value of 0 means no automatic deletion"`
+	ProjectDeleteBefore  time.Duration `yaml:"project_delete_before" env:"STORAGE_USERS_PURGE_TRASH_BIN_PROJECT_DELETE_BEFORE" desc:"Specifies the period of time in which items that have been in the project trash-bin for longer than this value should be deleted. A value of 0 means no automatic deletion"`
+}
diff --git a/services/storage-users/pkg/config/defaults/defaultconfig.go b/services/storage-users/pkg/config/defaults/defaultconfig.go
index 83e2d6470f9..6a413d1dae3 100644
--- a/services/storage-users/pkg/config/defaults/defaultconfig.go
+++ b/services/storage-users/pkg/config/defaults/defaultconfig.go
@@ -2,6 +2,7 @@ package defaults
 
 import (
 	"path/filepath"
+	"time"
 
 	"github.com/owncloud/ocis/v2/ocis-pkg/config/defaults"
 	"github.com/owncloud/ocis/v2/ocis-pkg/shared"
@@ -87,6 +88,12 @@ func DefaultConfig() *config.Config {
 			Store:    "memory",
 			Database: "users",
 		},
+		Tasks: config.Tasks{
+			PurgeTrashBin: config.PurgeTrashBin{
+				ProjectDeleteBefore:  30 * 24 * time.Hour,
+				PersonalDeleteBefore: 30 * 24 * time.Hour,
+			},
+		},
 	}
 }
 
@@ -139,6 +146,10 @@ func EnsureDefaults(cfg *config.Config) {
 			cfg.GRPC.TLS.Key = cfg.Commons.GRPCServiceTLS.Key
 		}
 	}
+
+	if cfg.Tasks.PurgeTrashBin.UserID == "" && cfg.Commons != nil {
+		cfg.Tasks.PurgeTrashBin.UserID = cfg.Commons.AdminUserID
+	}
 }
 
 func Sanitize(cfg *config.Config) {
diff --git a/services/storage-users/pkg/event/event.go b/services/storage-users/pkg/event/event.go
new file mode 100644
index 00000000000..bd5273f7dcd
--- /dev/null
+++ b/services/storage-users/pkg/event/event.go
@@ -0,0 +1,51 @@
+package event
+
+import (
+	"crypto/tls"
+	"crypto/x509"
+	"os"
+
+	"github.com/cs3org/reva/v2/pkg/events/stream"
+	"github.com/go-micro/plugins/v4/events/natsjs"
+	ociscrypto "github.com/owncloud/ocis/v2/ocis-pkg/crypto"
+	"github.com/owncloud/ocis/v2/services/storage-users/pkg/config"
+	"go-micro.dev/v4/events"
+)
+
+// NewStream prepares the requested nats stream and returns it.
+func NewStream(cfg config.Events) (events.Stream, error) {
+	var tlsConf *tls.Config
+
+	if cfg.EnableTLS {
+		var rootCAPool *x509.CertPool
+		if cfg.TLSRootCaCertPath != "" {
+			rootCrtFile, err := os.Open(cfg.TLSRootCaCertPath)
+			if err != nil {
+				return nil, err
+			}
+
+			rootCAPool, err = ociscrypto.NewCertPoolFromPEM(rootCrtFile)
+			if err != nil {
+				return nil, err
+			}
+			cfg.TLSInsecure = false
+		}
+
+		tlsConf = &tls.Config{
+			MinVersion: tls.VersionTLS12,
+			RootCAs:    rootCAPool,
+		}
+	}
+
+	s, err := stream.Nats(
+		natsjs.TLSConfig(tlsConf),
+		natsjs.Address(cfg.Addr),
+		natsjs.ClusterID(cfg.ClusterID),
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return s, nil
+}
diff --git a/services/storage-users/pkg/event/service.go b/services/storage-users/pkg/event/service.go
new file mode 100644
index 00000000000..0f78ba7707c
--- /dev/null
+++ b/services/storage-users/pkg/event/service.go
@@ -0,0 +1,85 @@
+package event
+
+import (
+	"time"
+
+	apiGateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
+	apiUser "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
+	"github.com/cs3org/reva/v2/pkg/events"
+	"github.com/owncloud/ocis/v2/ocis-pkg/log"
+	"github.com/owncloud/ocis/v2/services/storage-users/pkg/config"
+	"github.com/owncloud/ocis/v2/services/storage-users/pkg/task"
+)
+
+const (
+	consumerGroup = "storage-users"
+)
+
+// Service wraps all common logic that is needed to react to incoming events.
+type Service struct {
+	gatewayClient apiGateway.GatewayAPIClient
+	eventStream   events.Stream
+	logger        log.Logger
+	config        config.Config
+}
+
+// NewService prepares and returns a Service implementation.
+func NewService(gatewayClient apiGateway.GatewayAPIClient, eventStream events.Stream, logger log.Logger, conf config.Config) (Service, error) {
+	svc := Service{
+		gatewayClient: gatewayClient,
+		eventStream:   eventStream,
+		logger:        logger,
+		config:        conf,
+	}
+
+	return svc, nil
+}
+
+// Run to fulfil Runner interface
+func (s Service) Run() error {
+	ch, err := events.Consume(s.eventStream, consumerGroup, PurgeTrashBin{})
+	if err != nil {
+		return err
+	}
+
+	for e := range ch {
+		var errs []error
+
+		switch ev := e.(type) {
+		case PurgeTrashBin:
+			executionTime := ev.ExecutionTime
+			if executionTime.IsZero() {
+				executionTime = time.Now()
+			}
+
+			executantID := ev.ExecutantID
+			if executantID == nil {
+				executantID = &apiUser.UserId{OpaqueId: s.config.Tasks.PurgeTrashBin.UserID}
+			}
+
+			tasks := map[task.SpaceType]time.Time{
+				task.Project:  executionTime.Add(-s.config.Tasks.PurgeTrashBin.ProjectDeleteBefore),
+				task.Personal: executionTime.Add(-s.config.Tasks.PurgeTrashBin.PersonalDeleteBefore),
+			}
+
+			for spaceType, deleteBefore := range tasks {
+				// skip task execution if the deleteBefore time is the same as the now time,
+				// which indicates that the duration configuration for this space type is set to 0 which is the equivalent to disabled.
+				if deleteBefore.Equal(executionTime) {
+					continue
+				}
+
+				if err = task.PurgeTrashBin(executantID, deleteBefore, spaceType, s.gatewayClient, s.config.Commons.MachineAuthAPIKey); err != nil {
+					errs = append(errs, err)
+				}
+			}
+
+		}
+
+		for _, err := range errs {
+			s.logger.Error().Err(err).Interface("event", e)
+		}
+	}
+
+	return nil
+}
diff --git a/services/storage-users/pkg/event/trigger.go b/services/storage-users/pkg/event/trigger.go
new file mode 100644
index 00000000000..a190f4da4d7
--- /dev/null
+++ b/services/storage-users/pkg/event/trigger.go
@@ -0,0 +1,21 @@
+package event
+
+import (
+	"encoding/json"
+	"time"
+
+	apiUser "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
+)
+
+// PurgeTrashBin wraps all needed information to purge a trash-bin
+type PurgeTrashBin struct {
+	ExecutantID   *apiUser.UserId
+	ExecutionTime time.Time
+}
+
+// Unmarshal to fulfill umarshaller interface
+func (PurgeTrashBin) Unmarshal(v []byte) (interface{}, error) {
+	e := PurgeTrashBin{}
+	err := json.Unmarshal(v, &e)
+	return e, err
+}
diff --git a/services/storage-users/pkg/task/task.go b/services/storage-users/pkg/task/task.go
new file mode 100644
index 00000000000..c1bfe9bb6b4
--- /dev/null
+++ b/services/storage-users/pkg/task/task.go
@@ -0,0 +1,11 @@
+package task
+
+// SpaceType represents known space types
+type SpaceType string
+
+const (
+	// Personal represents a space of type personal
+	Personal SpaceType = "personal"
+	// Project represents a space of type project
+	Project SpaceType = "project"
+)
diff --git a/services/storage-users/pkg/task/trash_bin.go b/services/storage-users/pkg/task/trash_bin.go
new file mode 100644
index 00000000000..354f9cf09d1
--- /dev/null
+++ b/services/storage-users/pkg/task/trash_bin.go
@@ -0,0 +1,112 @@
+package task
+
+import (
+	"fmt"
+	"time"
+
+	apiGateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
+	apiUser "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
+	apiRpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
+	apiProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
+	"github.com/cs3org/reva/v2/pkg/errtypes"
+	"github.com/cs3org/reva/v2/pkg/utils"
+)
+
+// PurgeTrashBin can be used to purge space trash-bin's,
+// the provided executantID must have space access.
+// removeBefore specifies how long an item must be in the trash-bin to be deleted,
+// items that stay there for a shorter time are ignored and kept in place.
+func PurgeTrashBin(executantID *apiUser.UserId, deleteBefore time.Time, spaceType SpaceType, gwc apiGateway.GatewayAPIClient, machineAuthAPIKey string) error {
+	executantCtx, _, err := utils.Impersonate(executantID, gwc, machineAuthAPIKey)
+	if err != nil {
+		return err
+	}
+
+	listStorageSpacesResponse, err := gwc.ListStorageSpaces(executantCtx, &apiProvider.ListStorageSpacesRequest{
+		Filters: []*apiProvider.ListStorageSpacesRequest_Filter{
+			{
+				Type: apiProvider.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE,
+				Term: &apiProvider.ListStorageSpacesRequest_Filter_SpaceType{
+					SpaceType: string(spaceType),
+				},
+			},
+		},
+	})
+	if err != nil {
+		return err
+	}
+
+	for _, storageSpace := range listStorageSpacesResponse.StorageSpaces {
+		var (
+			err                   error
+			impersonationID       *apiUser.UserId
+			storageSpaceReference = &apiProvider.Reference{
+				ResourceId: storageSpace.GetRoot(),
+			}
+		)
+
+		switch SpaceType(storageSpace.GetSpaceType()) {
+		case Personal:
+			impersonationID = storageSpace.GetOwner().GetId()
+		case Project:
+			var permissionsMap map[string]*apiProvider.ResourcePermissions
+			err := utils.ReadJSONFromOpaque(storageSpace.GetOpaque(), "grants", &permissionsMap)
+			if err != nil {
+				break
+			}
+
+			for id, permissions := range permissionsMap {
+				if !permissions.Delete {
+					continue
+				}
+
+				impersonationID = &apiUser.UserId{
+					OpaqueId: id,
+				}
+				break
+			}
+		default:
+			continue
+		}
+
+		if err != nil {
+			return err
+		}
+
+		if impersonationID == nil {
+			return fmt.Errorf("can't impersonate space user for space: %s", storageSpace.GetId().GetOpaqueId())
+		}
+
+		impersonatedCtx, _, err := utils.Impersonate(impersonationID, gwc, machineAuthAPIKey)
+		if err != nil {
+			return err
+		}
+
+		listRecycleResponse, err := gwc.ListRecycle(impersonatedCtx, &apiProvider.ListRecycleRequest{Ref: storageSpaceReference})
+		if err != nil {
+			return err
+		}
+
+		for _, recycleItem := range listRecycleResponse.GetRecycleItems() {
+			doDelete := utils.TSToUnixNano(recycleItem.DeletionTime) < utils.TSToUnixNano(utils.TimeToTS(deleteBefore))
+			if !doDelete {
+				continue
+			}
+
+			purgeRecycleResponse, err := gwc.PurgeRecycle(impersonatedCtx, &apiProvider.PurgeRecycleRequest{
+				Ref: storageSpaceReference,
+				Key: recycleItem.Key,
+			})
+
+			if purgeRecycleResponse.GetStatus().GetCode() != apiRpc.Code_CODE_OK {
+				return errtypes.NewErrtypeFromStatus(purgeRecycleResponse.Status)
+			}
+
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}


Test patch:
diff --git a/docs/ocis/development/testing.md b/docs/ocis/development/testing.md
index fafe6365e34..df76f02e6a7 100644
--- a/docs/ocis/development/testing.md
+++ b/docs/ocis/development/testing.md
@@ -24,11 +24,11 @@ make -C tests/acceptance/docker help
 Basically we have two sources for feature tests and test suites:
 
 - [oCIS feature test and test suites](https://github.com/owncloud/ocis/tree/master/tests/acceptance/features)
-- [ownCloud feature tests and test suites](https://github.com/owncloud/core/tree/master/tests/acceptance/features)
+- [tests and test suites transferred from ownCloud, they have prefix coreApi](https://github.com/owncloud/ocis/tree/master/tests/acceptance/features)
 
 At the moment both can be applied to oCIS since the api of oCIS is designed to be compatible with ownCloud.
 
-Since we have to offer a migration path to existing users of ownCloud, you can use your existing ownCloud as the storage backend for oCIS. As another storage backend we offer oCIS native storage, also called "oCIS". This stores files directly on disk. Which storage backend is used is also reflected in the tests, there are always different tests for oCIS storage and ownCloud storage.
+As a storage backend, we offer oCIS native storage, also called "ocis". This stores files directly on disk. Along with that we also provide `S3` storage driver.
 
 You can invoke two types of test suite runs:
 
@@ -37,18 +37,26 @@ You can invoke two types of test suite runs:
 
 ### Run full test suite
 
-The names of the full test suite make targets have the same naming as in the CI pipeline.
+The names of the full test suite make targets have the same naming as in the CI pipeline. The available local ocis specific test suites are `apiAccountsHashDifficulty`, `apiArchiver`, `apiContract`, `apiGraph`, `apiSpaces`, `apiSpacesShares`, `apiCors`, `apiAsyncUpload`. They can be run with `ocis` storage and `S3` storage.
 
-For example `make -C tests/acceptance/docker localApiTests-apiAccountsHashDifficulty-ocis` runs the same tests as the `localApiTests-apiAccountsHashDifficulty-ocis` CI pipeline, which runs the oCIS test suite "apiAccountsHashDifficulty" against an oCIS with oCIS storage.
+> Note: In order to see the tests log attach `show-test-logs` in the command
 
-For example `make -C tests/acceptance/docker Core-API-Tests-owncloud-storage-3`runs the same tests as the `Core-API-Tests-owncloud-storage-3` CI pipeline, which runs the third (out of ten) ownCloud test suite against an oCIS with owncloud storage.
+For example `make -C tests/acceptance/docker localApiTests-apiAccountsHashDifficulty-ocis` runs the same tests as the `localApiTests-apiAccountsHashDifficulty-ocis` CI pipeline, which runs the oCIS test suite "apiAccountsHashDifficulty" against an oCIS with ocis storage.
+
+For example `make -C tests/acceptance/docker localApiTests-apiAccountsHashDifficulty-s3ng` runs the oCIS test suite "apiAccountsHashDifficulty" against an oCIS with s3 storage.
+
+> Note: To run the tests from `apiAsyncUpload` suite you need to provide extra environment variable `POSTPROCESSING_DELAY`
+
+For example `make -C tests/acceptance/docker Core-API-Tests-ocis-storage-3` runs the same tests as the `Core-API-Tests-ocis-storage-3` CI pipeline, which runs the third (out of ten) test suite transferred from ownCloud against an oCIS with ocis storage.
+
+For example `make -C tests/acceptance/docker Core-API-Tests-s3ng-storage-3` runs the third (out of ten) test suite transferred from ownCloud against an oCIS with s3 storage.
 
 ### Run single feature test
 
 The single feature tests can also be run against the different storage backends. Therefore, multiple make targets with the schema test-<test source>-feature-<storage backend> exist. To select a single feature you have to add an additional `BEHAT_FEATURE=...` parameter when invoking the make command:
 
 ```
-make -C tests/acceptance/docker test-ocis-feature-ocis-storage BEHAT_FEATURE='tests/acceptance/features/apiAccountsHashDifficulty/addUser.feature'
+make -C tests/acceptance/docker test-ocis-feature-ocis-storage BEHAT_FEATURE='tests/acceptance/features/apiAccountsHashDifficulty/addUser.feature:21'
 ```
 
 This must be pointing to a valid feature definition.
@@ -56,8 +64,32 @@ This must be pointing to a valid feature definition.
 To run a single scenario in a feature, then mention the line number of the scenario:
 
 ```
-make -C tests/acceptance/docker test-ocis-feature-ocis-storage BEHAT_FEATURE='tests/acceptance/features/apiAccountsHashDifficulty/addUser.feature:20'
+make -C tests/acceptance/docker test-ocis-feature-ocis-storage BEHAT_FEATURE='tests/acceptance/features/apiAccountsHashDifficulty/addUser.feature:21'
+```
+
+Similarly, with S3 storage,
+- run a whole feature
+```
+make -C tests/acceptance/docker test-ocis-feature-s3ng-storage BEHAT_FEATURE='tests/acceptance/features/apiAccountsHashDifficulty/addUser.feature'
+```
+
+- run a single scenario
 ```
+make -C tests/acceptance/docker test-ocis-feature-s3ng-storage BEHAT_FEATURE='tests/acceptance/features/apiAccountsHashDifficulty/addUser.feature:21'
+```
+
+In the same way for the tests transferred from oc10 can be run as
+- run a whole feature
+```
+make -C tests/acceptance/docker test-core-feature-ocis-storage BEHAT_FEATURE='tests/acceptance/features/coreApiAuth/webDavAuth.feature'
+```
+
+- run a single scenario
+```
+make -C tests/acceptance/docker test-core-feature-ocis-storage BEHAT_FEATURE='tests/acceptance/features/coreApiAuth/webDavAuth.feature:13'
+```
+
+> Note: the tests transferred from oc10 start with coreApi
 
 ### oCIS image to be tested (or: skip build and take existing image)
 
@@ -90,7 +122,7 @@ make -C tests/acceptance/docker clean
 ## Testing with test suite natively installed
 
 We have two sets of tests:
-- `test-acceptance-from-core-api` set was transferred from [core](https://github.com/owncloud/core) repository 
+- `test-acceptance-from-core-api` set was transferred from [core](https://github.com/owncloud/core) repository
 The suite name of all tests transferred from the core starts with "core"
 
 - `test-acceptance-api` set was created for ocis. Mainly for testing spaces features
@@ -133,7 +165,7 @@ TEST_OCIS=true \
 
 Make sure to adjust the settings `TEST_SERVER_URL` according to your environment.
 
-To run a single feature add `BEHAT_FEATURE=<feature file>` 
+To run a single feature add `BEHAT_FEATURE=<feature file>`
 
 example: `BEHAT_SUITE=tests/acceptance/features/apiGraph/createUser.feature`
 
diff --git a/services/storage-users/pkg/task/task_suite_test.go b/services/storage-users/pkg/task/task_suite_test.go
new file mode 100644
index 00000000000..dbcb7996a78
--- /dev/null
+++ b/services/storage-users/pkg/task/task_suite_test.go
@@ -0,0 +1,13 @@
+package task_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+func TestTask(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Task Suite")
+}
diff --git a/services/storage-users/pkg/task/trash_bin_test.go b/services/storage-users/pkg/task/trash_bin_test.go
new file mode 100644
index 00000000000..1da8c231d17
--- /dev/null
+++ b/services/storage-users/pkg/task/trash_bin_test.go
@@ -0,0 +1,230 @@
+package task_test
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"google.golang.org/grpc"
+	"time"
+
+	apiGateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
+	apiUser "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
+	apiRpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
+	apiProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
+	apiTypes "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
+	"github.com/cs3org/reva/v2/pkg/rgrpc/status"
+	"github.com/cs3org/reva/v2/pkg/utils"
+	cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks"
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"github.com/owncloud/ocis/v2/services/storage-users/pkg/task"
+	"github.com/stretchr/testify/mock"
+)
+
+func MustMarshal(v any) []byte {
+	b, err := json.Marshal(v)
+	if err != nil {
+		panic(err)
+	}
+	return b
+}
+
+var _ = Describe("trash", func() {
+	var (
+		gwc                       *cs3mocks.GatewayAPIClient
+		ctx                       context.Context
+		now                       time.Time
+		genericError              error
+		user                      *apiUser.User
+		getUserResponse           *apiUser.GetUserResponse
+		authenticateResponse      *apiGateway.AuthenticateResponse
+		listStorageSpacesResponse *apiProvider.ListStorageSpacesResponse
+		personalSpace             *apiProvider.StorageSpace
+		projectSpace              *apiProvider.StorageSpace
+		virtualSpace              *apiProvider.StorageSpace
+	)
+
+	BeforeEach(func() {
+		gwc = &cs3mocks.GatewayAPIClient{}
+		ctx = context.Background()
+		now = time.Now()
+		genericError = errors.New("any")
+		getUserResponse = &apiUser.GetUserResponse{
+			Status: status.NewOK(ctx),
+		}
+		authenticateResponse = &apiGateway.AuthenticateResponse{
+			Status: status.NewOK(ctx),
+			Token:  "",
+		}
+		listStorageSpacesResponse = &apiProvider.ListStorageSpacesResponse{
+			Status:        status.NewOK(ctx),
+			StorageSpaces: []*apiProvider.StorageSpace{},
+		}
+		personalSpace = &apiProvider.StorageSpace{
+			SpaceType: "personal",
+			Id: &apiProvider.StorageSpaceId{
+				OpaqueId: "personal",
+			},
+			Root: &apiProvider.ResourceId{
+				OpaqueId: "personal",
+			},
+		}
+		projectSpace = &apiProvider.StorageSpace{
+			SpaceType: "project",
+			Id: &apiProvider.StorageSpaceId{
+				OpaqueId: "project",
+			},
+			Root: &apiProvider.ResourceId{
+				OpaqueId: "project",
+			},
+			Opaque: &apiTypes.Opaque{},
+		}
+		// virtual is here as an example,
+		// the task ignores all space types expect `project` and `personal`.
+		virtualSpace = &apiProvider.StorageSpace{
+			SpaceType: "virtual",
+			Id: &apiProvider.StorageSpaceId{
+				OpaqueId: "virtual",
+			},
+			Root: &apiProvider.ResourceId{
+				OpaqueId: "virtual",
+			},
+		}
+		user = &apiUser.User{
+			Id: &apiUser.UserId{
+				OpaqueId: "user",
+			},
+		}
+
+	})
+
+	Describe("PurgeTrashBin", func() {
+		It("throws an error if the user cannot authenticate", func() {
+			gwc.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
+			gwc.On("Authenticate", mock.Anything, mock.Anything).Return(nil, genericError)
+
+			err := task.PurgeTrashBin(user.Id, now, task.Project, gwc, "")
+			Expect(err).To(HaveOccurred())
+		})
+		It("throws an error if space listing fails", func() {
+			gwc.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
+			gwc.On("Authenticate", mock.Anything, mock.Anything).Return(authenticateResponse, nil)
+			gwc.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(nil, genericError)
+
+			err := task.PurgeTrashBin(user.Id, now, task.Project, gwc, "")
+			Expect(err).To(HaveOccurred())
+		})
+		It("throws an error if a personal space user can't be impersonated", func() {
+			listStorageSpacesResponse.StorageSpaces = []*apiProvider.StorageSpace{personalSpace}
+			gwc.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
+			gwc.On("Authenticate", mock.Anything, mock.Anything).Return(authenticateResponse, nil)
+			gwc.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(listStorageSpacesResponse, nil)
+
+			err := task.PurgeTrashBin(user.Id, now, task.Project, gwc, "")
+			Expect(err).To(MatchError(errors.New("can't impersonate space user for space: personal")))
+		})
+		It("throws an error if a project space user can't be impersonated", func() {
+			listStorageSpacesResponse.StorageSpaces = []*apiProvider.StorageSpace{projectSpace}
+			gwc.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
+			gwc.On("Authenticate", mock.Anything, mock.Anything).Return(authenticateResponse, nil)
+			gwc.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(listStorageSpacesResponse, nil)
+
+			err := task.PurgeTrashBin(user.Id, now, task.Project, gwc, "")
+			Expect(err).To(MatchError(errors.New("can't impersonate space user for space: project")))
+		})
+		It("throws an error if a project space has no user with delete permissions", func() {
+			listStorageSpacesResponse.StorageSpaces = []*apiProvider.StorageSpace{projectSpace}
+			projectSpace.Opaque.Map = map[string]*apiTypes.OpaqueEntry{
+				"grants": {
+					Value: MustMarshal(map[string]*apiProvider.ResourcePermissions{
+						"admin": {
+							Delete: false,
+						},
+					}),
+				},
+			}
+			gwc.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
+			gwc.On("Authenticate", mock.Anything, mock.Anything).Return(authenticateResponse, nil)
+			gwc.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(listStorageSpacesResponse, nil)
+
+			err := task.PurgeTrashBin(user.Id, now, task.Project, gwc, "")
+			Expect(err).To(MatchError(errors.New("can't impersonate space user for space: project")))
+		})
+		It("only deletes items older than the specified period", func() {
+			var (
+				recycleItems = map[string][]*apiProvider.RecycleItem{
+					"personal": {
+						{Key: "now", DeletionTime: utils.TimeToTS(now)},
+						{Key: "after", DeletionTime: utils.TimeToTS(now.Add(1 * time.Second))},
+						{Key: "before", DeletionTime: utils.TimeToTS(now.Add(-1 * time.Second))},
+					},
+					"project": {
+						{Key: "now", DeletionTime: utils.TimeToTS(now)},
+						{Key: "after", DeletionTime: utils.TimeToTS(now.Add(1 * time.Minute))},
+						{Key: "before", DeletionTime: utils.TimeToTS(now.Add(-1 * time.Minute))},
+					},
+					"virtual": {
+						{Key: "now", DeletionTime: utils.TimeToTS(now)},
+						{Key: "after", DeletionTime: utils.TimeToTS(now.Add(1 * time.Hour))},
+						{Key: "before", DeletionTime: utils.TimeToTS(now.Add(-1 * time.Hour))},
+					},
+				}
+			)
+
+			personalSpace.Owner = user
+			listStorageSpacesResponse.StorageSpaces = []*apiProvider.StorageSpace{
+				personalSpace,
+				projectSpace,
+				virtualSpace,
+			}
+			projectSpace.Opaque.Map = map[string]*apiTypes.OpaqueEntry{
+				"grants": {
+					Decoder: "json",
+					Value: MustMarshal(map[string]*apiProvider.ResourcePermissions{
+						"admin": {
+							Delete: true,
+						},
+					}),
+				},
+			}
+
+			gwc.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
+			gwc.On("Authenticate", mock.Anything, mock.Anything).Return(authenticateResponse, nil)
+			gwc.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(listStorageSpacesResponse, nil)
+			gwc.On("ListRecycle", mock.Anything, mock.Anything).Return(
+				func(_ context.Context, req *apiProvider.ListRecycleRequest, _ ...grpc.CallOption) *apiProvider.ListRecycleResponse {
+					return &apiProvider.ListRecycleResponse{
+						RecycleItems: recycleItems[req.Ref.ResourceId.OpaqueId],
+					}
+				}, nil,
+			)
+			gwc.On("PurgeRecycle", mock.Anything, mock.Anything).Return(
+				func(_ context.Context, req *apiProvider.PurgeRecycleRequest, _ ...grpc.CallOption) *apiProvider.PurgeRecycleResponse {
+					var items []*apiProvider.RecycleItem
+					for _, item := range recycleItems[req.Ref.ResourceId.OpaqueId] {
+						if req.Key == item.Key {
+							continue
+						}
+
+						items = append(items, item)
+					}
+
+					recycleItems[req.Ref.ResourceId.OpaqueId] = items
+
+					return &apiProvider.PurgeRecycleResponse{
+						Status: &apiRpc.Status{
+							Code: apiRpc.Code_CODE_OK,
+						},
+					}
+				}, nil,
+			)
+
+			err := task.PurgeTrashBin(user.Id, now, task.Project, gwc, "")
+			Expect(err).To(BeNil())
+			Expect(recycleItems["personal"]).To(HaveLen(2))
+			Expect(recycleItems["project"]).To(HaveLen(2))
+			// virtual spaces are ignored
+			Expect(recycleItems["virtual"]).To(HaveLen(3))
+		})
+	})
+})
diff --git a/tests/acceptance/docker/Makefile b/tests/acceptance/docker/Makefile
index 7d23d9913e0..0e746be3c9d 100644
--- a/tests/acceptance/docker/Makefile
+++ b/tests/acceptance/docker/Makefile
@@ -29,6 +29,7 @@ OCIS_IMAGE_TAG ?= dev
 # static
 DIVIDE_INTO_NUM_PARTS := 10
 PARTS = 1 2 3 4 5 6 7 8 9 10
+LOCAL_API_SUITES = $(shell ls ../features | grep ^api*)
 
 COMPOSE_PROJECT_NAME := ocis-acceptance-tests
 
@@ -42,8 +43,8 @@ help:
 	@echo -e "oCIS feature tests and test suites can be found here:"
 	@echo -e "\thttps://github.com/owncloud/ocis/tree/master/tests/acceptance/features"
 	@echo
-	@echo -e "ownCloud feature tests and test suites can be found here:"
-	@echo -e "\thttps://github.com/owncloud/core/tree/master/tests/acceptance/features"
+	@echo -e "test suites that test ownCloud 10 API core compatibility are found here and they start with prefix coreApi-:"
+	@echo -e "\thttps://github.com/owncloud/ocis/tree/master/tests/acceptance/features"
 	@echo
 	@echo -e "The oCIS to be tested will be build from your current working state."
 	@echo -e "You also can select the oCIS Docker image for all tests by setting"
@@ -52,13 +53,10 @@ help:
 	@echo -e "https://hub.docker.com/r/owncloud/ocis."
 	@echo
 	@echo -e "${GREEN}Run full oCIS test suites against oCIS with oCIS storage:${RESET}\n"
-	@echo -e "\tmake localApiTests-apiAccountsHashDifficulty-ocis\t\t${BLUE}run apiAccountsHashDifficulty test suite${RESET}"
+	@echo -e "\tmake localApiTests-apiAccountsHashDifficulty-ocis\t\t${BLUE}run apiAccountsHashDifficulty test suite,  where available test suite are apiAccountsHashDifficulty apiArchiver apiContract apiGraph apiSpaces apiSpacesShares apiAsyncUpload apiCors${RESET}"
 	@echo
 	@echo -e "${GREEN}Run full oCIS test suites against oCIS with s3ng storage:${RESET}\n"
-	@echo -e "\tmake localApiTests-apiAccountsHashDifficulty-s3ng\t\t${BLUE}run apiAccountsHashDifficulty test suite${RESET}"
-	@echo
-	@echo -e "${GREEN}Run full oCIS test suites against oCIS with ownCloud storage:${RESET}\n"
-	@echo -e "\tmake localApiTests-apiAccountsHashDifficulty-owncloud\t\t${BLUE}run apiAccountsHashDifficulty test suite${RESET}"
+	@echo -e "\tmake localApiTests-apiAccountsHashDifficulty-s3ng\t\t${BLUE}run apiAccountsHashDifficulty test suite, where available test suite are apiAccountsHashDifficulty apiArchiver apiContract apiGraph apiSpaces apiSpacesShares apiAsyncUpload apiCors${RESET}"
 	@echo
 	@echo -e "${GREEN}Run full ownCloud test suites against oCIS with oCIS storage:${RESET}\n"
 	@echo -e "\tmake Core-API-Tests-ocis-storage-${RED}X${RESET}\t\t${BLUE}run test suite number X, where ${RED}X = 1 .. 10${RESET}"
@@ -82,19 +80,19 @@ help:
 	@echo -e "\texample: ${RED}tests/acceptance/features/apiAccountsHashDifficulty/addUser.feature${RESET}"
 	@echo
 	@echo -e "${GREEN}Run an ownCloud feature test against oCIS with oCIS storage:${RESET}\n"
-	@echo -e "\tmake test-oc10-feature-ocis-storage ${YELLOW}BEHAT_FEATURE='...'${RESET}\t${BLUE}run single feature test${RESET}"
+	@echo -e "\tmake test-core-feature-ocis-storage ${YELLOW}BEHAT_FEATURE='...'${RESET}\t${BLUE}run single feature test${RESET}"
 	@echo
 	@echo -e "\twhere ${YELLOW}BEHAT_FEATURE='...'${RESET} contains a relative path to the feature definition."
-	@echo -e "\texample: ${RED}tests/acceptance/features/apiAuth/cors.feature${RESET}"
+	@echo -e "\texample: ${RED}tests/acceptance/features/coreApiAuth/webDavAuth.feature${RESET}"
 	@echo
 	@echo -e "${GREEN}Run an ownCloud feature test against oCIS with s3ng storage:${RESET}\n"
-	@echo -e "\tmake test-oc10-feature-s3ng-storage ${YELLOW}BEHAT_FEATURE='...'${RESET}\t${BLUE}run single feature test${RESET}"
+	@echo -e "\tmake test-core-feature-s3ng-storage ${YELLOW}BEHAT_FEATURE='...'${RESET}\t${BLUE}run single feature test${RESET}"
 	@echo
 	@echo -e "\twhere ${YELLOW}BEHAT_FEATURE='...'${RESET} contains a relative path to the feature definition."
-	@echo -e "\texample: ${RED}tests/acceptance/features/apiAuth/cors.feature${RESET}"
+	@echo -e "\texample: ${RED}tests/acceptance/features/coreApiAuth/webDavAuth.feature${RESET}"
 	@echo
 	@echo -e "\twhere ${YELLOW}BEHAT_FEATURE='...'${RESET} contains a relative path to the feature definition."
-	@echo -e "\texample: ${RED}tests/acceptance/features/apiAuth/cors.feature${RESET}"
+	@echo -e "\texample: ${RED}tests/acceptance/features/coreApiAuth/webDavAuth.feature${RESET}"
 	@echo
 	@echo
 	@echo -e "${GREEN}Show output of tests:${RESET}\n"
@@ -121,41 +119,54 @@ test-ocis-feature-s3ng-storage: ## test a ocis feature with s3ng storage, usage:
 	START_CEPH=1 \
 	$(MAKE) --no-print-directory testSuite
 
-.PHONY: test-oc10-feature-ocis-storage
-test-oc10-feature-ocis-storage: ## test a oC10 feature with oCIS storage, usage: make ... BEHAT_FEATURE='tests/acceptance/features/apiAuth/cors.feature'
+.PHONY: test-core-feature-ocis-storage
+test-core-feature-ocis-storage: ## test a core feature with oCIS storage, usage: make ... BEHAT_FEATURE='tests/acceptance/features/coreApiAuth/webDavAuth.feature'
 	@TEST_SOURCE=oc10 \
 	STORAGE_DRIVER=ocis \
 	BEHAT_FEATURE=$(BEHAT_FEATURE) \
 	$(MAKE) --no-print-directory testSuite
 
-.PHONY: test-oc10-feature-s3ng-storage
-test-oc10-feature-s3ng-storage: ## test a oC10 feature with s3ng storage, usage: make ... BEHAT_FEATURE='tests/acceptance/features/apiAuth/cors.feature'
+.PHONY: test-core-feature-s3ng-storage
+test-core-feature-s3ng-storage: ## test a core feature with s3ng storage, usage: make ... BEHAT_FEATURE='tests/acceptance/features/coreApiAuth/webDavAuth.feature'
 	@TEST_SOURCE=oc10 \
 	STORAGE_DRIVER=s3ng \
 	BEHAT_FEATURE=$(BEHAT_FEATURE) \
 	START_CEPH=1 \
 	$(MAKE) --no-print-directory testSuite
 
-.PHONY: localApiTests-apiAccountsHashDifficulty-owncloud
-localApiTests-apiAccountsHashDifficulty-owncloud: ## run apiAccountsHashDifficulty test suite with owncloud storage
+localSuiteOcis = $(addprefix localApiTests-, $(addsuffix -ocis,${LOCAL_API_SUITES}))
+.PHONY: $(localSuiteOcis)
+$(localSuiteOcis): ## run local api test suite with ocis storage
+	@$(eval BEHAT_SUITE=$(shell echo "$@" | cut -d'-' -f2))
 	@TEST_SOURCE=ocis \
-	STORAGE_DRIVER=owncloud \
-	BEHAT_SUITE=apiAccountsHashDifficulty \
+	STORAGE_DRIVER=ocis \
+	BEHAT_SUITE=$(BEHAT_SUITE) \
 	$(MAKE) --no-print-directory testSuite
 
-.PHONY: localApiTests-apiAccountsHashDifficulty-ocis
-localApiTests-apiAccountsHashDifficulty-ocis: ## run apiAccountsHashDifficulty test suite with ocis storage
+localSuites3ng = $(addprefix localApiTests-, $(addsuffix -s3ng,${LOCAL_API_SUITES}))
+.PHONY: $(localSuites3ng)
+$(localSuites3ng): ## run local api test suite with s3 storage
+	@$(eval BEHAT_SUITE=$(shell echo "$@" | cut -d'-' -f2))
 	@TEST_SOURCE=ocis \
+	STORAGE_DRIVER=s3ng \
+	BEHAT_SUITE=$(BEHAT_SUITE) \
+	$(MAKE) --no-print-directory testSuite
+
+targetsOCIS = $(addprefix Core-API-Tests-ocis-storage-,$(PARTS))
+.PHONY: $(targetsOCIS)
+$(targetsOCIS):
+	@$(eval RUN_PART=$(shell echo "$@" | tr -dc '0-9'))
+	@TEST_SOURCE=oc10 \
 	STORAGE_DRIVER=ocis \
-	BEHAT_SUITE=apiAccountsHashDifficulty \
+	RUN_PART=$(RUN_PART) \
 	$(MAKE) --no-print-directory testSuite
 
-targets = $(addprefix Core-API-Tests-ocis-storage-,$(PARTS))
-.PHONY: $(targets)
+targetsS3ng = $(addprefix Core-API-Tests-s3ng-storage-,$(PARTS))
+.PHONY: $(targetsS3ng)
 $(targets):
 	@$(eval RUN_PART=$(shell echo "$@" | tr -dc '0-9'))
 	@TEST_SOURCE=oc10 \
-	STORAGE_DRIVER=ocis \
+	STORAGE_DRIVER=s3ng \
 	RUN_PART=$(RUN_PART) \
 	$(MAKE) --no-print-directory testSuite
 
@@ -220,7 +231,7 @@ clean-docker-volumes: ## clean docker volumes created during acceptance tests
 
 .PHONY: clean-files
 clean-files:
-	@$(MAKE) --no-print-directory -C ../. clean-tests
+	@$(MAKE) --no-print-directory -C ../../../. clean-tests
 
 .PHONY: clean
 clean: clean-docker-container clean-docker-volumes clean-dev-docker-image clean-files ## clean all
diff --git a/tests/acceptance/docker/src/acceptance.yml b/tests/acceptance/docker/src/acceptance.yml
index cdbe3421157..cfec5d1ed44 100644
--- a/tests/acceptance/docker/src/acceptance.yml
+++ b/tests/acceptance/docker/src/acceptance.yml
@@ -4,12 +4,10 @@ services:
     command: /bin/bash /test/run-tests.sh
     environment:
       OCIS_ROOT: /drone/src
-      PATH_TO_CORE: /srv/app/testrunner
-      SKELETON_DIR: /srv/app/tmp/testing/data/apiSkeleton
       TEST_OCIS: "true"
       RUN_ON_OCIS: "true"
       TEST_SERVER_URL: https://ocis-server:9200
-      TESTING_DIR: /srv/app/tmp/testing
+      TEST_WITH_GRAPH_API: "true"
 
       STORAGE_DRIVER: $STORAGE_DRIVER
       TEST_SOURCE: $TEST_SOURCE
@@ -21,11 +19,5 @@ services:
       - ../../../../.drone.env
     volumes:
       - ./run-tests.sh:/test/run-tests.sh
-      - oCISownCloud10testsuite:/srv
       - ../../../../:/drone/src
       - ../../../../vendor-bin/behat/composer.json:/tmp/vendor-bin/behat/composer.json
-      - oCISownCloud10testsuite-vendor:/drone/src/vendor
-
-volumes:
-  oCISownCloud10testsuite:
-  oCISownCloud10testsuite-vendor:
diff --git a/tests/acceptance/docker/src/ocis-base.yml b/tests/acceptance/docker/src/ocis-base.yml
index 8b2f75bddbe..83962f9c0f2 100644
--- a/tests/acceptance/docker/src/ocis-base.yml
+++ b/tests/acceptance/docker/src/ocis-base.yml
@@ -8,12 +8,20 @@ services:
       STORAGE_USERS_DRIVER_LOCAL_ROOT: /srv/app/tmp/ocis/local/root
       STORAGE_USERS_DRIVER_OCIS_ROOT: /srv/app/tmp/ocis/storage/users
       STORAGE_SYSTEM_DRIVER_OCIS_ROOT: /srv/app/tmp/ocis/storage/metadata
-      STORAGE_SHARING_USER_JSON_FILE: /srv/app/tmp/ocis/shares.json
+      SHARING_USER_JSON_FILE: /srv/app/tmp/ocis/shares.json
       SETTINGS_DATA_PATH: "/srv/app/tmp/ocis/settings"
       PROXY_ENABLE_BASIC_AUTH: "true"
       WEB_UI_CONFIG: /drone/src/tests/config/drone/ocis-config.json
       ACCOUNTS_HASH_DIFFICULTY: 4
       OCIS_INSECURE: "true"
+      "IDM_CREATE_DEMO_USERS": "true"
+      "IDM_ADMIN_PASSWORD": "admin"
+      "FRONTEND_SEARCH_MIN_LENGTH": "2"
+
+      "OCIS_CORS_ALLOW_ORIGINS": "https://aphno.badal"
+
+      "POSTPROCESSING_DELAY": ${POSTPROCESSING_DELAY:-0s}
+
       # s3ng specific settings
       STORAGE_USERS_S3NG_ENDPOINT: http://ceph:8080
       STORAGE_USERS_S3NG_REGION: default
@@ -22,7 +30,8 @@ services:
       STORAGE_USERS_S3NG_BUCKET: test
     volumes:
       - ../../../config:/drone/src/tests/config
-      - oCISownCloud10testsuite:/srv
-
-volumes:
-  oCISownCloud10testsuite:
+    entrypoint: /bin/sh
+    # run ocis init to initialize a configuration file with random secrets
+    # it will fail on subsequent runs, because the config file already exists
+    # therefore we ignore the error and then start the ocis server
+    command: ["-c", "ocis init || true; ocis server"]
diff --git a/tests/acceptance/docker/src/run-tests.sh b/tests/acceptance/docker/src/run-tests.sh
index 8b601d6f03a..e61dfb5f1ff 100644
--- a/tests/acceptance/docker/src/run-tests.sh
+++ b/tests/acceptance/docker/src/run-tests.sh
@@ -5,42 +5,21 @@
 
 git config --global advice.detachedHead false
 
-## GET DEPENDENCIES
-
-if cd $TESTING_DIR > /dev/null 2>&1
-then
-    git pull
-else
-    git clone -b master --depth=1 https://github.com/owncloud/testing.git $TESTING_DIR
-fi
-
-if cd $PATH_TO_CORE > /dev/null 2>&1
-then
-    git checkout $CORE_BRANCH
-    git pull
-    git checkout $CORE_COMMITID
-else
-    git clone -b $CORE_BRANCH --single-branch --no-tags https://github.com/owncloud/core.git $PATH_TO_CORE
-    cd $PATH_TO_CORE
-    git checkout $CORE_COMMITID
-fi
-
 ## CONFIGURE TEST
 
 if [ "$TEST_SOURCE" = "oc10" ]
 then
-    if [ "$STORAGE_DRIVER" = "owncloud" ]
-    then
-        export OCIS_REVA_DATA_ROOT='/srv/app/tmp/ocis/owncloud/data/'
-        export BEHAT_FILTER_TAGS='~@notToImplementOnOCIS&&~@toImplementOnOCIS&&~comments-app-required&&~@federation-app-required&&~@notifications-app-required&&~systemtags-app-required&&~@local_storage&&~@skipOnOcis-OC-Storage'
-        export OCIS_SKELETON_STRATEGY='copy'
-        export EXPECTED_FAILURES_FILE='/drone/src/tests/acceptance/expected-failures-API-on-OWNCLOUD-storage.md'
-    elif [ "$STORAGE_DRIVER" = "ocis" ]
+    if [ "$STORAGE_DRIVER" = "ocis" ]
     then
         export OCIS_REVA_DATA_ROOT=''
         export BEHAT_FILTER_TAGS='~@notToImplementOnOCIS&&~@toImplementOnOCIS&&~comments-app-required&&~@federation-app-required&&~@notifications-app-required&&~systemtags-app-required&&~@local_storage&&~@skipOnOcis-OCIS-Storage'
         export OCIS_SKELETON_STRATEGY='upload'
         export EXPECTED_FAILURES_FILE='/drone/src/tests/acceptance/expected-failures-API-on-OCIS-storage.md'
+    elif [ "$STORAGE_DRIVER" = "s3ng" ]
+    then
+        export BEHAT_FILTER_TAGS='~@skip&&~@skipOnOcis-S3NG-Storage'
+        export OCIS_REVA_DATA_ROOT=''
+        export OCIS_SKELETON_STRATEGY='upload'
     else
         echo "non existing STORAGE selected"
         exit 1
@@ -51,12 +30,7 @@ then
 elif [ "$TEST_SOURCE" = "ocis" ]
 then
 
-    if [ "$STORAGE_DRIVER" = "owncloud" ]
-    then
-        export BEHAT_FILTER_TAGS='~@skip&&~@skipOnOcis-OC-Storage'
-        export OCIS_REVA_DATA_ROOT='/srv/app/tmp/ocis/owncloud/data/'
-        export OCIS_SKELETON_STRATEGY='copy'
-    elif [ "$STORAGE_DRIVER" = "ocis" ]
+    if [ "$STORAGE_DRIVER" = "ocis" ]
     then
         export BEHAT_FILTER_TAGS='~@skip&&~@skipOnOcis-OCIS-Storage'
         export OCIS_REVA_DATA_ROOT=''
@@ -95,12 +69,12 @@ fi
 
 if [ "$TEST_SOURCE" = "oc10" ]
 then
-    make -C /srv/app/testrunner test-acceptance-api
+   sleep 10
+   make -C $OCIS_ROOT test-acceptance-from-core-api
 elif [ "$TEST_SOURCE" = "ocis" ]
 then
-    cd $OCIS_ROOT
     sleep 10
-    make test-acceptance-api
+    make -C $OCIS_ROOT test-acceptance-api
 else
     echo "non existing TEST_SOURCE selected"
     exit 1
diff --git a/tests/acceptance/features/apiSpacesShares/shareSpaces.feature b/tests/acceptance/features/apiSpacesShares/shareSpaces.feature
index a52c7727e4e..239105cf9b1 100644
--- a/tests/acceptance/features/apiSpacesShares/shareSpaces.feature
+++ b/tests/acceptance/features/apiSpacesShares/shareSpaces.feature
@@ -233,4 +233,20 @@ Feature: Share spaces
     When the administrator deletes group "group2" using the Graph API
     Then the HTTP status code should be "204"
     And the user "Brian" should not have a space called "share space"
-    And the user "Bob" should not have a space called "share space"
\ No newline at end of file
+    And the user "Bob" should not have a space called "share space"
+
+
+  Scenario Outline: User increases permissions for one member of the group or for the entire group
+    Given group "sales" has been created
+    And the administrator has added a user "Brian" to the group "sales" using GraphApi
+    And user "Alice" has shared a space "share space" to <firstRecipient> with role "viewer"
+    When user "Brian" uploads a file inside space "share space" with content "Test" to "test.txt" using the WebDAV API
+    Then the HTTP status code should be "403"
+    When user "Alice" shares a space "share space" to <secondRecipient> with role "editor"
+    Then the HTTP status code should be "200"
+    When user "Brian" uploads a file inside space "share space" with content "Test" to "test.txt" using the WebDAV API
+    Then the HTTP status code should be "201"
+    Examples:
+      | firstRecipient | secondRecipient |
+      | group "sales"  | user "Brian"    |
+      | user "Brian"   | group "sales"   |
diff --git a/tests/acceptance/features/apiSpacesShares/shareSubItemOfSpace.feature b/tests/acceptance/features/apiSpacesShares/shareSubItemOfSpace.feature
index 8d8a6a01284..e1b25aa6579 100644
--- a/tests/acceptance/features/apiSpacesShares/shareSubItemOfSpace.feature
+++ b/tests/acceptance/features/apiSpacesShares/shareSubItemOfSpace.feature
@@ -126,3 +126,17 @@ Feature: Share a file or folder that is inside a space
     Then the HTTP status code should be "200"
     And the information about the last share for user "Brian" should include
       | expiration | 2044-01-01 |
+
+
+  Scenario: A user deletes the expiration date
+    Given user "Alice" has created a share inside of space "share sub-item" with settings:
+      | path       | folder                   |
+      | shareWith  | Brian                    |
+      | role       | viewer                   |
+      | expireDate | 2042-01-01T23:59:59+0100 |
+    And user "Brian" has accepted share "/folder" offered by user "Alice"
+    When user "Alice" changes the last share with settings:
+      | expireDate |  |
+    Then the HTTP status code should be "200"
+    And the information about the last share for user "Brian" should include
+      | expiration |  |
diff --git a/tests/acceptance/features/bootstrap/SpacesContext.php b/tests/acceptance/features/bootstrap/SpacesContext.php
index 7b7e5deaeaf..3221397ab2e 100644
--- a/tests/acceptance/features/bootstrap/SpacesContext.php
+++ b/tests/acceptance/features/bootstrap/SpacesContext.php
@@ -3278,17 +3278,13 @@ class SpacesContext implements Context {
 		);
 		Assert::assertIsArray($spaceAsArray = $this->getSpaceByNameFromResponse($spaceName), "No space with name $spaceName found");
 		$recipientType === 'user' ? $recipientId = $this->getUserIdByUserName($recipient) : $recipientId = $this->getGroupIdByGroupName($recipient);
-
-		$recipientId = $this->getUserIdByUserName($user);
+		$foundRoleInResponse = false;
 		foreach ($spaceAsArray['root']['permissions'] as $permission) {
-			$foundRoleInResponse = false;
-			if (\array_key_exists($recipientType, $permission['grantedToIdentities'][0])) {
-				if ($permission['roles'][0] === $role || $permission['grantedToIdentities'][0][$recipientType]['id'] === $recipientId) {
-					$foundRoleInResponse = true;
-					break;
-				}
-				Assert::assertTrue($foundRoleInResponse, "the response does not contain the $recipientType $recipient");
+			if (isset($permission['grantedToIdentities'][0][$recipientType]) && $permission['roles'][0] === $role && $permission['grantedToIdentities'][0][$recipientType]['id'] === $recipientId) {
+				$foundRoleInResponse = true;
+				break;
 			}
 		}
+		Assert::assertTrue($foundRoleInResponse, "the response does not contain the $recipientType $recipient");
 	}
 }