vendor/ezsystems/ezpublish-kernel/eZ/Publish/Core/Repository/Permission/PermissionResolver.php line 224

Open in your IDE?
  1. <?php
  2. /**
  3.  * @copyright Copyright (C) eZ Systems AS. All rights reserved.
  4.  * @license For full copyright and license information view LICENSE file distributed with this source code.
  5.  */
  6. namespace eZ\Publish\Core\Repository\Permission;
  7. use eZ\Publish\API\Repository\PermissionResolver as PermissionResolverInterface;
  8. use eZ\Publish\API\Repository\Repository as RepositoryInterface;
  9. use eZ\Publish\API\Repository\Values\User\Limitation;
  10. use eZ\Publish\API\Repository\Values\User\LookupLimitationResult;
  11. use eZ\Publish\API\Repository\Values\User\LookupPolicyLimitations;
  12. use eZ\Publish\API\Repository\Values\User\UserReference as APIUserReference;
  13. use eZ\Publish\API\Repository\Values\ValueObject;
  14. use eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue;
  15. use eZ\Publish\Core\Repository\Helper\LimitationService;
  16. use eZ\Publish\Core\Repository\Helper\RoleDomainMapper;
  17. use eZ\Publish\SPI\Limitation\Target;
  18. use eZ\Publish\SPI\Limitation\TargetAwareType;
  19. use eZ\Publish\SPI\Limitation\Type as LimitationType;
  20. use eZ\Publish\SPI\Persistence\User\Handler as UserHandler;
  21. use Exception;
  22. /**
  23.  * Core implementation of PermissionResolver interface.
  24.  */
  25. class PermissionResolver implements PermissionResolverInterface
  26. {
  27.     /**
  28.      * Counter for the current sudo nesting level {@see sudo()}.
  29.      *
  30.      * @var int
  31.      */
  32.     private $sudoNestingLevel 0;
  33.     /** @var \eZ\Publish\Core\Repository\Helper\RoleDomainMapper */
  34.     private $roleDomainMapper;
  35.     /** @var \eZ\Publish\Core\Repository\Helper\LimitationService */
  36.     private $limitationService;
  37.     /** @var \eZ\Publish\SPI\Persistence\User\Handler */
  38.     private $userHandler;
  39.     /**
  40.      * Currently logged in user reference for permission purposes.
  41.      *
  42.      * @var \eZ\Publish\API\Repository\Values\User\UserReference
  43.      */
  44.     private $currentUserRef;
  45.     /**
  46.      * Map of system configured policies, for validation usage.
  47.      *
  48.      * @var array
  49.      */
  50.     private $policyMap;
  51.     /**
  52.      * @param \eZ\Publish\Core\Repository\Helper\RoleDomainMapper $roleDomainMapper
  53.      * @param \eZ\Publish\Core\Repository\Helper\LimitationService $limitationService
  54.      * @param \eZ\Publish\SPI\Persistence\User\Handler $userHandler
  55.      * @param \eZ\Publish\API\Repository\Values\User\UserReference $userReference
  56.      * @param array $policyMap Map of system configured policies, for validation usage.
  57.      */
  58.     public function __construct(
  59.         RoleDomainMapper $roleDomainMapper,
  60.         LimitationService $limitationService,
  61.         UserHandler $userHandler,
  62.         APIUserReference $userReference,
  63.         array $policyMap = []
  64.     ) {
  65.         $this->roleDomainMapper $roleDomainMapper;
  66.         $this->limitationService $limitationService;
  67.         $this->userHandler $userHandler;
  68.         $this->currentUserRef $userReference;
  69.         $this->policyMap $policyMap;
  70.     }
  71.     public function getCurrentUserReference()
  72.     {
  73.         return $this->currentUserRef;
  74.     }
  75.     public function setCurrentUserReference(APIUserReference $userReference)
  76.     {
  77.         $id $userReference->getUserId();
  78.         if (!$id) {
  79.             throw new InvalidArgumentValue('$user->getUserId()'$id);
  80.         }
  81.         $this->currentUserRef $userReference;
  82.     }
  83.     public function hasAccess($module$functionAPIUserReference $userReference null)
  84.     {
  85.         if (!isset($this->policyMap[$module])) {
  86.             throw new InvalidArgumentValue('module'"module: {$module}/ function: {$function}");
  87.         } elseif (!array_key_exists($function$this->policyMap[$module])) {
  88.             throw new InvalidArgumentValue('function'"module: {$module}/ function: {$function}");
  89.         }
  90.         // Full access if sudo nesting level is set by {@see sudo()}
  91.         if ($this->sudoNestingLevel 0) {
  92.             return true;
  93.         }
  94.         if ($userReference === null) {
  95.             $userReference $this->getCurrentUserReference();
  96.         }
  97.         // Uses SPI to avoid triggering permission checks in Role/User service
  98.         $permissionSets = [];
  99.         $spiRoleAssignments $this->userHandler->loadRoleAssignmentsByGroupId($userReference->getUserId(), true);
  100.         foreach ($spiRoleAssignments as $spiRoleAssignment) {
  101.             $permissionSet = ['limitation' => null'policies' => []];
  102.             $spiRole $this->userHandler->loadRole($spiRoleAssignment->roleId);
  103.             foreach ($spiRole->policies as $spiPolicy) {
  104.                 if ($spiPolicy->module === '*' && $spiRoleAssignment->limitationIdentifier === null) {
  105.                     return true;
  106.                 }
  107.                 if ($spiPolicy->module !== $module && $spiPolicy->module !== '*') {
  108.                     continue;
  109.                 }
  110.                 if ($spiPolicy->function === '*' && $spiRoleAssignment->limitationIdentifier === null) {
  111.                     return true;
  112.                 }
  113.                 if ($spiPolicy->function !== $function && $spiPolicy->function !== '*') {
  114.                     continue;
  115.                 }
  116.                 if ($spiPolicy->limitations === '*' && $spiRoleAssignment->limitationIdentifier === null) {
  117.                     return true;
  118.                 }
  119.                 $permissionSet['policies'][] = $this->roleDomainMapper->buildDomainPolicyObject($spiPolicy);
  120.             }
  121.             if (!empty($permissionSet['policies'])) {
  122.                 if ($spiRoleAssignment->limitationIdentifier !== null) {
  123.                     $permissionSet['limitation'] = $this->limitationService
  124.                         ->getLimitationType($spiRoleAssignment->limitationIdentifier)
  125.                         ->buildValue($spiRoleAssignment->values);
  126.                 }
  127.                 $permissionSets[] = $permissionSet;
  128.             }
  129.         }
  130.         if (!empty($permissionSets)) {
  131.             return $permissionSets;
  132.         }
  133.         return false// No policies matching $module and $function, or they contained limitations
  134.     }
  135.     public function canUser($module$functionValueObject $object, array $targets = [])
  136.     {
  137.         $permissionSets $this->hasAccess($module$function);
  138.         if ($permissionSets === false || $permissionSets === true) {
  139.             return $permissionSets;
  140.         }
  141.         if (empty($targets)) {
  142.             $targets null;
  143.         }
  144.         $currentUserRef $this->getCurrentUserReference();
  145.         foreach ($permissionSets as $permissionSet) {
  146.             /**
  147.              * First deal with Role limitation if any.
  148.              *
  149.              * Here we accept ACCESS_GRANTED and ACCESS_ABSTAIN, the latter in cases where $object and $targets
  150.              * are not supported by limitation.
  151.              *
  152.              * @var \eZ\Publish\API\Repository\Values\User\Limitation[]
  153.              */
  154.             if (
  155.                 $permissionSet['limitation'] instanceof Limitation
  156.                 && $this->isDeniedByRoleLimitation(
  157.                     $permissionSet['limitation'],
  158.                     $currentUserRef,
  159.                     $object,
  160.                     $targets
  161.                 )
  162.             ) {
  163.                 continue;
  164.             }
  165.             /**
  166.              * Loop over all policies.
  167.              *
  168.              * These are already filtered by hasAccess and given hasAccess did not return boolean
  169.              * there must be some, so only return true if one of them says yes.
  170.              *
  171.              * @var \eZ\Publish\API\Repository\Values\User\Policy
  172.              */
  173.             foreach ($permissionSet['policies'] as $policy) {
  174.                 $limitations $policy->getLimitations();
  175.                 /*
  176.                  * Return true if policy gives full access (aka no limitations)
  177.                  */
  178.                 if ($limitations === '*') {
  179.                     return true;
  180.                 }
  181.                 /*
  182.                  * Loop over limitations, all must return ACCESS_GRANTED for policy to pass.
  183.                  * If limitations was empty array this means same as '*'
  184.                  */
  185.                 $limitationsPass true;
  186.                 foreach ($limitations as $limitation) {
  187.                     $type $this->limitationService->getLimitationType($limitation->getIdentifier());
  188.                     $accessVote $type->evaluate(
  189.                         $limitation,
  190.                         $currentUserRef,
  191.                         $object,
  192.                         $this->prepareTargetsForType($targets$type)
  193.                     );
  194.                     /*
  195.                      * For policy limitation atm only support ACCESS_GRANTED
  196.                      *
  197.                      * Reasoning: Right now, use of a policy limitation not valid for a policy is per definition a
  198.                      * BadState. To reach this you would have to configure the "policyMap" wrongly, like using
  199.                      * Node (Location) limitation on state/assign. So in this case Role Limitations will return
  200.                      * ACCESS_ABSTAIN (== no access here), and other limitations will throw InvalidArgument above,
  201.                      * both cases forcing dev to investigate to find miss configuration. This might be relaxed in
  202.                      * the future if valid use cases for ACCESS_ABSTAIN on policy limitations becomes known.
  203.                      */
  204.                     if ($accessVote !== LimitationType::ACCESS_GRANTED) {
  205.                         $limitationsPass false;
  206.                         break; // Break to next policy, all limitations must pass
  207.                     }
  208.                 }
  209.                 if ($limitationsPass) {
  210.                     return true;
  211.                 }
  212.             }
  213.         }
  214.         return false// None of the limitation sets wanted to let you in, sorry!
  215.     }
  216.     /**
  217.      * {@inheritdoc}
  218.      */
  219.     public function lookupLimitations(
  220.         string $module,
  221.         string $function,
  222.         ValueObject $object,
  223.         array $targets = [],
  224.         array $limitationsIdentifiers = []
  225.     ): LookupLimitationResult {
  226.         $permissionSets $this->hasAccess($module$function);
  227.         if (is_bool($permissionSets)) {
  228.             return new LookupLimitationResult($permissionSets);
  229.         }
  230.         if (empty($targets)) {
  231.             $targets null;
  232.         }
  233.         $currentUserReference $this->getCurrentUserReference();
  234.         $passedLimitations = [];
  235.         $passedRoleLimitations = [];
  236.         foreach ($permissionSets as $permissionSet) {
  237.             if ($this->isDeniedByRoleLimitation($permissionSet['limitation'], $currentUserReference$object$targets)) {
  238.                 continue;
  239.             }
  240.             /** @var \eZ\Publish\API\Repository\Values\User\Policy $policy */
  241.             foreach ($permissionSet['policies'] as $policy) {
  242.                 $policyLimitations $policy->getLimitations();
  243.                 /** Return empty array if policy gives full access (aka no limitations) */
  244.                 if ($policyLimitations === '*') {
  245.                     return new LookupLimitationResult(true);
  246.                 }
  247.                 $limitationsPass true;
  248.                 $possibleLimitations = [];
  249.                 $possibleRoleLimitation $permissionSet['limitation'];
  250.                 foreach ($policyLimitations as $limitation) {
  251.                     $limitationsPass $this->isGrantedByLimitation($limitation$currentUserReference$object$targets);
  252.                     if (!$limitationsPass) {
  253.                         break;
  254.                     }
  255.                     $possibleLimitations[] = $limitation;
  256.                 }
  257.                 $limitationFilter = function (Limitation $limitation) use ($limitationsIdentifiers) {
  258.                     return \in_array($limitation->getIdentifier(), $limitationsIdentifierstrue);
  259.                 };
  260.                 if (!empty($limitationsIdentifiers)) {
  261.                     $possibleLimitations array_filter($possibleLimitations$limitationFilter);
  262.                     if (!\in_array($possibleRoleLimitation$limitationsIdentifierstrue)) {
  263.                         $possibleRoleLimitation null;
  264.                     }
  265.                 }
  266.                 if ($limitationsPass) {
  267.                     $passedLimitations[] = new LookupPolicyLimitations($policy$possibleLimitations);
  268.                     if (null !== $possibleRoleLimitation) {
  269.                         $passedRoleLimitations[] = $possibleRoleLimitation;
  270.                     }
  271.                 }
  272.             }
  273.         }
  274.         return new LookupLimitationResult(!empty($passedLimitations), $passedRoleLimitations$passedLimitations);
  275.     }
  276.     /**
  277.      * @param \eZ\Publish\API\Repository\Values\User\Limitation $limitation
  278.      * @param \eZ\Publish\API\Repository\Values\User\UserReference $currentUserReference
  279.      * @param \eZ\Publish\API\Repository\Values\ValueObject $object
  280.      * @param array|null $targets
  281.      *
  282.      * @return bool
  283.      *
  284.      * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
  285.      * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
  286.      */
  287.     private function isGrantedByLimitation(
  288.         Limitation $limitation,
  289.         APIUserReference $currentUserReference,
  290.         ValueObject $object,
  291.         ?array $targets
  292.     ): bool {
  293.         $type $this->limitationService->getLimitationType($limitation->getIdentifier());
  294.         $accessVote $type->evaluate($limitation$currentUserReference$object$targets);
  295.         return $accessVote === LimitationType::ACCESS_GRANTED;
  296.     }
  297.     /**
  298.      * @param \eZ\Publish\API\Repository\Values\User\Limitation|null $limitation
  299.      * @param \eZ\Publish\API\Repository\Values\User\UserReference $currentUserReference
  300.      * @param \eZ\Publish\API\Repository\Values\ValueObject $object
  301.      * @param array|null $targets
  302.      *
  303.      * @return bool
  304.      *
  305.      * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
  306.      * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
  307.      */
  308.     private function isDeniedByRoleLimitation(
  309.         ?Limitation $limitation,
  310.         APIUserReference $currentUserReference,
  311.         ValueObject $object,
  312.         ?array $targets
  313.     ): bool {
  314.         if (null === $limitation) {
  315.             return false;
  316.         }
  317.         $type $this->limitationService->getLimitationType($limitation->getIdentifier());
  318.         $accessVote $type->evaluate($limitation$currentUserReference$object$targets);
  319.         return $accessVote === LimitationType::ACCESS_DENIED;
  320.     }
  321.     /**
  322.      * @internal For internal use only, do not depend on this method.
  323.      *
  324.      * Allows API execution to be performed with full access sand-boxed.
  325.      *
  326.      * The closure sandbox will do a catch all on exceptions and rethrow after
  327.      * re-setting the sudo flag.
  328.      *
  329.      * Example use:
  330.      *     $location = $repository->sudo(
  331.      *         function ( Repository $repo ) use ( $locationId )
  332.      *         {
  333.      *             return $repo->getLocationService()->loadLocation( $locationId )
  334.      *         }
  335.      *     );
  336.      *
  337.      * @param \Closure $callback
  338.      * @param \eZ\Publish\API\Repository\Repository $outerRepository
  339.      *
  340.      * @throws \RuntimeException Thrown on recursive sudo() use.
  341.      * @throws \Exception Re throws exceptions thrown inside $callback
  342.      *
  343.      * @return mixed
  344.      */
  345.     public function sudo(callable $callbackRepositoryInterface $outerRepository)
  346.     {
  347.         ++$this->sudoNestingLevel;
  348.         try {
  349.             $returnValue $callback($outerRepository);
  350.         } catch (Exception $e) {
  351.             --$this->sudoNestingLevel;
  352.             throw $e;
  353.         }
  354.         --$this->sudoNestingLevel;
  355.         return $returnValue;
  356.     }
  357.     /**
  358.      * Prepare list of targets for the given Type keeping BC.
  359.      *
  360.      * @param array|null $targets
  361.      * @param \eZ\Publish\SPI\Limitation\Type $type
  362.      *
  363.      * @return array|null
  364.      */
  365.     private function prepareTargetsForType(?array $targetsLimitationType $type): ?array
  366.     {
  367.         $isTargetAware $type instanceof TargetAwareType;
  368.         // BC: null for empty targets is still expected by some Limitations, so needs to be preserved
  369.         if (null === $targets) {
  370.             return $isTargetAware ? [] : null;
  371.         }
  372.         // BC: for TargetAware Limitations return only instances of Target, for others return only non-Target instances
  373.         $targets array_filter(
  374.             $targets,
  375.             function ($target) use ($isTargetAware) {
  376.                 $isTarget $target instanceof Target;
  377.                 return $isTargetAware $isTarget : !$isTarget;
  378.             }
  379.         );
  380.         // BC: treat empty targets after filtering as if they were empty the whole time
  381.         if (!$isTargetAware && empty($targets)) {
  382.             return null;
  383.         }
  384.         return $targets;
  385.     }
  386. }