diff --git a/.github/workflows/phpspec-task.yml b/.github/workflows/phpspec-task.yml index ed70ba5..f2a2f2e 100644 --- a/.github/workflows/phpspec-task.yml +++ b/.github/workflows/phpspec-task.yml @@ -7,7 +7,7 @@ jobs: runs-on: ${{ matrix.operating-system }} strategy: matrix: - operating-system: [ ubuntu-18.04 ] + operating-system: [ ubuntu-20.04 ] php-versions: [ '7.4', '8.0.12', '8.1', '8.2' ] name: Evaluate Behavior, ${{ matrix.php-versions }} on ${{ matrix.operating-system }} steps: diff --git a/README.md b/README.md index 46f48ab..eea2e5a 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,20 @@ Configuring guards is very simple. Your module's config would look like so: 'list' => [ 'user' ], // role 'user' can access 'listAction' on AdminController ], ], + \Application\Controller\ComplexController::class => [ + 'default' => ['user'], + 'actions' => [ // action-level guards + 'save' => [ + AccessService::GUARD_ACTION => 'save', // you can lean on action/resource rules as well + AccessService::GUARD_RESOURCE => 'complex', // which call 'isAllowed' on AccessService + ], + 'delete' => [ + AccessService::GUARD_ROLE => 'admin', // it is also possible to override the role requirement + AccessService::GUARD_ACTION => 'save', + AccessService::GUARD_RESOURCE => 'complex', + ], + ], + ], ], ], ], diff --git a/bundle/Spec/CirclicalUser/Service/AccessServiceSpec.php b/bundle/Spec/CirclicalUser/Service/AccessServiceSpec.php index d2ca5a1..89e25a5 100644 --- a/bundle/Spec/CirclicalUser/Service/AccessServiceSpec.php +++ b/bundle/Spec/CirclicalUser/Service/AccessServiceSpec.php @@ -20,6 +20,7 @@ use CirclicalUser\Provider\GroupPermissionInterface; use CirclicalUser\Provider\UserPermissionProviderInterface; use CirclicalUser\Provider\RoleProviderInterface; +use CirclicalUser\Service\AccessService; use PhpSpec\ObjectBehavior; use Prophecy\Argument; @@ -39,6 +40,7 @@ function let( UserPermissionInterface $userRule1, UserPermissionInterface $userRule2, UserPermissionInterface $userRule3, + UserPermissionInterface $userRule4, ResourceInterface $resourceObject, GroupPermissionInterface $groupActionRule, UserMapper $userMapper, @@ -115,6 +117,13 @@ function let( $userRule3->can(Argument::type('string'))->willReturn(false); $userRule3->can('bar')->willReturn(true); + $userRule4->getActions()->willReturn(['save']); + $userRule4->getResourceClass()->willReturn('string'); + $userRule4->getResourceId()->willReturn('complex'); + $userRule4->getUser()->willReturn($user); + $userRule4->can(Argument::type('string'))->willReturn(false); + $userRule4->can('save')->willReturn(true); + $resourceObject->getClass()->willReturn("ResourceObject"); $resourceObject->getId()->willReturn("1234"); @@ -125,8 +134,10 @@ function let( $groupActionRule->can(Argument::type('string'))->willReturn(false); $groupActionRule->can('bar')->willReturn(true); + $userRules->getUserPermission(Argument::type('string'), Argument::any())->willReturn(null); $userRules->getUserPermission('beer', $admin)->willReturn($userRule1); + $userRules->getUserPermission('complex', $user)->willReturn($userRule4); $userRules->create($user, 'string', 'beer', ['buy'])->willReturn($userRule2); $userRules->save($userRule2)->willReturn(null); $userRules->getResourceUserPermission($resourceObject, $user)->willReturn($userRule3); @@ -136,6 +147,7 @@ function let( $userRules->getUserPermission('badresult', $user)->willReturn($someObject); $groupRules->getPermissions('beer')->willReturn([$rule1, $rule2, $rule3]); + $groupRules->getPermissions('complex')->willReturn([]); $groupRules->getResourcePermissions($resourceObject)->willReturn([$groupActionRule]); $groupRules->getResourcePermissionsByClass('ResourceObject')->willReturn([$groupActionRule]); @@ -170,6 +182,20 @@ function let( 'login' => [], ], ], + 'Admin\Controller\ComplexController' => [ + 'default' => ['user'], + 'actions' => [ + 'save' => [ + AccessService::GUARD_ACTION => 'save', + AccessService::GUARD_RESOURCE => 'complex', + ], + 'delete' => [ + AccessService::GUARD_ROLE => 'admin', + AccessService::GUARD_ACTION => 'save', + AccessService::GUARD_RESOURCE => 'complex', + ], + ], + ], ], ], ]; @@ -370,6 +396,38 @@ function it_allows_action_overrides() $this->canAccessAction('Foo\Controller\IndexController', 'login')->shouldBe(true); } + function it_allows_resource_based_action_rules(User $user) + { + $this->setUser($user); + $this->canAccessAction('Admin\Controller\ComplexController', 'save')->shouldBe(true); + } + + /** + * There is no resource rule defined for this action, so it should not be allowed. + */ + function it_still_falls_back_onto_controller_definitions_when_actions_are_not_defined(User $user) + { + $this->setUser($user); + $this->canAccessAction('Admin\Controller\ComplexController', 'load')->shouldBe(true); + } + + /** + * There is no resource rule defined for this action, so it should not be allowed. + */ + function it_will_protect_in_cases_where_users_are_not_defined(User $user) + { + $this->canAccessAction('Admin\Controller\ComplexController', 'save')->shouldBe(false); + } + + /** + * There is no resource rule defined for this action, so it should not be allowed. + */ + function it_supports_overriding_default_roles(User $user) + { + $this->canAccessAction('Admin\Controller\ComplexController', 'delete')->shouldBe(false); + } + + function it_returns_roles_when_no_user_is_set() { $this->getRoles()->shouldHaveCount(0); diff --git a/src/Factory/AbstractDoctrineMapperFactory.php b/src/Factory/AbstractDoctrineMapperFactory.php index 5870475..003506d 100644 --- a/src/Factory/AbstractDoctrineMapperFactory.php +++ b/src/Factory/AbstractDoctrineMapperFactory.php @@ -4,8 +4,8 @@ namespace CirclicalUser\Factory; -use Interop\Container\ContainerInterface; use Laminas\ServiceManager\Factory\AbstractFactoryInterface; +use Psr\Container\ContainerInterface; use function strstr; diff --git a/src/Factory/Controller/Plugin/AuthenticationPluginFactory.php b/src/Factory/Controller/Plugin/AuthenticationPluginFactory.php index e04e2f5..091e978 100644 --- a/src/Factory/Controller/Plugin/AuthenticationPluginFactory.php +++ b/src/Factory/Controller/Plugin/AuthenticationPluginFactory.php @@ -7,8 +7,8 @@ use CirclicalUser\Controller\Plugin\AuthenticationPlugin; use CirclicalUser\Service\AccessService; use CirclicalUser\Service\AuthenticationService; -use Interop\Container\ContainerInterface; use Laminas\ServiceManager\Factory\FactoryInterface; +use Psr\Container\ContainerInterface; class AuthenticationPluginFactory implements FactoryInterface { diff --git a/src/Factory/Listener/AccessListenerFactory.php b/src/Factory/Listener/AccessListenerFactory.php index c3e87ea..4153f8e 100644 --- a/src/Factory/Listener/AccessListenerFactory.php +++ b/src/Factory/Listener/AccessListenerFactory.php @@ -7,9 +7,9 @@ use CirclicalUser\Listener\AccessListener; use CirclicalUser\Service\AccessService; use Exception; -use Interop\Container\ContainerInterface; use InvalidArgumentException; use Laminas\ServiceManager\Factory\FactoryInterface; +use Psr\Container\ContainerInterface; use function class_exists; diff --git a/src/Factory/Listener/UserEntityListenerFactory.php b/src/Factory/Listener/UserEntityListenerFactory.php index e5935ad..104ccbb 100644 --- a/src/Factory/Listener/UserEntityListenerFactory.php +++ b/src/Factory/Listener/UserEntityListenerFactory.php @@ -6,8 +6,8 @@ use CirclicalUser\Listener\UserEntityListener; use DomainException; -use Interop\Container\ContainerInterface; use Laminas\ServiceManager\Factory\FactoryInterface; +use Psr\Container\ContainerInterface; class UserEntityListenerFactory implements FactoryInterface { diff --git a/src/Factory/Mapper/UserMapperFactory.php b/src/Factory/Mapper/UserMapperFactory.php index 8d7316a..ebd06ad 100644 --- a/src/Factory/Mapper/UserMapperFactory.php +++ b/src/Factory/Mapper/UserMapperFactory.php @@ -7,8 +7,8 @@ use CirclicalUser\Exception\ConfigurationException; use CirclicalUser\Mapper\UserMapper; use CirclicalUser\Provider\UserInterface; -use Interop\Container\ContainerInterface; use Laminas\ServiceManager\Factory\FactoryInterface; +use Psr\Container\ContainerInterface; use function class_exists; use function class_implements; diff --git a/src/Factory/Service/AccessServiceFactory.php b/src/Factory/Service/AccessServiceFactory.php index 56edb0a..9326aa7 100644 --- a/src/Factory/Service/AccessServiceFactory.php +++ b/src/Factory/Service/AccessServiceFactory.php @@ -12,9 +12,9 @@ use CirclicalUser\Provider\RoleProviderInterface; use CirclicalUser\Service\AccessService; use CirclicalUser\Service\AuthenticationService; -use Interop\Container\ContainerInterface; use Laminas\ServiceManager\Factory\FactoryInterface; use LogicException; +use Psr\Container\ContainerInterface; class AccessServiceFactory implements FactoryInterface { diff --git a/src/Factory/Service/AuthenticationServiceFactory.php b/src/Factory/Service/AuthenticationServiceFactory.php index 66bd661..ea25ee5 100644 --- a/src/Factory/Service/AuthenticationServiceFactory.php +++ b/src/Factory/Service/AuthenticationServiceFactory.php @@ -9,8 +9,8 @@ use CirclicalUser\Mapper\UserResetTokenMapper; use CirclicalUser\Provider\PasswordCheckerInterface; use CirclicalUser\Service\AuthenticationService; -use Interop\Container\ContainerInterface; use Laminas\ServiceManager\Factory\FactoryInterface; +use Psr\Container\ContainerInterface; use RuntimeException; use function base64_decode; diff --git a/src/Factory/Service/PasswordChecker/PasswordCheckerFactory.php b/src/Factory/Service/PasswordChecker/PasswordCheckerFactory.php index 907bf8b..45048bd 100644 --- a/src/Factory/Service/PasswordChecker/PasswordCheckerFactory.php +++ b/src/Factory/Service/PasswordChecker/PasswordCheckerFactory.php @@ -7,9 +7,9 @@ use CirclicalUser\Exception\PasswordStrengthCheckerException; use CirclicalUser\Provider\PasswordCheckerInterface; use CirclicalUser\Service\PasswordChecker\PasswordNotChecked; -use Interop\Container\ContainerInterface; use Laminas\ServiceManager\Factory\FactoryInterface; use Psr\Container\ContainerExceptionInterface; +use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; use RuntimeException; diff --git a/src/Factory/Strategy/RedirectStrategyFactory.php b/src/Factory/Strategy/RedirectStrategyFactory.php index 01c6652..e91327e 100644 --- a/src/Factory/Strategy/RedirectStrategyFactory.php +++ b/src/Factory/Strategy/RedirectStrategyFactory.php @@ -5,9 +5,9 @@ namespace CirclicalUser\Factory\Strategy; use CirclicalUser\Strategy\RedirectStrategy; -use Interop\Container\ContainerInterface; use InvalidArgumentException; use Laminas\ServiceManager\Factory\FactoryInterface; +use Psr\Container\ContainerInterface; class RedirectStrategyFactory implements FactoryInterface { diff --git a/src/Factory/Validator/PasswordValidatorFactory.php b/src/Factory/Validator/PasswordValidatorFactory.php index 8c72060..0a56018 100644 --- a/src/Factory/Validator/PasswordValidatorFactory.php +++ b/src/Factory/Validator/PasswordValidatorFactory.php @@ -6,8 +6,8 @@ use CirclicalUser\Provider\PasswordCheckerInterface; use CirclicalUser\Validator\PasswordValidator; -use Interop\Container\ContainerInterface; use Laminas\ServiceManager\Factory\FactoryInterface; +use Psr\Container\ContainerInterface; class PasswordValidatorFactory implements FactoryInterface { diff --git a/src/Factory/View/Helper/ControllerAccessViewHelperFactory.php b/src/Factory/View/Helper/ControllerAccessViewHelperFactory.php index d334512..e713dcf 100644 --- a/src/Factory/View/Helper/ControllerAccessViewHelperFactory.php +++ b/src/Factory/View/Helper/ControllerAccessViewHelperFactory.php @@ -6,8 +6,8 @@ use CirclicalUser\Service\AccessService; use CirclicalUser\View\Helper\ControllerAccessViewHelper; -use Interop\Container\ContainerInterface; use Laminas\ServiceManager\Factory\FactoryInterface; +use Psr\Container\ContainerInterface; class ControllerAccessViewHelperFactory implements FactoryInterface { diff --git a/src/Factory/View/Helper/RoleAccessViewHelperFactory.php b/src/Factory/View/Helper/RoleAccessViewHelperFactory.php index 4ab5599..42f9fe1 100644 --- a/src/Factory/View/Helper/RoleAccessViewHelperFactory.php +++ b/src/Factory/View/Helper/RoleAccessViewHelperFactory.php @@ -6,8 +6,8 @@ use CirclicalUser\Service\AccessService; use CirclicalUser\View\Helper\RoleAccessViewHelper; -use Interop\Container\ContainerInterface; use Laminas\ServiceManager\Factory\FactoryInterface; +use Psr\Container\ContainerInterface; class RoleAccessViewHelperFactory implements FactoryInterface { diff --git a/src/Service/AccessService.php b/src/Service/AccessService.php index 49fad11..e4f58b8 100644 --- a/src/Service/AccessService.php +++ b/src/Service/AccessService.php @@ -23,6 +23,7 @@ use CirclicalUser\Provider\UserProviderInterface; use Exception; +use function array_key_exists; use function array_unique; use function get_class; use function in_array; @@ -33,6 +34,9 @@ class AccessService { public const ACCESS_DENIED = 'ACL_ACCESS_DENIED'; public const ACCESS_UNAUTHORIZED = 'ACCESS_UNAUTHORIZED'; + public const GUARD_ROLE = 'role'; + public const GUARD_ACTION = 'action'; + public const GUARD_RESOURCE = 'resource'; private ?User $user; private UserProviderInterface $userProvider; @@ -167,6 +171,18 @@ public function canAccessAction(string $controllerName, string $action): bool return true; } + $actionConfiguration = $this->actions[$controllerName][$action]; + + if (is_array($actionConfiguration) && array_key_exists(self::GUARD_RESOURCE, $actionConfiguration) && array_key_exists(self::GUARD_ACTION, $actionConfiguration)) { + if (!empty($actionConfiguration[self::GUARD_ROLE])) { + if (!$this->hasRoleWithName($actionConfiguration[self::GUARD_ROLE])) { + return false; + } + } + + return $this->isAllowed($actionConfiguration[self::GUARD_RESOURCE], $actionConfiguration[self::GUARD_ACTION]); + } + foreach ($this->actions[$controllerName][$action] as $role) { if ($this->hasRoleWithName($role)) { return true; @@ -248,8 +264,6 @@ public function getRoleWithName(string $roleName): ?RoleInterface /** * Add a role for the current User * - * @internal param $roleId - * * @throws InvalidRoleException * @throws UserRequiredException */ diff --git a/src/Service/AuthenticationService.php b/src/Service/AuthenticationService.php index dfda46c..088695b 100644 --- a/src/Service/AuthenticationService.php +++ b/src/Service/AuthenticationService.php @@ -386,9 +386,9 @@ private function setCookie(string $name, string $value, int $expiry, bool $httpO * - COOKIE_USER has its contents encrypted by the system key * - the random-named-cookie has its contents encrypted by the user key * - * @throws InvalidKey * @see self::setSessionCookies * + * @throws InvalidKey */ public function getIdentity(): ?User { @@ -507,7 +507,6 @@ private function purgeHashCookies(?string $skipCookie = null) /** * @param User $user Used by some password checkers to provide better checking - * * @throws WeakPasswordException */ private function enforcePasswordStrength(string $password, User $user) @@ -523,7 +522,6 @@ private function enforcePasswordStrength(string $password, User $user) * * @param User $user The user to whom this password gets assigned * @param string $newPassword Cleartext password that's being hashed - * * @throws NoSuchUserException * @throws WeakPasswordException */ @@ -547,7 +545,6 @@ public function resetPassword(User $user, string $newPassword) * * @param User $user The user to validate password for * @param string $password Cleartext password that'w will be verified - * * @throws PersistedUserRequiredException * @throws UserWithoutAuthenticationRecordException */