Skip to content

Commit 94fadea

Browse files
authored
Merge pull request #264 from jamisonbryant/feature/before-scope-interface-2.x
Feature: BeforeScopeInterface (2.next)
2 parents d3b08f6 + cfe53ac commit 94fadea

File tree

4 files changed

+140
-1
lines changed

4 files changed

+140
-1
lines changed

docs/en/policies.rst

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,32 @@ Before hooks are expected to return one of three values:
131131
- ``false`` The user is not allowed to proceed with the action.
132132
- ``null`` The before hook did not make a decision, and the authorization method
133133
will be invoked.
134-
134+
135+
Scope Pre-conditions
136+
====================
137+
138+
Like policies, scopes can also define pre-conditions. These are useful when you
139+
want to apply common conditions to all scopes in a policy. To use pre-conditions
140+
on scopes you need to implement the ``BeforeScopeInterface`` in your scope policy::
141+
142+
namespace App\Policy;
143+
144+
use Authorization\Policy\BeforeScopeInterface;
145+
146+
class ArticlesTablePolicy implements BeforeScopeInterface
147+
{
148+
public function beforeScope($user, $query, $action)
149+
{
150+
if ($user->getOriginalData()->is_trial_user) {
151+
return $query->where(['Articles.is_paid_only' => false]);
152+
}
153+
// fall through
154+
}
155+
}
156+
157+
Before scope hooks are expected to return the modified resource object, or if
158+
``null`` is returned then the scope method will be invoked as normal.
159+
135160
Applying Policies
136161
-----------------
137162
See :ref:`applying-policy-scopes` for how to apply policies in your controller actions.

src/AuthorizationService.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
use Authorization\Exception\Exception;
2020
use Authorization\Policy\BeforePolicyInterface;
21+
use Authorization\Policy\BeforeScopeInterface;
2122
use Authorization\Policy\Exception\MissingMethodException;
2223
use Authorization\Policy\ResolverInterface;
2324
use Authorization\Policy\Result;
@@ -120,6 +121,15 @@ public function applyScope(?IdentityInterface $user, string $action, $resource)
120121
{
121122
$this->authorizationChecked = true;
122123
$policy = $this->resolver->getPolicy($resource);
124+
125+
if ($policy instanceof BeforeScopeInterface) {
126+
$result = $policy->beforeScope($user, $resource, $action);
127+
128+
if ($result !== null) {
129+
return $result;
130+
}
131+
}
132+
123133
$handler = $this->getScopeHandler($policy, $action);
124134

125135
return $handler($user, $resource);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/**
5+
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
6+
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
7+
*
8+
* Licensed under The MIT License
9+
* For full copyright and license information, please see the LICENSE.txt
10+
* Redistributions of files must retain the above copyright notice.
11+
*
12+
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
13+
* @link https://cakephp.org CakePHP(tm) Project
14+
* @since 2.4.0
15+
* @license https://opensource.org/licenses/mit-license.php MIT License
16+
*/
17+
namespace Authorization\Policy;
18+
19+
use Authorization\IdentityInterface;
20+
21+
/**
22+
* This interface should be implemented if a policy class needs to perform a
23+
* pre-authorization check before the scope is applied to the resource.
24+
*/
25+
interface BeforeScopeInterface
26+
{
27+
/**
28+
* Defines a pre-scope check.
29+
*
30+
* If a non-null value is returned, the scope application will be skipped and the un-scoped resource
31+
* will be returned. In case of `null`, the scope will be applied.
32+
*
33+
* @param \Authorization\IdentityInterface|null $identity Identity object.
34+
* @param mixed $resource The resource being operated on.
35+
* @param string $action The action/operation being performed.
36+
* @return mixed
37+
*/
38+
public function beforeScope(?IdentityInterface $identity, $resource, string $action);
39+
}

tests/TestCase/AuthorizationServiceTest.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Authorization\AuthorizationService;
2020
use Authorization\IdentityDecorator;
2121
use Authorization\Policy\BeforePolicyInterface;
22+
use Authorization\Policy\BeforeScopeInterface;
2223
use Authorization\Policy\Exception\MissingMethodException;
2324
use Authorization\Policy\MapResolver;
2425
use Authorization\Policy\Result;
@@ -375,6 +376,70 @@ public function testBeforeOther()
375376
$service->can($user, 'add', $entity);
376377
}
377378

379+
public function testBeforeScopeNonNull()
380+
{
381+
$entity = new Article();
382+
383+
$policy = $this->getMockBuilder(BeforeScopeInterface::class)
384+
->onlyMethods(['beforeScope'])
385+
->addMethods(['scopeIndex'])
386+
->getMock();
387+
388+
$policy->expects($this->once())
389+
->method('beforeScope')
390+
->with($this->isInstanceOf(IdentityDecorator::class), $entity, 'index')
391+
->willReturn('foo');
392+
393+
$policy->expects($this->never())
394+
->method('scopeIndex');
395+
396+
$resolver = new MapResolver([
397+
Article::class => $policy,
398+
]);
399+
400+
$service = new AuthorizationService($resolver);
401+
402+
$user = new IdentityDecorator($service, [
403+
'role' => 'admin',
404+
]);
405+
406+
$result = $service->applyScope($user, 'index', $entity);
407+
$this->assertEquals('foo', $result);
408+
}
409+
410+
public function testBeforeScopeNull()
411+
{
412+
$entity = new Article();
413+
414+
$policy = $this->getMockBuilder(BeforeScopeInterface::class)
415+
->onlyMethods(['beforeScope'])
416+
->addMethods(['scopeIndex'])
417+
->getMock();
418+
419+
$policy->expects($this->once())
420+
->method('beforeScope')
421+
->with($this->isInstanceOf(IdentityDecorator::class), $entity, 'index')
422+
->willReturn(null);
423+
424+
$policy->expects($this->once())
425+
->method('scopeIndex')
426+
->with($this->isInstanceOf(IdentityDecorator::class), $entity)
427+
->willReturn('bar');
428+
429+
$resolver = new MapResolver([
430+
Article::class => $policy,
431+
]);
432+
433+
$service = new AuthorizationService($resolver);
434+
435+
$user = new IdentityDecorator($service, [
436+
'role' => 'admin',
437+
]);
438+
439+
$result = $service->applyScope($user, 'index', $entity);
440+
$this->assertEquals('bar', $result);
441+
}
442+
378443
public function testMissingMethod()
379444
{
380445
$entity = new Article();

0 commit comments

Comments
 (0)