diff --git a/docs/en/policies.rst b/docs/en/policies.rst index d30ae2b4..e2f6bd67 100644 --- a/docs/en/policies.rst +++ b/docs/en/policies.rst @@ -132,6 +132,32 @@ Before hooks are expected to return one of three values: - ``null`` The before hook did not make a decision, and the authorization method will be invoked. +Scope Pre-conditions +==================== + +Like policies, scopes can also define pre-conditions. These are useful when you +want to apply common conditions to all scopes in a policy. To use pre-conditions +on scopes you need to implement the ``BeforeScopeInterface`` in your scope policy:: + + namespace App\Policy; + + use Authorization\Policy\BeforeScopeInterface; + + class ArticlesTablePolicy implements BeforeScopeInterface + { + public function beforeScope($user, $query, $action) + { + if ($user->getOriginalData()->is_trial_user) { + return $query->where(['Articles.is_paid_only' => false]); + } + // fall through + } + } + +Before scope hooks are expected to return the modified resource object, or if +``null`` is returned then the scope method will be invoked as normal. + + Applying Policies ----------------- diff --git a/src/AuthorizationService.php b/src/AuthorizationService.php index fdec3125..3171dfbf 100644 --- a/src/AuthorizationService.php +++ b/src/AuthorizationService.php @@ -18,6 +18,7 @@ use Authorization\Exception\Exception; use Authorization\Policy\BeforePolicyInterface; +use Authorization\Policy\BeforeScopeInterface; use Authorization\Policy\Exception\MissingMethodException; use Authorization\Policy\ResolverInterface; use Authorization\Policy\Result; @@ -115,6 +116,15 @@ public function applyScope(?IdentityInterface $user, string $action, mixed $reso { $this->authorizationChecked = true; $policy = $this->resolver->getPolicy($resource); + + if ($policy instanceof BeforeScopeInterface) { + $result = $policy->beforeScope($user, $resource, $action); + + if ($result !== null) { + return $result; + } + } + $handler = $this->getScopeHandler($policy, $action); return $handler($user, $resource, ...$optionalArgs); diff --git a/src/Identity.php b/src/Identity.php index 35f60e44..38c70ac4 100644 --- a/src/Identity.php +++ b/src/Identity.php @@ -49,7 +49,7 @@ public function __construct(AuthorizationServiceInterface $service, AuthenIdenti /** * Get the primary key/id field for the identity. * - * @return array|string|int|null + * @return array|string|int|null */ public function getIdentifier(): string|int|array|null { diff --git a/src/Policy/BeforeScopeInterface.php b/src/Policy/BeforeScopeInterface.php new file mode 100644 index 00000000..acf61a2b --- /dev/null +++ b/src/Policy/BeforeScopeInterface.php @@ -0,0 +1,39 @@ +assertFalse($result); } + public function testBeforeScopeNonNull() + { + $entity = new Article(); + + $policy = $this->getMockBuilder(BeforeScopeInterface::class) + ->onlyMethods(['beforeScope']) + ->addMethods(['scopeIndex']) + ->getMock(); + + $policy->expects($this->once()) + ->method('beforeScope') + ->with($this->isInstanceOf(IdentityDecorator::class), $entity, 'index') + ->willReturn('foo'); + + $policy->expects($this->never()) + ->method('scopeIndex'); + + $resolver = new MapResolver([ + Article::class => $policy, + ]); + + $service = new AuthorizationService($resolver); + + $user = new IdentityDecorator($service, [ + 'role' => 'admin', + ]); + + $result = $service->applyScope($user, 'index', $entity); + $this->assertEquals('foo', $result); + } + + public function testBeforeScopeNull() + { + $entity = new Article(); + + $policy = $this->getMockBuilder(BeforeScopeInterface::class) + ->onlyMethods(['beforeScope']) + ->addMethods(['scopeIndex']) + ->getMock(); + + $policy->expects($this->once()) + ->method('beforeScope') + ->with($this->isInstanceOf(IdentityDecorator::class), $entity, 'index') + ->willReturn(null); + + $policy->expects($this->once()) + ->method('scopeIndex') + ->with($this->isInstanceOf(IdentityDecorator::class), $entity) + ->willReturn('bar'); + + $resolver = new MapResolver([ + Article::class => $policy, + ]); + + $service = new AuthorizationService($resolver); + + $user = new IdentityDecorator($service, [ + 'role' => 'admin', + ]); + + $result = $service->applyScope($user, 'index', $entity); + $this->assertEquals('bar', $result); + } + public function testMissingMethod() { $entity = new Article();