From 2c4f01829bef696166a9e4028eef923472bc4916 Mon Sep 17 00:00:00 2001 From: Sybio Date: Sat, 20 Dec 2025 09:04:09 +0100 Subject: [PATCH] feat(twig): functions has_permission() and permission() --- CHANGELOG.md | 7 +++ README.md | 45 ++++++++++++++++ ROADMAP.md | 3 +- composer.json | 1 + src/Decision/PermissionDecision.php | 26 ++++++++++ src/Decision/PermissionDecisionFactory.php | 20 ++++++++ .../PermissionDecisionFactoryInterface.php | 15 ++++++ src/Decision/PermissionDecisionInterface.php | 14 +++++ .../PermissionBundleConfiguration.php | 33 ++++++++++++ src/Twig/HasPermissionTwigFunction.php | 40 +++++++++++++++ src/Twig/PermissionTwigFunction.php | 51 +++++++++++++++++++ 11 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 src/Decision/PermissionDecision.php create mode 100644 src/Decision/PermissionDecisionFactory.php create mode 100644 src/Decision/PermissionDecisionFactoryInterface.php create mode 100644 src/Decision/PermissionDecisionInterface.php create mode 100644 src/Twig/HasPermissionTwigFunction.php create mode 100644 src/Twig/PermissionTwigFunction.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 5336320..270cd2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.1.0] - 2025-12-27 + +### Added +- Twig extension `has_permission()` function to check permissions in templates (returns boolean) +- Twig extension `permission()` function to get detailed permission decision with violations +- PermissionDecisionInterface and PermissionDecision to represent permission validation results + ## [1.0.0] - 2025-12-19 ### Added diff --git a/README.md b/README.md index e7c1a2b..7349d50 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,51 @@ if ($violations->count() > 0) { } ``` +### Method 3: With Twig Extensions + +The bundle provides two Twig functions to check permissions directly in your templates. + +#### Function `has_permission()` + +This function returns a boolean indicating whether the permission is granted or not. + +**Usage**: +```twig +{% if has_permission('App\\Security\\Permission\\EditArticlePermission', articleId, userId) %} + +{% else %} +

You are not authorized to edit this article.

+{% endif %} +``` + +The function takes: +- First argument: The fully qualified class name of the permission (as a string) +- Remaining arguments: The arguments to pass to the permission's constructor + +#### Function `permission()` + +This function returns a decision object containing both the granted status and the violations list. + +**Usage**: +```twig +{% set decision = permission('App\\Security\\Permission\\EditArticlePermission', articleId, userId) %} + +{% if decision.granted %} + +{% else %} +

Access denied. Reasons:

+ +{% endif %} +``` + +The returned object has: +- `granted` (bool): Whether the permission is granted +- `violations` (ConstraintViolationListInterface): The list of violations if the permission was denied + ## Complete example Here's a complete example with a (little) more complex permission: diff --git a/ROADMAP.md b/ROADMAP.md index 1cad26a..9d1d050 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,9 +4,8 @@ This document outlines planned features and improvements for the Permission Bund ## Planned Features -### v1.1.0 (Planned) +### v1.2.0 (Planned) - **PHP Attribute HasPermission** : A PHP 8 attribute to simplify permission checks in controllers -- **Twig Extension has_permission** : A Twig function to check permissions directly in templates ## Ideas for Future Versions diff --git a/composer.json b/composer.json index 14e12e3..cdf5a27 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "symfony/dependency-injection": "^7.1", "symfony/http-kernel": "^7.1", "symfony/security-core": "^7.1", + "symfony/twig-bundle": "^7.1", "symfony/validator": "^7.1" }, "require-dev": { diff --git a/src/Decision/PermissionDecision.php b/src/Decision/PermissionDecision.php new file mode 100644 index 0000000..9d1ce55 --- /dev/null +++ b/src/Decision/PermissionDecision.php @@ -0,0 +1,26 @@ +granted; + } + + public function getViolations(): ConstraintViolationListInterface + { + return $this->violations; + } +} diff --git a/src/Decision/PermissionDecisionFactory.php b/src/Decision/PermissionDecisionFactory.php new file mode 100644 index 0000000..ab1cd24 --- /dev/null +++ b/src/Decision/PermissionDecisionFactory.php @@ -0,0 +1,20 @@ +addTag('security.voter'), ); + /** @see PermissionDecisionFactory */ + $container->setDefinition( + 'sybio_permission.result_factory', + (new Definition(PermissionDecisionFactory::class)) + ->setPublic(false), + ); + + /** @see HasPermissionTwigFunction */ + $container->setDefinition( + 'sybio_permission.twig.has_permission', + (new Definition(HasPermissionTwigFunction::class)) + ->setArgument(0, new Reference('security.authorization_checker')) + ->setArgument(1, $container->getParameter('sybio_permission.security_attribute')) + ->addTag('twig.extension') + ->setPublic(false), + ); + + /** @see PermissionTwigFunction */ + $container->setDefinition( + 'sybio_permission.twig.permission', + (new Definition(PermissionTwigFunction::class)) + ->setArgument(0, new Reference('security.authorization_checker')) + ->setArgument(1, new Reference('sybio_permission.validator')) + ->setArgument(2, new Reference('sybio_permission.result_factory')) + ->addTag('twig.extension') + ->setPublic(false), + ); + $container->setAlias(PermissionConstraintValidator::class, 'sybio_permission.constraint_validator'); $container->setAlias(PermissionValidatorInterface::class, 'sybio_permission.validator'); $container->setAlias(PermissionValidator::class, 'sybio_permission.validator'); $container->setAlias(PermissionVoter::class, 'sybio_permission.voter'); + $container->setAlias(PermissionDecisionFactoryInterface::class, 'sybio_permission.result_factory'); } public function getAlias(): string diff --git a/src/Twig/HasPermissionTwigFunction.php b/src/Twig/HasPermissionTwigFunction.php new file mode 100644 index 0000000..0ab59cf --- /dev/null +++ b/src/Twig/HasPermissionTwigFunction.php @@ -0,0 +1,40 @@ +hasPermission(...)), + ]; + } + + /** + * @param class-string $permissionClass + */ + public function hasPermission( + string $permissionClass, + mixed ...$arguments, + ): bool { + $permission = new $permissionClass(...$arguments); + + return $this->authorizationChecker->isGranted( + $permissionClass, + $permission, + ); + } +} diff --git a/src/Twig/PermissionTwigFunction.php b/src/Twig/PermissionTwigFunction.php new file mode 100644 index 0000000..941025b --- /dev/null +++ b/src/Twig/PermissionTwigFunction.php @@ -0,0 +1,51 @@ +permission(...)), + ]; + } + + /** + * @param class-string $permissionClass + */ + public function permission( + string $permissionClass, + mixed ...$arguments, + ): PermissionDecisionInterface { + $permission = new $permissionClass(...$arguments); + + $isGranted = $this->authorizationChecker->isGranted( + $permissionClass, + $permission, + ); + $violations = $this->permissionValidator->getLastViolations(); + + return $this->permissionDecisionFactory->createDecision( + $isGranted, + $violations, + ); + } +}