diff --git a/Neos.Flow/Classes/Annotations/Policy.php b/Neos.Flow/Classes/Annotations/Policy.php new file mode 100644 index 0000000000..bb659da462 --- /dev/null +++ b/Neos.Flow/Classes/Annotations/Policy.php @@ -0,0 +1,41 @@ +permission, PrivilegeInterface::ABSTAIN, PrivilegeInterface::DENY, PrivilegeInterface::GRANT), 1614931217); + } + } +} diff --git a/Neos.Flow/Classes/Package.php b/Neos.Flow/Classes/Package.php index 94405c7a34..88f37fa094 100644 --- a/Neos.Flow/Classes/Package.php +++ b/Neos.Flow/Classes/Package.php @@ -26,6 +26,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 @@ -160,5 +162,7 @@ public function boot(Core\Bootstrap $bootstrap) $annotationsCacheFlusher = $bootstrap->getObjectManager()->get(AnnotationsCacheFlusher::class); $annotationsCacheFlusher->flushConfigurationCachesByCompiledClass($classNames); }); + + $dispatcher->connect(PolicyService::class, 'configurationLoaded', PolicyAnnotationService::class, 'ammendPolicyConfiguration'); } } diff --git a/Neos.Flow/Classes/Security/Policy/PolicyAnnotationService.php b/Neos.Flow/Classes/Security/Policy/PolicyAnnotationService.php new file mode 100644 index 0000000000..ee75698131 --- /dev/null +++ b/Neos.Flow/Classes/Security/Policy/PolicyAnnotationService.php @@ -0,0 +1,65 @@ +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 + ]; + } + } + } + } +} diff --git a/Neos.Flow/Documentation/TheDefinitiveGuide/PartIII/Security.rst b/Neos.Flow/Documentation/TheDefinitiveGuide/PartIII/Security.rst index 82cfd01690..69eafb62d0 100644 --- a/Neos.Flow/Documentation/TheDefinitiveGuide/PartIII/Security.rst +++ b/Neos.Flow/Documentation/TheDefinitiveGuide/PartIII/Security.rst @@ -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 -------------------------- diff --git a/Neos.Flow/Tests/Unit/Security/Policy/PolicyAnnotationServiceTest.php b/Neos.Flow/Tests/Unit/Security/Policy/PolicyAnnotationServiceTest.php new file mode 100644 index 0000000000..c0e98823e6 --- /dev/null +++ b/Neos.Flow/Tests/Unit/Security/Policy/PolicyAnnotationServiceTest.php @@ -0,0 +1,106 @@ +policyAnnotationService = new PolicyAnnotationService(); + + $this->mockReflectionService = $this->getMockBuilder(ReflectionService::class)->disableOriginalConstructor()->getMock(); + $this->inject($this->policyAnnotationService, 'reflectionService', $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, + ); + } +}