Skip to content

Commit 8184295

Browse files
committed
Merge branch '2.x' into 3.x
2 parents 8c3f3b3 + 94fadea commit 8184295

File tree

5 files changed

+142
-1
lines changed

5 files changed

+142
-1
lines changed

docs/en/policies.rst

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,32 @@ Before hooks are expected to return one of three values:
132132
- ``null`` The before hook did not make a decision, and the authorization method
133133
will be invoked.
134134

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+
160+
135161
Applying Policies
136162
-----------------
137163

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;
@@ -115,6 +116,15 @@ public function applyScope(?IdentityInterface $user, string $action, mixed $reso
115116
{
116117
$this->authorizationChecked = true;
117118
$policy = $this->resolver->getPolicy($resource);
119+
120+
if ($policy instanceof BeforeScopeInterface) {
121+
$result = $policy->beforeScope($user, $resource, $action);
122+
123+
if ($result !== null) {
124+
return $result;
125+
}
126+
}
127+
118128
$handler = $this->getScopeHandler($policy, $action);
119129

120130
return $handler($user, $resource, ...$optionalArgs);

src/Identity.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public function __construct(AuthorizationServiceInterface $service, AuthenIdenti
4949
/**
5050
* Get the primary key/id field for the identity.
5151
*
52-
* @return array|string|int|null
52+
* @return array<array-key, mixed>|string|int|null
5353
*/
5454
public function getIdentifier(): string|int|array|null
5555
{
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: 66 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\OrmResolver;
@@ -30,6 +31,7 @@
3031
use TestApp\Model\Table\ArticlesTable;
3132
use TestApp\Policy\ArticlePolicy;
3233
use TestApp\Policy\MagicCallPolicy;
34+
use TypeError;
3335

3436
class AuthorizationServiceTest extends TestCase
3537
{
@@ -470,6 +472,70 @@ public function testBeforeResultFalse()
470472
$this->assertFalse($result);
471473
}
472474

475+
public function testBeforeScopeNonNull()
476+
{
477+
$entity = new Article();
478+
479+
$policy = $this->getMockBuilder(BeforeScopeInterface::class)
480+
->onlyMethods(['beforeScope'])
481+
->addMethods(['scopeIndex'])
482+
->getMock();
483+
484+
$policy->expects($this->once())
485+
->method('beforeScope')
486+
->with($this->isInstanceOf(IdentityDecorator::class), $entity, 'index')
487+
->willReturn('foo');
488+
489+
$policy->expects($this->never())
490+
->method('scopeIndex');
491+
492+
$resolver = new MapResolver([
493+
Article::class => $policy,
494+
]);
495+
496+
$service = new AuthorizationService($resolver);
497+
498+
$user = new IdentityDecorator($service, [
499+
'role' => 'admin',
500+
]);
501+
502+
$result = $service->applyScope($user, 'index', $entity);
503+
$this->assertEquals('foo', $result);
504+
}
505+
506+
public function testBeforeScopeNull()
507+
{
508+
$entity = new Article();
509+
510+
$policy = $this->getMockBuilder(BeforeScopeInterface::class)
511+
->onlyMethods(['beforeScope'])
512+
->addMethods(['scopeIndex'])
513+
->getMock();
514+
515+
$policy->expects($this->once())
516+
->method('beforeScope')
517+
->with($this->isInstanceOf(IdentityDecorator::class), $entity, 'index')
518+
->willReturn(null);
519+
520+
$policy->expects($this->once())
521+
->method('scopeIndex')
522+
->with($this->isInstanceOf(IdentityDecorator::class), $entity)
523+
->willReturn('bar');
524+
525+
$resolver = new MapResolver([
526+
Article::class => $policy,
527+
]);
528+
529+
$service = new AuthorizationService($resolver);
530+
531+
$user = new IdentityDecorator($service, [
532+
'role' => 'admin',
533+
]);
534+
535+
$result = $service->applyScope($user, 'index', $entity);
536+
$this->assertEquals('bar', $result);
537+
}
538+
473539
public function testMissingMethod()
474540
{
475541
$entity = new Article();

0 commit comments

Comments
 (0)