Skip to content

Commit

Permalink
Merge branch 'feature/policyAnnotations' into feature/routeAnnotations
Browse files Browse the repository at this point in the history
  • Loading branch information
mficzel committed Mar 15, 2024
2 parents 096ae8e + 4e8b5c7 commit 372d18a
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 0 deletions.
41 changes: 41 additions & 0 deletions Neos.Flow/Classes/Annotations/Policy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);

namespace Neos\Flow\Annotations;

/*
* This file is part of the Neos.Flow package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Neos\Flow\Security\Authorization\Privilege\PrivilegeInterface;

/**
* Adds a policy configuration to a method
*
* This is a convenient way to add policies in project code
* but should not be used in libraries/packages that shall be
* configured for different use cases.
*
* @Annotation
* @NamedArgumentConstructor
* @Target({"METHOD"})
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
final class Policy
{
public function __construct(
public readonly string $role,
public readonly string $permission = 'grant',
) {
if (!in_array($permission, [PrivilegeInterface::ABSTAIN, PrivilegeInterface::DENY, PrivilegeInterface::GRANT])) {
throw new \InvalidArgumentException(sprintf('Permission value "%s" is invalid. Allowed values are "%s", "%s" and "%s"', $this->permission, PrivilegeInterface::ABSTAIN, PrivilegeInterface::DENY, PrivilegeInterface::GRANT), 1614931217);
}
}
}
4 changes: 4 additions & 0 deletions Neos.Flow/Classes/Package.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
use Neos\Flow\Security\Authentication\TokenInterface;
use Neos\Flow\Security\Context;
use Neos\Flow\Security\Cryptography\PrecomposedHashProvider;
use Neos\Flow\Security\Policy\PolicyAnnotationService;
use Neos\Flow\Security\Policy\PolicyService;

/**
* The Flow Package
Expand Down Expand Up @@ -162,5 +164,7 @@ public function boot(Core\Bootstrap $bootstrap)
$annotationsCacheFlusher->registerAnnotation(Route::class, ['Flow_Mvc_Routing_Route', 'Flow_Mvc_Routing_Resolve']);
$annotationsCacheFlusher->flushConfigurationCachesByCompiledClass($classNames);
});

$dispatcher->connect(PolicyService::class, 'configurationLoaded', PolicyAnnotationService::class, 'ammendPolicyConfiguration');
}
}
54 changes: 54 additions & 0 deletions Neos.Flow/Classes/Security/Policy/PolicyAnnotationService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);

namespace Neos\Flow\Security\Policy;

/*
* This file is part of the Neos.Flow package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Reflection\ReflectionService;
use Neos\Flow\Security\Authorization\Privilege\Method\MethodPrivilege;

class PolicyAnnotationService
{
public function __construct(
public readonly ReflectionService $reflectionService
) {
}

/**
* Add policy configuration for Flow\Policy annotations and attributes
*/
public function ammendPolicyConfiguration(array &$policyConfiguration): void
{
$annotatedClasses = $this->reflectionService->getClassesContainingMethodsAnnotatedWith(Flow\Policy::class);
foreach ($annotatedClasses as $className) {
$annotatedMethods = $this->reflectionService->getMethodsAnnotatedWith($className, Flow\Policy::class);
// avoid methods beeing called multiple times when attributes are assigned more than once
$annotatedMethods = array_unique($annotatedMethods);
foreach ($annotatedMethods as $methodName) {
/**
* @var Flow\Policy[] $annotations
*/
$annotations = $this->reflectionService->getMethodAnnotations($className, $methodName, Flow\Policy::class);
$privilegeTargetMatcher = sprintf('method(%s->%s())', $className, $methodName);
$privilegeTargetIdentifier = 'FromPhpAttribute:' . (str_replace('\\', '.', $className)) . ':'. $methodName . ':'. md5($privilegeTargetMatcher);
$policyConfiguration['privilegeTargets'][MethodPrivilege::class][$privilegeTargetIdentifier] = ['matcher' => $privilegeTargetMatcher];
foreach ($annotations as $annotation) {
$policyConfiguration['roles'][$annotation->role]['privileges'][] = [
'privilegeTarget' => $privilegeTargetIdentifier,
'permission' => $annotation->permission
];
}
}
}
}
}
38 changes: 38 additions & 0 deletions Neos.Flow/Documentation/TheDefinitiveGuide/PartIII/Security.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1105,6 +1105,44 @@ By defining privilege targets, all matched subjects (methods, entities, etc.) wi
permissions to allow access to them for certain roles. The use of a DENY permission should be the ultimate last
resort for edge cases. Be careful, there is no way to override a DENY permission, if you use it anyways!

