-
Notifications
You must be signed in to change notification settings - Fork 206
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
EZP-28143: getPermissionCriterion() should be cached per user (#2126)
* EZP-28143: getPermissionCriterion() should be cached per user * Add Tests
- Loading branch information
Showing
12 changed files
with
1,095 additions
and
247 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
<?php | ||
|
||
/** | ||
* @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\API\Repository; | ||
|
||
/** | ||
* This service provides methods for resolving criterion permissions. | ||
* | ||
* @since 6.7.7 | ||
*/ | ||
interface PermissionCriterionResolver | ||
{ | ||
/** | ||
* Get criteria representation for a permission. | ||
* | ||
* Will return a criteria if current user has limited access to the given module/function, | ||
* however if user has either full or no access then boolean is returned. | ||
* | ||
* @param string $module | ||
* @param string $function | ||
* | ||
* @return bool|\eZ\Publish\API\Repository\Values\Content\Query\Criterion | ||
*/ | ||
public function getPermissionsCriterion($module, $function); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
126 changes: 126 additions & 0 deletions
126
eZ/Publish/Core/Repository/Permission/CachedPermissionService.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
<?php | ||
|
||
/** | ||
* @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\Permission; | ||
|
||
use eZ\Publish\API\Repository\PermissionResolver as APIPermissionResolver; | ||
use eZ\Publish\API\Repository\PermissionCriterionResolver as APIPermissionCriterionResolver; | ||
use eZ\Publish\API\Repository\Repository as RepositoryInterface; | ||
use eZ\Publish\API\Repository\Values\User\UserReference; | ||
use eZ\Publish\API\Repository\Values\ValueObject; | ||
|
||
/** | ||
* Cache implementation of PermissionResolver and PermissionCriterionResolver interface. | ||
* | ||
* Implements both interfaces as the cached permission criterion lookup needs to be | ||
* expired when a different user is set as current users in the system. | ||
* | ||
* Cache is only done for content/read policy, as that is the one needed by search service. | ||
* | ||
* The logic here uses a cache TTL of a few seconds, as this is in-memory cache we are not | ||
* able to know if any other concurrent user might be changing permissions. | ||
*/ | ||
class CachedPermissionService implements APIPermissionResolver, APIPermissionCriterionResolver | ||
{ | ||
/** | ||
* @var \eZ\Publish\API\Repository\PermissionResolver | ||
*/ | ||
private $permissionResolver; | ||
|
||
/** | ||
* @var \eZ\Publish\API\Repository\PermissionCriterionResolver | ||
*/ | ||
private $permissionCriterionResolver; | ||
|
||
/** | ||
* @var int | ||
*/ | ||
private $cacheTTL; | ||
|
||
/** | ||
* Cached value for current user's getCriterion() result. | ||
* | ||
* Value is null if not yet set or cleared. | ||
* | ||
* @var bool|\eZ\Publish\API\Repository\Values\Content\Query\Criterion | ||
*/ | ||
private $permissionCriterion; | ||
|
||
/** | ||
* Cache time stamp. | ||
* | ||
* @var int | ||
*/ | ||
private $permissionCriterionTs; | ||
|
||
/** | ||
* CachedPermissionService constructor. | ||
* | ||
* @param \eZ\Publish\API\Repository\PermissionResolver $permissionResolver | ||
* @param \eZ\Publish\API\Repository\PermissionCriterionResolver $permissionCriterionResolver | ||
* @param int $cacheTTL By default set to 5 seconds, should be low to avoid to many permission exceptions on long running requests / processes (even if tolerant search service should handle that) | ||
*/ | ||
public function __construct( | ||
APIPermissionResolver $permissionResolver, | ||
APIPermissionCriterionResolver $permissionCriterionResolver, | ||
$cacheTTL = 5 | ||
) { | ||
$this->permissionResolver = $permissionResolver; | ||
$this->permissionCriterionResolver = $permissionCriterionResolver; | ||
$this->cacheTTL = $cacheTTL; | ||
} | ||
|
||
public function getCurrentUserReference() | ||
{ | ||
return $this->permissionResolver->getCurrentUserReference(); | ||
} | ||
|
||
public function setCurrentUserReference(UserReference $userReference) | ||
{ | ||
$this->permissionCriterion = null; | ||
|
||
return $this->permissionResolver->setCurrentUserReference($userReference); | ||
} | ||
|
||
public function hasAccess($module, $function, UserReference $userReference = null) | ||
{ | ||
return $this->permissionResolver->hasAccess($module, $function, $userReference); | ||
} | ||
|
||
public function canUser($module, $function, ValueObject $object, array $targets = []) | ||
{ | ||
return $this->permissionResolver->canUser($module, $function, $object, $targets); | ||
} | ||
|
||
public function getPermissionsCriterion($module = 'content', $function = 'read') | ||
{ | ||
// We only cache content/read lookup as those are the once frequently done, and it's only one we can safely | ||
// do that won't harm the system if it becomes stale (but user might experience permissions exceptions if it do) | ||
if ($module !== 'content' || $function !== 'read') { | ||
return $this->permissionCriterionResolver->getPermissionsCriterion($module, $function); | ||
} | ||
|
||
if ($this->permissionCriterion !== null) { | ||
// If we are still within the cache TTL, then return the cached value | ||
if ((time() - $this->permissionCriterionTs) < $this->cacheTTL) { | ||
return $this->permissionCriterion; | ||
} | ||
} | ||
|
||
$this->permissionCriterionTs = time(); | ||
$this->permissionCriterion = $this->permissionCriterionResolver->getPermissionsCriterion($module, $function); | ||
|
||
return $this->permissionCriterion; | ||
} | ||
|
||
/** | ||
* @internal For internal use only, do not depend on this method. | ||
*/ | ||
public function sudo(\Closure $callback, RepositoryInterface $outerRepository) | ||
{ | ||
return $this->permissionResolver->sudo($callback, $outerRepository); | ||
} | ||
} |
139 changes: 139 additions & 0 deletions
139
eZ/Publish/Core/Repository/Permission/PermissionCriterionResolver.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
<?php | ||
|
||
/** | ||
* @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\Permission; | ||
|
||
use eZ\Publish\API\Repository\PermissionCriterionResolver as APIPermissionCriterionResolver; | ||
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\LogicalAnd; | ||
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\LogicalOr; | ||
use eZ\Publish\API\Repository\Values\User\Limitation; | ||
use eZ\Publish\API\Repository\PermissionResolver as PermissionResolverInterface; | ||
use eZ\Publish\Core\Repository\Helper\LimitationService; | ||
use RuntimeException; | ||
|
||
/** | ||
* Implementation of Permissions Criterion Resolver. | ||
*/ | ||
class PermissionCriterionResolver implements APIPermissionCriterionResolver | ||
{ | ||
/** | ||
* @var \eZ\Publish\API\Repository\PermissionResolver | ||
*/ | ||
private $permissionResolver; | ||
|
||
/** | ||
* @var \eZ\Publish\Core\Repository\Helper\LimitationService | ||
*/ | ||
private $limitationService; | ||
|
||
/** | ||
* Constructor. | ||
* | ||
* @param \eZ\Publish\API\Repository\PermissionResolver $permissionResolver | ||
* @param \eZ\Publish\Core\Repository\Helper\LimitationService $limitationService | ||
*/ | ||
public function __construct( | ||
PermissionResolverInterface $permissionResolver, | ||
LimitationService $limitationService | ||
) { | ||
$this->permissionResolver = $permissionResolver; | ||
$this->limitationService = $limitationService; | ||
} | ||
|
||
/** | ||
* Get content-read Permission criteria if needed and return false if no access at all. | ||
* | ||
* @uses \eZ\Publish\API\Repository\PermissionResolver::getCurrentUserReference() | ||
* @uses \eZ\Publish\API\Repository\PermissionResolver::hasAccess() | ||
* | ||
* @throws \RuntimeException If empty array of limitations are provided from hasAccess() | ||
* | ||
* @param string $module | ||
* @param string $function | ||
* | ||
* @return bool|\eZ\Publish\API\Repository\Values\Content\Query\Criterion | ||
*/ | ||
public function getPermissionsCriterion($module = 'content', $function = 'read') | ||
{ | ||
$permissionSets = $this->permissionResolver->hasAccess($module, $function); | ||
if (is_bool($permissionSets)) { | ||
return $permissionSets; | ||
} | ||
|
||
if (empty($permissionSets)) { | ||
throw new RuntimeException("Got an empty array of limitations from hasAccess( '{$module}', '{$function}' )"); | ||
} | ||
|
||
/* | ||
* RoleAssignment is a OR condition, so is policy, while limitations is a AND condition | ||
* | ||
* If RoleAssignment has limitation then policy OR conditions are wrapped in a AND condition with the | ||
* role limitation, otherwise it will be merged into RoleAssignment's OR condition. | ||
*/ | ||
$currentUserRef = $this->permissionResolver->getCurrentUserReference(); | ||
$roleAssignmentOrCriteria = []; | ||
foreach ($permissionSets as $permissionSet) { | ||
// $permissionSet is a RoleAssignment, but in the form of role limitation & role policies hash | ||
$policyOrCriteria = []; | ||
/** | ||
* @var \eZ\Publish\API\Repository\Values\User\Policy | ||
*/ | ||
foreach ($permissionSet['policies'] as $policy) { | ||
$limitations = $policy->getLimitations(); | ||
if ($limitations === '*' || empty($limitations)) { | ||
// Given policy gives full access, optimize away all role policies (but not role limitation if any) | ||
// This should be optimized on create/update of Roles, however we keep this here for bc with older data | ||
$policyOrCriteria = []; | ||
break; | ||
} | ||
|
||
$limitationsAndCriteria = []; | ||
foreach ($limitations as $limitation) { | ||
$type = $this->limitationService->getLimitationType($limitation->getIdentifier()); | ||
$limitationsAndCriteria[] = $type->getCriterion($limitation, $currentUserRef); | ||
} | ||
|
||
$policyOrCriteria[] = isset($limitationsAndCriteria[1]) ? | ||
new LogicalAnd($limitationsAndCriteria) : | ||
$limitationsAndCriteria[0]; | ||
} | ||
|
||
/** | ||
* Apply role limitations if there is one. | ||
* | ||
* @var \eZ\Publish\API\Repository\Values\User\Limitation[] | ||
*/ | ||
if ($permissionSet['limitation'] instanceof Limitation) { | ||
// We need to match both the limitation AND *one* of the policies, aka; roleLimit AND policies(OR) | ||
$type = $this->limitationService->getLimitationType($permissionSet['limitation']->getIdentifier()); | ||
if (!empty($policyOrCriteria)) { | ||
$roleAssignmentOrCriteria[] = new LogicalAnd( | ||
[ | ||
$type->getCriterion($permissionSet['limitation'], $currentUserRef), | ||
isset($policyOrCriteria[1]) ? new LogicalOr($policyOrCriteria) : $policyOrCriteria[0], | ||
] | ||
); | ||
} else { | ||
$roleAssignmentOrCriteria[] = $type->getCriterion($permissionSet['limitation'], $currentUserRef); | ||
} | ||
} elseif (!empty($policyOrCriteria)) { | ||
// Otherwise merge $policyOrCriteria into $roleAssignmentOrCriteria | ||
// There is no role limitation, so any of the policies can globally match in the returned OR criteria | ||
$roleAssignmentOrCriteria = empty($roleAssignmentOrCriteria) ? | ||
$policyOrCriteria : | ||
array_merge($roleAssignmentOrCriteria, $policyOrCriteria); | ||
} | ||
} | ||
|
||
if (empty($roleAssignmentOrCriteria)) { | ||
return false; | ||
} | ||
|
||
return isset($roleAssignmentOrCriteria[1]) ? | ||
new LogicalOr($roleAssignmentOrCriteria) : | ||
$roleAssignmentOrCriteria[0]; | ||
} | ||
} |
Oops, something went wrong.