From aae2662c0427aac054c2f282da3753854f5da434 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Wed, 9 Sep 2026 21:35:51 +0100 Subject: [PATCH] =?UTF-8?q?feat(auth)=20:=20prise=20en=20charge=20des=20ca?= =?UTF-8?q?ract=C3=A8res=20g=C3=A9n=C3=A9riques=20hi=C3=A9rarchiques=20pou?= =?UTF-8?q?r=20les=20autorisations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajout de la correspondance hiérarchique des caractères génériques pour les autorisations Schild. - Prise en charge des caractères génériques emboîtés en fin de chaîne, tels que forum.posts.* - Prise en charge des caractères génériques au milieu d'une chaîne, tels que forum.*.create - Partage de la correspondance des caractères génériques entre les vérifications d'autorisations des utilisateurs et celles des groupes --- src/Authorization/PermissionMatcher.php | 135 ++++++++++++++++++++++ src/Authorization/Traits/Authorizable.php | 22 +--- src/Entities/Group.php | 14 +-- 3 files changed, 144 insertions(+), 27 deletions(-) create mode 100644 src/Authorization/PermissionMatcher.php diff --git a/src/Authorization/PermissionMatcher.php b/src/Authorization/PermissionMatcher.php new file mode 100644 index 0000000..0eecf3a --- /dev/null +++ b/src/Authorization/PermissionMatcher.php @@ -0,0 +1,135 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace BlitzPHP\Schild\Authorization; + +/** + * Compare les autorisations accordées aux noms des autorisations demandées pour les mécanismes internes d'autorisation de Schild. + */ +final class PermissionMatcher +{ + /** + * @param list $grants + */ + public static function matches(string $permission, array $grants): bool + { + $permissionSegments = self::splitIfValid($permission); + + if ($permissionSegments === null) { + return false; + } + + $permissionSegmentCount = count($permissionSegments); + + foreach ($grants as $grant) { + if ($grant === $permission) { + return true; + } + + $wildcardPosition = strpos($grant, '*'); + + if ($wildcardPosition === false) { + continue; + } + + if ( + $wildcardPosition > 0 + && $wildcardPosition === strlen($grant) - 1 + && $grant[$wildcardPosition - 1] === '.' + ) { + $prefix = substr($grant, 0, $wildcardPosition - 1); + + if (self::isLiteral($prefix) && str_starts_with($permission, $prefix . '.')) { + return true; + } + + continue; + } + + $grantSegments = self::splitIfValid($grant); + + if ($grantSegments === null) { + continue; + } + + if (self::matchesWildcardGrant($grantSegments, $permissionSegments, $permissionSegmentCount)) { + return true; + } + } + + return false; + } + + /** + * @param list $grantSegments + * @param list $permissionSegments + */ + private static function matchesWildcardGrant(array $grantSegments, array $permissionSegments, int $permissionSegmentCount): bool + { + $grantSegmentCount = count($grantSegments); + + if ($grantSegments[$grantSegmentCount - 1] === '*') { + $grantSegmentCount--; + + return $permissionSegmentCount > $grantSegmentCount + && self::segmentsMatch($grantSegments, $permissionSegments, $grantSegmentCount); + } + + return $permissionSegmentCount === $grantSegmentCount + && self::segmentsMatch($grantSegments, $permissionSegments, $grantSegmentCount); + } + + /** + * @param list $grantSegments + * @param list $permissionSegments + */ + private static function segmentsMatch(array $grantSegments, array $permissionSegments, int $segmentCount): bool + { + for ($index = 0; $index < $segmentCount; $index++) { + if ($grantSegments[$index] !== '*' && $grantSegments[$index] !== $permissionSegments[$index]) { + return false; + } + } + + return true; + } + + /** + * @return list|null + */ + private static function splitIfValid(string $permission): ?array + { + if ($permission === '' || str_starts_with($permission, '*')) { + return null; + } + + $segments = explode('.', $permission); + + foreach ($segments as $segment) { + if ($segment === '' || ($segment !== '*' && str_contains($segment, '*'))) { + return null; + } + } + + return $segments; + } + + private static function isLiteral(string $permission): bool + { + return $permission !== '' + && ! str_contains($permission, '*') + && ! str_contains($permission, '..') + && ! str_starts_with($permission, '.') + && ! str_ends_with($permission, '.'); + } +} diff --git a/src/Authorization/Traits/Authorizable.php b/src/Authorization/Traits/Authorizable.php index 6194034..35d8686 100644 --- a/src/Authorization/Traits/Authorizable.php +++ b/src/Authorization/Traits/Authorizable.php @@ -13,6 +13,7 @@ namespace BlitzPHP\Schild\Authorization\Traits; +use BlitzPHP\Schild\Authorization\PermissionMatcher; use BlitzPHP\Schild\Exceptions\AuthorizationException; use BlitzPHP\Schild\Exceptions\LogicException; use BlitzPHP\Schild\Models\GroupModel; @@ -239,7 +240,7 @@ public function hasPermission(string $permission): bool * Vérifie les autorisations d'utilisateur et leurs autorisations de groupe * pour voir si l'utilisateur dispose d'une autorisation spécifique. * - * @param string $permission chaînes composées d'une portée et d'une action, comme les users.create + * @param string $permissions Chaîne(s) d'autorisation, généralement séparées par des points, comme `users.create` */ public function can(string ...$permissions): bool { @@ -253,18 +254,10 @@ public function can(string ...$permissions): bool $matrix = parametre('auth-groups.matrix'); foreach ($permissions as $permission) { - // L'autorisation doit contenir une portée et une action - if (! str_contains($permission, '.')) { - throw new LogicException( - 'Une autorisation doit être une chaîne composée d\'une portée et d\'une action, comme `users.create`.' - . ' Autorisation non valide: ' . $permission, - ); - } - $permission = strtolower($permission); // Vérifier les autorisations de l'utilisateur - if (in_array($permission, $this->permissionsCache, true)) { + if (PermissionMatcher::matches($permission, $this->permissionsCache)) { return true; } @@ -273,14 +266,7 @@ public function can(string ...$permissions): bool } foreach ($this->groupCache as $group) { - // Vérifier correspondance exacte - if (isset($matrix[$group]) && in_array($permission, $matrix[$group], true)) { - return true; - } - - // Vérifier match joker - $check = substr($permission, 0, strpos($permission, '.')) . '.*'; - if (isset($matrix[$group]) && in_array($check, $matrix[$group], true)) { + if (isset($matrix[$group]) && PermissionMatcher::matches($permission, $matrix[$group])) { return true; } } diff --git a/src/Entities/Group.php b/src/Entities/Group.php index a6d4958..18f6295 100644 --- a/src/Entities/Group.php +++ b/src/Entities/Group.php @@ -13,6 +13,8 @@ namespace BlitzPHP\Schild\Entities; +use BlitzPHP\Schild\Authorization\PermissionMatcher; + /** * Représente un seul groupe d'utilisateurs et fournit des fonctions utilitaires. */ @@ -75,15 +77,9 @@ public function can(string $permission): bool { $this->populatePermissions(); - // Vérifier la correspondance exacte - if (! empty($this->permissions) && in_array($permission, $this->permissions, true)) { - return true; - } - - // Vérifier la correspondance générique - $check = substr($permission, 0, strpos($permission, '.')) . '.*'; - - return ! empty($this->permissions) && in_array($check, $this->permissions, true); + return $this->permissions !== null + && $this->permissions !== [] + && PermissionMatcher::matches($permission, $this->permissions); } /**