Skip to content

Commit

Permalink
EZP-28143: getPermissionCriterion() should be cached per user (#2126)
Browse files Browse the repository at this point in the history
* EZP-28143: getPermissionCriterion() should be cached per user

* Add Tests
  • Loading branch information
andrerom committed Nov 8, 2017
1 parent fb7179d commit 1ba7447
Show file tree
Hide file tree
Showing 12 changed files with 1,095 additions and 247 deletions.
28 changes: 28 additions & 0 deletions eZ/Publish/API/Repository/PermissionCriterionResolver.php
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);
}
17 changes: 9 additions & 8 deletions eZ/Publish/Core/Repository/LocationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/
namespace eZ\Publish\Core\Repository;

use eZ\Publish\API\Repository\PermissionCriterionResolver;
use eZ\Publish\API\Repository\Values\Content\LocationUpdateStruct;
use eZ\Publish\API\Repository\Values\Content\LocationCreateStruct;
use eZ\Publish\API\Repository\Values\Content\ContentInfo;
Expand Down Expand Up @@ -63,9 +64,9 @@ class LocationService implements LocationServiceInterface
protected $nameSchemaService;

/**
* @var \eZ\Publish\Core\Repository\PermissionsCriterionHandler
* @var \eZ\Publish\API\Repository\PermissionCriterionResolver
*/
protected $permissionsCriterionHandler;
protected $permissionCriterionResolver;

/**
* Setups service with reference to repository object that created it & corresponding handler.
Expand All @@ -74,15 +75,15 @@ class LocationService implements LocationServiceInterface
* @param \eZ\Publish\SPI\Persistence\Handler $handler
* @param \eZ\Publish\Core\Repository\Helper\DomainMapper $domainMapper
* @param \eZ\Publish\Core\Repository\Helper\NameSchemaService $nameSchemaService
* @param \eZ\Publish\Core\Repository\PermissionsCriterionHandler $permissionsCriterionHandler
* @param \eZ\Publish\API\Repository\PermissionCriterionResolver $permissionCriterionResolver
* @param array $settings
*/
public function __construct(
RepositoryInterface $repository,
Handler $handler,
Helper\DomainMapper $domainMapper,
Helper\NameSchemaService $nameSchemaService,
PermissionsCriterionHandler $permissionsCriterionHandler,
PermissionCriterionResolver $permissionCriterionResolver,
array $settings = array()
) {
$this->repository = $repository;
Expand All @@ -93,7 +94,7 @@ public function __construct(
$this->settings = $settings + array(
//'defaultSetting' => array(),
);
$this->permissionsCriterionHandler = $permissionsCriterionHandler;
$this->permissionCriterionResolver = $permissionCriterionResolver;
}

/**
Expand Down Expand Up @@ -127,7 +128,7 @@ public function copySubtree(APILocation $subtree, APILocation $targetParentLocat
/** Check read access to whole source subtree
* @var bool|\eZ\Publish\API\Repository\Values\Content\Query\Criterion
*/
$contentReadCriterion = $this->permissionsCriterionHandler->getPermissionsCriterion();
$contentReadCriterion = $this->permissionCriterionResolver->getPermissionsCriterion();
if ($contentReadCriterion === false) {
throw new UnauthorizedException('content', 'read');
} elseif ($contentReadCriterion !== true) {
Expand Down Expand Up @@ -590,7 +591,7 @@ public function moveSubtree(APILocation $location, APILocation $newParentLocatio
/** Check read access to whole source subtree
* @var bool|\eZ\Publish\API\Repository\Values\Content\Query\Criterion
*/
$contentReadCriterion = $this->permissionsCriterionHandler->getPermissionsCriterion();
$contentReadCriterion = $this->permissionCriterionResolver->getPermissionsCriterion();
if ($contentReadCriterion === false) {
throw new UnauthorizedException('content', 'read');
} elseif ($contentReadCriterion !== true) {
Expand Down Expand Up @@ -669,7 +670,7 @@ public function deleteLocation(APILocation $location)
/** Check remove access to descendants
* @var bool|\eZ\Publish\API\Repository\Values\Content\Query\Criterion
*/
$contentReadCriterion = $this->permissionsCriterionHandler->getPermissionsCriterion('content', 'remove');
$contentReadCriterion = $this->permissionCriterionResolver->getPermissionsCriterion('content', 'remove');
if ($contentReadCriterion === false) {
throw new UnauthorizedException('content', 'remove');
} elseif ($contentReadCriterion !== true) {
Expand Down
126 changes: 126 additions & 0 deletions eZ/Publish/Core/Repository/Permission/CachedPermissionService.php
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 eZ/Publish/Core/Repository/Permission/PermissionCriterionResolver.php
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];
}
}
Loading

0 comments on commit 1ba7447

Please sign in to comment.