Using policy attributes / annotations
-------------------------------------

The ``Flow\Policy`` Attribute allows to specify method privileges directly by annotating the php code of the affected method.

.. note::

While this is a very convenient way to add policies in project code it should not be used in libraries/packages
that expect to be configured for the outside. In such cases the policy.yaml is still preferred as it is easier
to overwrite.

.. code-block:: php
use Neos\Flow\Mvc\Controller\ActionController;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Security\Authorization\Privilege\PrivilegeInterface;
class ExampleController extends ActionController
{
/**
* By assigning a policy with a role argument access to the method is granted to the specified role
*/
#[Flow\Policy(role: 'Neos.Flow:Everybody')]
public function everybodyAction(): void
{
}
/**
* By specifying the permission in addition and the DENY and ABSTAIN can be configured aswell
* Flow\Policy attributes can be assigned multiple times if multiple roles are to be configured
*/
#[Flow\Policy(role: 'Neos.Flow:Administrator', permission: PrivilegeInterface::GRANT)]
#[Flow\Policy(role: 'Neos.Flow:Anonymous', permission: PrivilegeInterface::DENY)]
public function adminButNotAnonymousAction(): void
{
}
}
Using privilege parameters
--------------------------

Expand Down
106 changes: 106 additions & 0 deletions Neos.Flow/Tests/Unit/Security/Policy/PolicyAnnotationServiceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php
namespace Neos\Flow\Tests\Unit\Security\Policy;

/*
* This file is part of the Neos.Flow package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Neos\Flow\Annotations\Policy;
use Neos\Flow\Reflection\ReflectionService;
use Neos\Flow\Security\Authorization\Privilege\Method\MethodPrivilege;
use Neos\Flow\Security\Authorization\Privilege\PrivilegeInterface;
use Neos\Flow\Security\Policy\PolicyAnnotationService;
use Neos\Flow\Tests\UnitTestCase;

/**
* Testcase for the PolicyAnnotationService
*/
class PolicyAnnotationServiceTest extends UnitTestCase
{
/**
* @var PolicyAnnotationService
*/
protected $policyAnnotationService;

/**
* @var ReflectionService|\PHPUnit\Framework\MockObject\MockObject
*/
protected $mockReflectionService;

protected function setUp(): void
{
$this->mockReflectionService = $this->getMockBuilder(ReflectionService::class)->disableOriginalConstructor()->getMock();
$this->policyAnnotationService = new PolicyAnnotationService(
$this->mockReflectionService
);
}

/**
* @test
*/
public function policyConfigurationIsNotModifiedIfNoAnnotationsAreFound()
{
$this->mockReflectionService->expects($this->once())
->method('getClassesContainingMethodsAnnotatedWith')
->with(Policy::class)
->willReturn([]);

$policyConfiguration = [];

$this->policyAnnotationService->ammendPolicyConfiguration($policyConfiguration);

$this->assertSame(
[],
$policyConfiguration,
);
}

/**
* @test
*/
public function policyConfigurationIsCreatedForAnnotationsCreated()
{
$this->mockReflectionService->expects($this->once())
->method('getClassesContainingMethodsAnnotatedWith')
->with(Policy::class)
->willReturn(['Vendor\Example']);

$this->mockReflectionService->expects($this->once())
->method('getMethodsAnnotatedWith')
->with('Vendor\Example', Policy::class)
->willReturn(['annotatedMethod']);

$this->mockReflectionService->expects($this->once())
->method('getMethodAnnotations')
->with('Vendor\Example', 'annotatedMethod', Policy::class)
->willReturn([new Policy('Neos.Flow:Administrator'), new Policy('Neos.Flow:Anonymous', PrivilegeInterface::DENY)]);

$policyConfiguration = [];

$this->policyAnnotationService->ammendPolicyConfiguration($policyConfiguration);
$expectedTargetId = 'FromPhpAttribute:Vendor.Example:annotatedMethod:' . md5('method(Vendor\Example->annotatedMethod())');

$this->assertSame(
[
'privilegeTargets' => [
MethodPrivilege::class => [
$expectedTargetId => [
'matcher' => 'method(Vendor\Example->annotatedMethod())'
]
]
],
'roles' => [
'Neos.Flow:Administrator' => ['privileges' => [['privilegeTarget'=> $expectedTargetId, 'permission' => 'grant']]],
'Neos.Flow:Anonymous' => ['privileges' => [['privilegeTarget'=> $expectedTargetId, 'permission' => 'deny']]]
]
],
$policyConfiguration,
);
}
}

0 comments on commit 372d18a

Please sign in to comment.