From 8ad7759195e76a20e7757397ce2931b4d3caeb2d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20R?= <andre.romcke@gmail.com>
Date: Mon, 10 Sep 2018 04:00:18 +0200
Subject: [PATCH] EZP-29474: Fix wrong usage of hasAccess in repository (#2413)

* EZP-29474: Fix wrong usage of hasAccess in repository

Fix wrong usage\* of `hasAccess()` in repository in favor of `canUser()` in order to solve:
- Issues when there are Role Assignment Limitation
- As policies are now extendable, better be prepared in case someone needs to customize and add limitations to any of these functions

I effect this makes Repo more correctly use permissions system, and will avoid quite some unneeded exceptions all across the repo.

There is also two behavour changes _(in terms of api spec)_ which will simplify API usage:
- loadSections(): PR proposes to change this to just filter sections _(preparing it for we/others adding policy limitations on the function, & avoiding issue with role limitation)_
- load user drafts: Same, proposes to filter the returned drafts instead of throwing

\* _Long story short, we forgot about Role limitations + extensibility when coding this, and made shortcuts hard coding that assumption, this PR fixes that._

* [API Doc] Add some api doc to clearify usage.

Also fix CS issue on test.

Other fixes:
* Always use target location on trash restore to properly take subtree limitations into account
* Adapt SubtreeLimitation to abstain on trashed content when no location target is provided (abstain is for for role limitations, means they will be ignored)
---
 .../API/Repository/PermissionResolver.php     |  9 ++--
 .../Core/Limitation/SubtreeLimitationType.php |  9 +++-
 .../Tests/SubtreeLimitationTypeTest.php       | 20 ++++----
 eZ/Publish/Core/Repository/ContentService.php |  1 +
 .../Core/Repository/ContentTypeService.php    | 28 +++++------
 .../Core/Repository/LanguageService.php       | 10 ++--
 .../Core/Repository/ObjectStateService.php    | 14 +++---
 eZ/Publish/Core/Repository/SectionService.php | 49 +++++++++++--------
 .../Tests/Service/Integration/TrashBase.php   | 20 ++++++--
 .../Tests/Service/Mock/UrlWildcardTest.php    | 31 ++++++++----
 eZ/Publish/Core/Repository/TrashService.php   | 22 ++++++---
 .../Core/Repository/URLWildcardService.php    |  2 +-
 12 files changed, 132 insertions(+), 83 deletions(-)

diff --git a/eZ/Publish/API/Repository/PermissionResolver.php b/eZ/Publish/API/Repository/PermissionResolver.php
index 0efb3de963d..030f869d692 100644
--- a/eZ/Publish/API/Repository/PermissionResolver.php
+++ b/eZ/Publish/API/Repository/PermissionResolver.php
@@ -29,10 +29,13 @@ public function getCurrentUserReference();
     public function setCurrentUserReference(UserReference $userReference);
 
     /**
-     * Returns boolean value or an array of limitations describing user's permissions
-     * on the given module and function.
+     * Low level permission function: Returns boolean value, or an array of limitations that user permission depends on.
      *
-     * Note: boolean value describes full access (true) or no access at all (false).
+     * Note: boolean value describes full access (true) or no access at all (false), array can be seen as a maybe..
+     *
+     * WARNING: This is a low level method, if possible strongly prefer to use canUser() as it is able to handle limitations.
+     *          This includes Role Assignment limitations, but also future policy limitations added in kernel,
+     *          or as plain user configuration and/or extending the system.
      *
      * @param string $module The module, aka controller identifier to check permissions on
      * @param string $function The function, aka the controller action to check permissions on
diff --git a/eZ/Publish/Core/Limitation/SubtreeLimitationType.php b/eZ/Publish/Core/Limitation/SubtreeLimitationType.php
index d7730afc92d..47626c2d35a 100644
--- a/eZ/Publish/Core/Limitation/SubtreeLimitationType.php
+++ b/eZ/Publish/Core/Limitation/SubtreeLimitationType.php
@@ -137,10 +137,15 @@ public function evaluate(APILimitationValue $value, APIUserReference $currentUse
 
         // Load locations if no specific placement was provided
         if ($targets === null) {
-            if ($object->published) {
+            // Skip check if content is in trash and no location is provided to check against
+            if ($object->isTrashed()) {
+                return self::ACCESS_ABSTAIN;
+            }
+
+            if ($object->isPublished()) {
                 $targets = $this->persistence->locationHandler()->loadLocationsByContent($object->id);
             } else {
-                // @todo Need support for draft locations to to work correctly
+                // @todo Need support for draft locations to work correctly
                 $targets = $this->persistence->locationHandler()->loadParentLocationsForDraftContent($object->id);
             }
         }
diff --git a/eZ/Publish/Core/Limitation/Tests/SubtreeLimitationTypeTest.php b/eZ/Publish/Core/Limitation/Tests/SubtreeLimitationTypeTest.php
index 4b858693889..31c59bd1c2b 100644
--- a/eZ/Publish/Core/Limitation/Tests/SubtreeLimitationTypeTest.php
+++ b/eZ/Publish/Core/Limitation/Tests/SubtreeLimitationTypeTest.php
@@ -283,7 +283,7 @@ public function providerForTestEvaluate()
         $versionInfoMock
             ->expects($this->once())
             ->method('getContentInfo')
-            ->will($this->returnValue(new ContentInfo(array('published' => true))));
+            ->willReturn(new ContentInfo(['published' => true, 'status' => ContentInfo::STATUS_PUBLISHED]));
 
         $versionInfoMock2 = $this->getMock(
             'eZ\\Publish\\API\\Repository\\Values\\Content\\VersionInfo',
@@ -296,13 +296,13 @@ public function providerForTestEvaluate()
         $versionInfoMock2
             ->expects($this->once())
             ->method('getContentInfo')
-            ->will($this->returnValue(new ContentInfo(array('published' => true))));
+            ->willReturn(new ContentInfo(['published' => true, 'status' => ContentInfo::STATUS_PUBLISHED]));
 
         return array(
             // ContentInfo, with targets, no access
             array(
                 'limitation' => new SubtreeLimitation(),
-                'object' => new ContentInfo(array('published' => true)),
+                'object' => new ContentInfo(['published' => true, 'status' => ContentInfo::STATUS_PUBLISHED]),
                 'targets' => array(new Location()),
                 'persistence' => array(),
                 'expected' => LimitationType::ACCESS_DENIED,
@@ -310,7 +310,7 @@ public function providerForTestEvaluate()
             // ContentInfo, with targets, no access
             array(
                 'limitation' => new SubtreeLimitation(array('limitationValues' => array('/1/2/'))),
-                'object' => new ContentInfo(array('published' => true)),
+                'object' => new ContentInfo(['published' => true, 'status' => ContentInfo::STATUS_PUBLISHED]),
                 'targets' => array(new Location(array('pathString' => '/1/55/'))),
                 'persistence' => array(),
                 'expected' => LimitationType::ACCESS_DENIED,
@@ -318,7 +318,7 @@ public function providerForTestEvaluate()
             // ContentInfo, with targets, with access
             array(
                 'limitation' => new SubtreeLimitation(array('limitationValues' => array('/1/2/'))),
-                'object' => new ContentInfo(array('published' => true)),
+                'object' => new ContentInfo(['published' => true, 'status' => ContentInfo::STATUS_PUBLISHED]),
                 'targets' => array(new Location(array('pathString' => '/1/2/'))),
                 'persistence' => array(),
                 'expected' => LimitationType::ACCESS_GRANTED,
@@ -326,7 +326,7 @@ public function providerForTestEvaluate()
             // ContentInfo, no targets, with access
             array(
                 'limitation' => new SubtreeLimitation(array('limitationValues' => array('/1/2/'))),
-                'object' => new ContentInfo(array('published' => true)),
+                'object' => new ContentInfo(['published' => true, 'status' => ContentInfo::STATUS_PUBLISHED]),
                 'targets' => null,
                 'persistence' => array(new Location(array('pathString' => '/1/2/'))),
                 'expected' => LimitationType::ACCESS_GRANTED,
@@ -334,7 +334,7 @@ public function providerForTestEvaluate()
             // ContentInfo, no targets, no access
             array(
                 'limitation' => new SubtreeLimitation(array('limitationValues' => array('/1/2/', '/1/43/'))),
-                'object' => new ContentInfo(array('published' => true)),
+                'object' => new ContentInfo(['published' => true, 'status' => ContentInfo::STATUS_PUBLISHED]),
                 'targets' => null,
                 'persistence' => array(new Location(array('pathString' => '/1/55/'))),
                 'expected' => LimitationType::ACCESS_DENIED,
@@ -342,7 +342,7 @@ public function providerForTestEvaluate()
             // ContentInfo, no targets, un-published, with access
             array(
                 'limitation' => new SubtreeLimitation(array('limitationValues' => array('/1/2/'))),
-                'object' => new ContentInfo(array('published' => false)),
+                'object' => new ContentInfo(['published' => false, 'status' => ContentInfo::STATUS_DRAFT]),
                 'targets' => null,
                 'persistence' => array(new Location(array('pathString' => '/1/2/'))),
                 'expected' => LimitationType::ACCESS_GRANTED,
@@ -350,7 +350,7 @@ public function providerForTestEvaluate()
             // ContentInfo, no targets, un-published, no access
             array(
                 'limitation' => new SubtreeLimitation(array('limitationValues' => array('/1/2/', '/1/43/'))),
-                'object' => new ContentInfo(array('published' => false)),
+                'object' => new ContentInfo(['published' => false, 'status' => ContentInfo::STATUS_DRAFT]),
                 'targets' => null,
                 'persistence' => array(new Location(array('pathString' => '/1/55/'))),
                 'expected' => LimitationType::ACCESS_DENIED,
@@ -406,7 +406,7 @@ public function providerForTestEvaluate()
             // invalid target
             array(
                 'limitation' => new SubtreeLimitation(),
-                'object' => new ContentInfo(array('published' => true)),
+                'object' => new ContentInfo(['published' => true, 'status' => ContentInfo::STATUS_PUBLISHED]),
                 'targets' => array(new ObjectStateLimitation()),
                 'persistence' => array(),
                 'expected' => LimitationType::ACCESS_ABSTAIN,
diff --git a/eZ/Publish/Core/Repository/ContentService.php b/eZ/Publish/Core/Repository/ContentService.php
index 9506eec41c2..47f7e6336c6 100644
--- a/eZ/Publish/Core/Repository/ContentService.php
+++ b/eZ/Publish/Core/Repository/ContentService.php
@@ -1111,6 +1111,7 @@ public function loadContentDrafts(User $user = null)
         $versionInfoList = array();
         foreach ($spiVersionInfoList as $spiVersionInfo) {
             $versionInfo = $this->domainMapper->buildVersionInfoDomainObject($spiVersionInfo);
+            // @todo: Change this to filter returned drafts by permissions instead of throwing
             if (!$this->repository->canUser('content', 'versionread', $versionInfo)) {
                 throw new UnauthorizedException('content', 'versionread', array('contentId' => $versionInfo->contentInfo->id));
             }
diff --git a/eZ/Publish/Core/Repository/ContentTypeService.php b/eZ/Publish/Core/Repository/ContentTypeService.php
index 765334005e1..828dbe6c9bc 100644
--- a/eZ/Publish/Core/Repository/ContentTypeService.php
+++ b/eZ/Publish/Core/Repository/ContentTypeService.php
@@ -116,7 +116,7 @@ public function __construct(
      */
     public function createContentTypeGroup(ContentTypeGroupCreateStruct  $contentTypeGroupCreateStruct)
     {
-        if ($this->repository->hasAccess('class', 'create') !== true) {
+        if (!$this->repository->canUser('class', 'create', $contentTypeGroupCreateStruct)) {
             throw new UnauthorizedException('ContentType', 'create');
         }
 
@@ -235,7 +235,7 @@ public function loadContentTypeGroups()
      */
     public function updateContentTypeGroup(APIContentTypeGroup $contentTypeGroup, ContentTypeGroupUpdateStruct $contentTypeGroupUpdateStruct)
     {
-        if ($this->repository->hasAccess('class', 'update') !== true) {
+        if (!$this->repository->canUser('class', 'update', $contentTypeGroup)) {
             throw new UnauthorizedException('ContentType', 'update');
         }
 
@@ -298,7 +298,7 @@ public function updateContentTypeGroup(APIContentTypeGroup $contentTypeGroup, Co
      */
     public function deleteContentTypeGroup(APIContentTypeGroup $contentTypeGroup)
     {
-        if ($this->repository->hasAccess('class', 'delete') !== true) {
+        if (!$this->repository->canUser('class', 'delete', $contentTypeGroup)) {
             throw new UnauthorizedException('ContentType', 'delete');
         }
 
@@ -655,7 +655,7 @@ protected function validateInputFieldDefinitionCreateStruct(
      */
     public function createContentType(APIContentTypeCreateStruct $contentTypeCreateStruct, array $contentTypeGroups)
     {
-        if ($this->repository->hasAccess('class', 'create') !== true) {
+        if (!$this->repository->canUser('class', 'create', $contentTypeCreateStruct, $contentTypeGroups)) {
             throw new UnauthorizedException('ContentType', 'create');
         }
 
@@ -1022,7 +1022,7 @@ function ($id) {
      */
     public function createContentTypeDraft(APIContentType $contentType)
     {
-        if ($this->repository->hasAccess('class', 'create') !== true) {
+        if (!$this->repository->canUser('class', 'create', $contentType)) {
             throw new UnauthorizedException('ContentType', 'create');
         }
 
@@ -1075,7 +1075,7 @@ function ($id) {
      */
     public function updateContentTypeDraft(APIContentTypeDraft $contentTypeDraft, ContentTypeUpdateStruct $contentTypeUpdateStruct)
     {
-        if ($this->repository->hasAccess('class', 'update') !== true) {
+        if (!$this->repository->canUser('class', 'update', $contentTypeDraft)) {
             throw new UnauthorizedException('ContentType', 'update');
         }
 
@@ -1149,7 +1149,7 @@ public function updateContentTypeDraft(APIContentTypeDraft $contentTypeDraft, Co
      */
     public function deleteContentType(APIContentType $contentType)
     {
-        if ($this->repository->hasAccess('class', 'delete') !== true) {
+        if (!$this->repository->canUser('class', 'delete', $contentType)) {
             throw new UnauthorizedException('ContentType', 'delete');
         }
 
@@ -1189,7 +1189,7 @@ public function deleteContentType(APIContentType $contentType)
      */
     public function copyContentType(APIContentType $contentType, User $creator = null)
     {
-        if ($this->repository->hasAccess('class', 'create') !== true) {
+        if (!$this->repository->canUser('class', 'create', $contentType)) {
             throw new UnauthorizedException('ContentType', 'create');
         }
 
@@ -1224,7 +1224,7 @@ public function copyContentType(APIContentType $contentType, User $creator = nul
      */
     public function assignContentTypeGroup(APIContentType $contentType, APIContentTypeGroup $contentTypeGroup)
     {
-        if ($this->repository->hasAccess('class', 'update') !== true) {
+        if (!$this->repository->canUser('class', 'update', $contentType)) {
             throw new UnauthorizedException('ContentType', 'update');
         }
 
@@ -1266,7 +1266,7 @@ public function assignContentTypeGroup(APIContentType $contentType, APIContentTy
      */
     public function unassignContentTypeGroup(APIContentType $contentType, APIContentTypeGroup $contentTypeGroup)
     {
-        if ($this->repository->hasAccess('class', 'update') !== true) {
+        if (!$this->repository->canUser('class', 'update', $contentType, [$contentTypeGroup])) {
             throw new UnauthorizedException('ContentType', 'update');
         }
 
@@ -1322,7 +1322,7 @@ public function unassignContentTypeGroup(APIContentType $contentType, APIContent
      */
     public function addFieldDefinition(APIContentTypeDraft $contentTypeDraft, FieldDefinitionCreateStruct $fieldDefinitionCreateStruct)
     {
-        if ($this->repository->hasAccess('class', 'update') !== true) {
+        if (!$this->repository->canUser('class', 'update', $contentTypeDraft)) {
             throw new UnauthorizedException('ContentType', 'update');
         }
 
@@ -1395,7 +1395,7 @@ public function addFieldDefinition(APIContentTypeDraft $contentTypeDraft, FieldD
      */
     public function removeFieldDefinition(APIContentTypeDraft $contentTypeDraft, APIFieldDefinition $fieldDefinition)
     {
-        if ($this->repository->hasAccess('class', 'update') !== true) {
+        if (!$this->repository->canUser('class', 'update', $contentTypeDraft)) {
             throw new UnauthorizedException('ContentType', 'update');
         }
 
@@ -1439,7 +1439,7 @@ public function removeFieldDefinition(APIContentTypeDraft $contentTypeDraft, API
      */
     public function updateFieldDefinition(APIContentTypeDraft $contentTypeDraft, APIFieldDefinition $fieldDefinition, FieldDefinitionUpdateStruct $fieldDefinitionUpdateStruct)
     {
-        if ($this->repository->hasAccess('class', 'update') !== true) {
+        if (!$this->repository->canUser('class', 'update', $contentTypeDraft)) {
             throw new UnauthorizedException('ContentType', 'update');
         }
 
@@ -1494,7 +1494,7 @@ public function updateFieldDefinition(APIContentTypeDraft $contentTypeDraft, API
      */
     public function publishContentTypeDraft(APIContentTypeDraft $contentTypeDraft)
     {
-        if ($this->repository->hasAccess('class', 'update') !== true) {
+        if (!$this->repository->canUser('class', 'update', $contentTypeDraft)) {
             throw new UnauthorizedException('ContentType', 'update');
         }
 
diff --git a/eZ/Publish/Core/Repository/LanguageService.php b/eZ/Publish/Core/Repository/LanguageService.php
index 339b716b5b8..4e6b9a82012 100644
--- a/eZ/Publish/Core/Repository/LanguageService.php
+++ b/eZ/Publish/Core/Repository/LanguageService.php
@@ -83,7 +83,7 @@ public function createLanguage(LanguageCreateStruct $languageCreateStruct)
             throw new InvalidArgumentValue('enabled', $languageCreateStruct->enabled, 'LanguageCreateStruct');
         }
 
-        if ($this->repository->hasAccess('content', 'translations') !== true) {
+        if (!$this->repository->canUser('content', 'translations', $languageCreateStruct)) {
             throw new UnauthorizedException('content', 'translations');
         }
 
@@ -133,7 +133,7 @@ public function updateLanguageName(Language $language, $newName)
             throw new InvalidArgumentValue('newName', $newName);
         }
 
-        if ($this->repository->hasAccess('content', 'translations') !== true) {
+        if (!$this->repository->canUser('content', 'translations', $language)) {
             throw new UnauthorizedException('content', 'translations');
         }
 
@@ -171,7 +171,7 @@ public function updateLanguageName(Language $language, $newName)
      */
     public function enableLanguage(Language $language)
     {
-        if ($this->repository->hasAccess('content', 'translations') !== true) {
+        if (!$this->repository->canUser('content', 'translations', $language)) {
             throw new UnauthorizedException('content', 'translations');
         }
 
@@ -209,7 +209,7 @@ public function enableLanguage(Language $language)
      */
     public function disableLanguage(Language $language)
     {
-        if ($this->repository->hasAccess('content', 'translations') !== true) {
+        if (!$this->repository->canUser('content', 'translations', $language)) {
             throw new UnauthorizedException('content', 'translations');
         }
 
@@ -303,7 +303,7 @@ public function loadLanguageById($languageId)
      */
     public function deleteLanguage(Language $language)
     {
-        if ($this->repository->hasAccess('content', 'translations') !== true) {
+        if (!$this->repository->canUser('content', 'translations', $language)) {
             throw new UnauthorizedException('content', 'translations');
         }
 
diff --git a/eZ/Publish/Core/Repository/ObjectStateService.php b/eZ/Publish/Core/Repository/ObjectStateService.php
index 358ff2d63c0..96bf9a3428e 100644
--- a/eZ/Publish/Core/Repository/ObjectStateService.php
+++ b/eZ/Publish/Core/Repository/ObjectStateService.php
@@ -81,7 +81,7 @@ public function __construct(RepositoryInterface $repository, Handler $objectStat
      */
     public function createObjectStateGroup(ObjectStateGroupCreateStruct $objectStateGroupCreateStruct)
     {
-        if ($this->repository->hasAccess('state', 'administrate') !== true) {
+        if (!$this->repository->canUser('state', 'administrate', $objectStateGroupCreateStruct)) {
             throw new UnauthorizedException('state', 'administrate');
         }
 
@@ -182,7 +182,7 @@ public function loadObjectStates(APIObjectStateGroup $objectStateGroup)
      */
     public function updateObjectStateGroup(APIObjectStateGroup $objectStateGroup, ObjectStateGroupUpdateStruct $objectStateGroupUpdateStruct)
     {
-        if ($this->repository->hasAccess('state', 'administrate') !== true) {
+        if (!$this->repository->canUser('state', 'administrate', $objectStateGroup)) {
             throw new UnauthorizedException('state', 'administrate');
         }
 
@@ -234,7 +234,7 @@ public function updateObjectStateGroup(APIObjectStateGroup $objectStateGroup, Ob
      */
     public function deleteObjectStateGroup(APIObjectStateGroup $objectStateGroup)
     {
-        if ($this->repository->hasAccess('state', 'administrate') !== true) {
+        if (!$this->repository->canUser('state', 'administrate', $objectStateGroup)) {
             throw new UnauthorizedException('state', 'administrate');
         }
 
@@ -266,7 +266,7 @@ public function deleteObjectStateGroup(APIObjectStateGroup $objectStateGroup)
      */
     public function createObjectState(APIObjectStateGroup $objectStateGroup, ObjectStateCreateStruct $objectStateCreateStruct)
     {
-        if ($this->repository->hasAccess('state', 'administrate') !== true) {
+        if (!$this->repository->canUser('state', 'administrate', $objectStateCreateStruct, [$objectStateGroup])) {
             throw new UnauthorizedException('state', 'administrate');
         }
 
@@ -340,7 +340,7 @@ public function loadObjectState($stateId)
      */
     public function updateObjectState(APIObjectState $objectState, ObjectStateUpdateStruct $objectStateUpdateStruct)
     {
-        if ($this->repository->hasAccess('state', 'administrate') !== true) {
+        if (!$this->repository->canUser('state', 'administrate', $objectState)) {
             throw new UnauthorizedException('state', 'administrate');
         }
 
@@ -401,7 +401,7 @@ public function setPriorityOfObjectState(APIObjectState $objectState, $priority)
             throw new InvalidArgumentValue('priority', $priority);
         }
 
-        if ($this->repository->hasAccess('state', 'administrate') !== true) {
+        if (!$this->repository->canUser('state', 'administrate', $objectState)) {
             throw new UnauthorizedException('state', 'administrate');
         }
 
@@ -430,7 +430,7 @@ public function setPriorityOfObjectState(APIObjectState $objectState, $priority)
      */
     public function deleteObjectState(APIObjectState $objectState)
     {
-        if ($this->repository->hasAccess('state', 'administrate') !== true) {
+        if (!$this->repository->canUser('state', 'administrate', $objectState)) {
             throw new UnauthorizedException('state', 'administrate');
         }
 
diff --git a/eZ/Publish/Core/Repository/SectionService.php b/eZ/Publish/Core/Repository/SectionService.php
index a83395b66b2..956f27297f2 100644
--- a/eZ/Publish/Core/Repository/SectionService.php
+++ b/eZ/Publish/Core/Repository/SectionService.php
@@ -33,6 +33,11 @@ class SectionService implements SectionServiceInterface
      */
     protected $repository;
 
+    /**
+     * @var \eZ\Publish\API\Repository\PermissionResolver
+     */
+    protected $permissionResolver;
+
     /**
      * @var \eZ\Publish\SPI\Persistence\Content\Section\Handler
      */
@@ -53,6 +58,7 @@ class SectionService implements SectionServiceInterface
     public function __construct(RepositoryInterface $repository, Handler $sectionHandler, array $settings = array())
     {
         $this->repository = $repository;
+        $this->permissionResolver = $repository->getPermissionResolver();
         $this->sectionHandler = $sectionHandler;
         // Union makes sure default settings are ignored if provided in argument
         $this->settings = $settings + array(
@@ -80,7 +86,7 @@ public function createSection(SectionCreateStruct $sectionCreateStruct)
             throw new InvalidArgumentValue('identifier', $sectionCreateStruct->identifier, 'SectionCreateStruct');
         }
 
-        if ($this->repository->hasAccess('section', 'edit') !== true) {
+        if (!$this->permissionResolver->canUser('section', 'edit', $sectionCreateStruct)) {
             throw new UnauthorizedException('section', 'edit');
         }
 
@@ -129,7 +135,7 @@ public function updateSection(Section $section, SectionUpdateStruct $sectionUpda
             throw new InvalidArgumentValue('identifier', $section->identifier, 'Section');
         }
 
-        if ($this->repository->canUser('section', 'edit', $section) !== true) {
+        if (!$this->permissionResolver->canUser('section', 'edit', $section)) {
             throw new UnauthorizedException('section', 'edit');
         }
 
@@ -176,13 +182,15 @@ public function updateSection(Section $section, SectionUpdateStruct $sectionUpda
      */
     public function loadSection($sectionId)
     {
-        if ($this->repository->hasAccess('section', 'view') !== true) {
+        $section = $this->buildDomainSectionObject(
+            $this->sectionHandler->load($sectionId)
+        );
+
+        if (!$this->permissionResolver->canUser('section', 'view', $section)) {
             throw new UnauthorizedException('section', 'view');
         }
 
-        $spiSection = $this->sectionHandler->load($sectionId);
-
-        return $this->buildDomainSectionObject($spiSection);
+        return $section;
     }
 
     /**
@@ -194,15 +202,14 @@ public function loadSection($sectionId)
      */
     public function loadSections()
     {
-        if ($this->repository->hasAccess('section', 'view') !== true) {
-            throw new UnauthorizedException('section', 'view');
-        }
-
-        $spiSections = $this->sectionHandler->loadAll();
+        $sections = [];
+        foreach ($this->sectionHandler->loadAll() as $spiSection) {
+            $sections[] = $section = $this->buildDomainSectionObject($spiSection);
 
-        $sections = array();
-        foreach ($spiSections as $spiSection) {
-            $sections[] = $this->buildDomainSectionObject($spiSection);
+            // @todo change API to just filter instead of throwing here
+            if (!$this->permissionResolver->canUser('section', 'view', $section)) {
+                throw new UnauthorizedException('section', 'view');
+            }
         }
 
         return $sections;
@@ -224,13 +231,15 @@ public function loadSectionByIdentifier($sectionIdentifier)
             throw new InvalidArgumentValue('sectionIdentifier', $sectionIdentifier);
         }
 
-        if ($this->repository->hasAccess('section', 'view') !== true) {
+        $section = $this->buildDomainSectionObject(
+            $this->sectionHandler->loadByIdentifier($sectionIdentifier)
+        );
+
+        if (!$this->permissionResolver->canUser('section', 'view', $section)) {
             throw new UnauthorizedException('section', 'view');
         }
 
-        $spiSection = $this->sectionHandler->loadByIdentifier($sectionIdentifier);
-
-        return $this->buildDomainSectionObject($spiSection);
+        return $section;
     }
 
     /**
@@ -279,7 +288,7 @@ public function assignSection(ContentInfo $contentInfo, Section $section)
         $loadedContentInfo = $this->repository->getContentService()->loadContentInfo($contentInfo->id);
         $loadedSection = $this->loadSection($section->id);
 
-        if ($this->repository->canUser('section', 'assign', $loadedContentInfo, $loadedSection) !== true) {
+        if (!$this->permissionResolver->canUser('section', 'assign', $loadedContentInfo, [$loadedSection])) {
             throw new UnauthorizedException(
                 'section',
                 'assign',
@@ -318,7 +327,7 @@ public function deleteSection(Section $section)
     {
         $loadedSection = $this->loadSection($section->id);
 
-        if ($this->repository->canUser('section', 'edit', $loadedSection) !== true) {
+        if (!$this->permissionResolver->canUser('section', 'edit', $loadedSection)) {
             throw new UnauthorizedException('section', 'edit', array('sectionId' => $loadedSection->id));
         }
 
diff --git a/eZ/Publish/Core/Repository/Tests/Service/Integration/TrashBase.php b/eZ/Publish/Core/Repository/Tests/Service/Integration/TrashBase.php
index cc1d66fc7d8..735bdd32d2f 100644
--- a/eZ/Publish/Core/Repository/Tests/Service/Integration/TrashBase.php
+++ b/eZ/Publish/Core/Repository/Tests/Service/Integration/TrashBase.php
@@ -8,6 +8,7 @@
  */
 namespace eZ\Publish\Core\Repository\Tests\Service\Integration;
 
+use eZ\Publish\API\Repository\Values\Content\ContentInfo;
 use eZ\Publish\Core\Repository\Tests\Service\Integration\Base as BaseServiceTest;
 use eZ\Publish\Core\Repository\Values\Content\TrashItem;
 use eZ\Publish\Core\Repository\Values\Content\Location;
@@ -288,8 +289,9 @@ public function testRecoverNonExistingTrashItem()
     {
         $trashService = $this->repository->getTrashService();
 
-        $trashItem = new TrashItem(array('id' => APIBaseTest::DB_INT_MAX, 'parentLocationId' => APIBaseTest::DB_INT_MAX));
-        $trashService->recover($trashItem);
+        $trashService->recover(
+            $this->getTrashItem(APIBaseTest::DB_INT_MAX, 2, APIBaseTest::DB_INT_MAX)
+        );
     }
 
     /**
@@ -398,8 +400,9 @@ public function testDeleteNonExistingTrashItem()
     {
         $trashService = $this->repository->getTrashService();
 
-        $trashItem = new TrashItem(array('id' => APIBaseTest::DB_INT_MAX));
-        $trashService->deleteTrashItem($trashItem);
+        $trashService->deleteTrashItem(
+            $this->getTrashItem(APIBaseTest::DB_INT_MAX)
+        );
     }
 
     /**
@@ -472,4 +475,13 @@ protected function createTestContentLocation($parentLocationId)
             )->versionInfo
         );
     }
+
+    private function getTrashItem($id, $contentId = 44, $parentLocationId = 2)
+    {
+        return new TrashItem([
+            'id' => $id,
+            'parentLocationId' => $parentLocationId,
+            'contentInfo' => new ContentInfo(['id' => $contentId]),
+        ]);
+    }
 }
diff --git a/eZ/Publish/Core/Repository/Tests/Service/Mock/UrlWildcardTest.php b/eZ/Publish/Core/Repository/Tests/Service/Mock/UrlWildcardTest.php
index 8b1b0616666..00c4503fa6e 100644
--- a/eZ/Publish/Core/Repository/Tests/Service/Mock/UrlWildcardTest.php
+++ b/eZ/Publish/Core/Repository/Tests/Service/Mock/UrlWildcardTest.php
@@ -6,7 +6,7 @@
  * @copyright Copyright (C) eZ Systems AS. All rights reserved.
  * @license For full copyright and license information view LICENSE file distributed with this source code.
  */
-namespace eZ\Publish\Core\Repository\Tests\Service\Integration;
+namespace eZ\Publish\Core\Repository\Tests\Service\Mock;
 
 use eZ\Publish\Core\Repository\Tests\Service\Mock\Base as BaseServiceMockTest;
 use eZ\Publish\API\Repository\Values\Content\URLWildcard;
@@ -295,20 +295,25 @@ public function testCreateWithRollback()
      */
     public function testRemoveThrowsUnauthorizedException()
     {
+        $wildcard = new URLWildcard(['id' => 'McBoom']);
+
         $mockedService = $this->getPartlyMockedURLWildcardService();
         $repositoryMock = $this->getRepositoryMock();
         $repositoryMock->expects(
             $this->once()
         )->method(
-            'hasAccess'
+            'canUser'
         )->with(
             $this->equalTo('content'),
-            $this->equalTo('urltranslator')
+            $this->equalTo('urltranslator'),
+            $this->equalTo($wildcard)
         )->will(
             $this->returnValue(false)
         );
 
-        $mockedService->remove(new URLWildcard());
+        $repositoryMock->expects($this->never())->method('beginTransaction');
+
+        $mockedService->remove($wildcard);
     }
 
     /**
@@ -319,6 +324,8 @@ public function testRemoveThrowsUnauthorizedException()
      */
     public function testRemove()
     {
+        $wildcard = new URLWildcard(['id' => 'McBomb']);
+
         $mockedService = $this->getPartlyMockedURLWildcardService();
         /** @var \PHPUnit_Framework_MockObject_MockObject $handlerMock */
         $handlerMock = $this->getPersistenceMock()->urlWildcardHandler();
@@ -327,10 +334,11 @@ public function testRemove()
         $repositoryMock->expects(
             $this->once()
         )->method(
-            'hasAccess'
+            'canUser'
         )->with(
             $this->equalTo('content'),
-            $this->equalTo('urltranslator')
+            $this->equalTo('urltranslator'),
+            $this->equalTo($wildcard)
         )->will(
             $this->returnValue(true)
         );
@@ -346,7 +354,7 @@ public function testRemove()
             $this->equalTo('McBomb')
         );
 
-        $mockedService->remove(new URLWildcard(array('id' => 'McBomb')));
+        $mockedService->remove($wildcard);
     }
 
     /**
@@ -358,6 +366,8 @@ public function testRemove()
      */
     public function testRemoveWithRollback()
     {
+        $wildcard = new URLWildcard(['id' => 'McBoo']);
+
         $mockedService = $this->getPartlyMockedURLWildcardService();
         /** @var \PHPUnit_Framework_MockObject_MockObject $handlerMock */
         $handlerMock = $this->getPersistenceMock()->urlWildcardHandler();
@@ -366,10 +376,11 @@ public function testRemoveWithRollback()
         $repositoryMock->expects(
             $this->once()
         )->method(
-            'hasAccess'
+            'canUser'
         )->with(
             $this->equalTo('content'),
-            $this->equalTo('urltranslator')
+            $this->equalTo('urltranslator'),
+            $this->equalTo($wildcard)
         )->will(
             $this->returnValue(true)
         );
@@ -387,7 +398,7 @@ public function testRemoveWithRollback()
             $this->throwException(new \Exception())
         );
 
-        $mockedService->remove(new URLWildcard(array('id' => 'McBoo')));
+        $mockedService->remove($wildcard);
     }
 
     /**
diff --git a/eZ/Publish/Core/Repository/TrashService.php b/eZ/Publish/Core/Repository/TrashService.php
index d7efaeea01e..7cd1728870a 100644
--- a/eZ/Publish/Core/Repository/TrashService.php
+++ b/eZ/Publish/Core/Repository/TrashService.php
@@ -86,16 +86,16 @@ public function __construct(
      */
     public function loadTrashItem($trashItemId)
     {
-        if ($this->repository->hasAccess('content', 'restore') !== true) {
-            throw new UnauthorizedException('content', 'restore');
-        }
-
         $spiTrashItem = $this->persistenceHandler->trashHandler()->loadTrashItem($trashItemId);
         $trash = $this->buildDomainTrashItemObject($spiTrashItem);
         if (!$this->repository->canUser('content', 'read', $trash->getContentInfo())) {
             throw new UnauthorizedException('content', 'read');
         }
 
+        if (!$this->repository->canUser('content', 'restore', $trash->getContentInfo())) {
+            throw new UnauthorizedException('content', 'restore');
+        }
+
         return $trash;
     }
 
@@ -117,7 +117,7 @@ public function trash(Location $location)
             throw new InvalidArgumentValue('id', $location->id, 'Location');
         }
 
-        if (!$this->repository->canUser('content', 'remove', $location->getContentInfo(), $location)) {
+        if (!$this->repository->canUser('content', 'remove', $location->getContentInfo(), [$location])) {
             throw new UnauthorizedException('content', 'remove');
         }
 
@@ -171,7 +171,12 @@ public function recover(APITrashItem $trashItem, Location $newParentLocation = n
             throw new InvalidArgumentValue('parentLocationId', $newParentLocation->id, 'Location');
         }
 
-        if ($this->repository->hasAccess('content', 'restore') !== true) {
+        if (!$this->repository->canUser(
+            'content',
+            'restore',
+            $trashItem->getContentInfo(),
+            [$newParentLocation ?: $trashItem]
+        )) {
             throw new UnauthorizedException('content', 'restore');
         }
 
@@ -216,6 +221,9 @@ public function recover(APITrashItem $trashItem, Location $newParentLocation = n
      */
     public function emptyTrash()
     {
+        // Will throw if you have Role assignment limitation where you have content/cleantrash permission.
+        // This is by design and means you can only delete one and one trash item, or you'll need to change how
+        // permissions is assigned to be on separate role with no Role assignment limitation.
         if ($this->repository->hasAccess('content', 'cleantrash') !== true) {
             throw new UnauthorizedException('content', 'cleantrash');
         }
@@ -242,7 +250,7 @@ public function emptyTrash()
      */
     public function deleteTrashItem(APITrashItem $trashItem)
     {
-        if ($this->repository->hasAccess('content', 'cleantrash') !== true) {
+        if (!$this->repository->canUser('content', 'cleantrash', $trashItem->getContentInfo())) {
             throw new UnauthorizedException('content', 'cleantrash');
         }
 
diff --git a/eZ/Publish/Core/Repository/URLWildcardService.php b/eZ/Publish/Core/Repository/URLWildcardService.php
index 08701028974..cc8d30bd698 100644
--- a/eZ/Publish/Core/Repository/URLWildcardService.php
+++ b/eZ/Publish/Core/Repository/URLWildcardService.php
@@ -139,7 +139,7 @@ protected function cleanUrl($url)
      */
     public function remove(URLWildcard $urlWildcard)
     {
-        if ($this->repository->hasAccess('content', 'urltranslator') !== true) {
+        if (!$this->repository->canUser('content', 'urltranslator', $urlWildcard)) {
             throw new UnauthorizedException('content', 'urltranslator');
         }