Skip to content

Commit

Permalink
Merge pull request #108 from Saeven/feature/action-resource-gate
Browse files Browse the repository at this point in the history
Feature/action resource gate
  • Loading branch information
Saeven authored Jun 13, 2023
2 parents 0793a9d + 0019e33 commit 3c22b8c
Show file tree
Hide file tree
Showing 17 changed files with 102 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/phpspec-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],
],
],
],
],
],
Expand Down
58 changes: 58 additions & 0 deletions bundle/Spec/CirclicalUser/Service/AccessServiceSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -39,6 +40,7 @@ function let(
UserPermissionInterface $userRule1,
UserPermissionInterface $userRule2,
UserPermissionInterface $userRule3,
UserPermissionInterface $userRule4,
ResourceInterface $resourceObject,
GroupPermissionInterface $groupActionRule,
UserMapper $userMapper,
Expand Down Expand Up @@ -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");

Expand All @@ -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);
Expand All @@ -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]);

Expand Down Expand Up @@ -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',
],
],
],
],
],
];
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/Factory/AbstractDoctrineMapperFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

namespace CirclicalUser\Factory;

use Interop\Container\ContainerInterface;
use Laminas\ServiceManager\Factory\AbstractFactoryInterface;
use Psr\Container\ContainerInterface;

use function strstr;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
2 changes: 1 addition & 1 deletion src/Factory/Listener/AccessListenerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion src/Factory/Listener/UserEntityListenerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
2 changes: 1 addition & 1 deletion src/Factory/Mapper/UserMapperFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/Factory/Service/AccessServiceFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
2 changes: 1 addition & 1 deletion src/Factory/Service/AuthenticationServiceFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion src/Factory/Strategy/RedirectStrategyFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
2 changes: 1 addition & 1 deletion src/Factory/Validator/PasswordValidatorFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
2 changes: 1 addition & 1 deletion src/Factory/View/Helper/RoleAccessViewHelperFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
18 changes: 16 additions & 2 deletions src/Service/AccessService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -248,8 +264,6 @@ public function getRoleWithName(string $roleName): ?RoleInterface
/**
* Add a role for the current User
*
* @internal param $roleId
*
* @throws InvalidRoleException
* @throws UserRequiredException
*/
Expand Down
5 changes: 1 addition & 4 deletions src/Service/AuthenticationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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)
Expand All @@ -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
*/
Expand All @@ -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
*/
Expand Down

0 comments on commit 3c22b8c

Please sign in to comment.