Skip to content

Commit

Permalink
Merge branch '2.x' into 3.x
Browse files Browse the repository at this point in the history
  • Loading branch information
markstory committed Nov 10, 2023
2 parents 8c3f3b3 + 94fadea commit 8184295
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 1 deletion.
26 changes: 26 additions & 0 deletions docs/en/policies.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----------------

Expand Down
10 changes: 10 additions & 0 deletions src/AuthorizationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/Identity.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<array-key, mixed>|string|int|null
*/
public function getIdentifier(): string|int|array|null
{
Expand Down
39 changes: 39 additions & 0 deletions src/Policy/BeforeScopeInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);

/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 2.4.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Authorization\Policy;

use Authorization\IdentityInterface;

/**
* This interface should be implemented if a policy class needs to perform a
* pre-authorization check before the scope is applied to the resource.
*/
interface BeforeScopeInterface
{
/**
* Defines a pre-scope check.
*
* If a non-null value is returned, the scope application will be skipped and the un-scoped resource
* will be returned. In case of `null`, the scope will be applied.
*
* @param \Authorization\IdentityInterface|null $identity Identity object.
* @param mixed $resource The resource being operated on.
* @param string $action The action/operation being performed.
* @return mixed
*/
public function beforeScope(?IdentityInterface $identity, $resource, string $action);

Check failure on line 38 in src/Policy/BeforeScopeInterface.php

View workflow job for this annotation

GitHub Actions / cs-stan / Coding Standard & Static Analysis

Method \Authorization\Policy\BeforeScopeInterface::beforeScope() does not have native type hint for its parameter $resource but it should be possible to add it based on @param annotation "mixed".

Check failure on line 38 in src/Policy/BeforeScopeInterface.php

View workflow job for this annotation

GitHub Actions / cs-stan / Coding Standard & Static Analysis

Method \Authorization\Policy\BeforeScopeInterface::beforeScope() does not have native return type hint for its return value but it should be possible to add it based on @return annotation "mixed".
}
66 changes: 66 additions & 0 deletions tests/TestCase/AuthorizationServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Authorization\AuthorizationService;
use Authorization\IdentityDecorator;
use Authorization\Policy\BeforePolicyInterface;
use Authorization\Policy\BeforeScopeInterface;
use Authorization\Policy\Exception\MissingMethodException;
use Authorization\Policy\MapResolver;
use Authorization\Policy\OrmResolver;
Expand All @@ -30,6 +31,7 @@
use TestApp\Model\Table\ArticlesTable;
use TestApp\Policy\ArticlePolicy;
use TestApp\Policy\MagicCallPolicy;
use TypeError;

Check failure on line 34 in tests/TestCase/AuthorizationServiceTest.php

View workflow job for this annotation

GitHub Actions / cs-stan / Coding Standard & Static Analysis

Type TypeError is not used in this file.

class AuthorizationServiceTest extends TestCase
{
Expand Down Expand Up @@ -470,6 +472,70 @@ public function testBeforeResultFalse()
$this->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();
Expand Down

0 comments on commit 8184295

Please sign in to comment.