diff --git a/src/Auth/Eloquent/User.php b/src/Auth/Eloquent/User.php index 3df06431409..00cd79054a4 100644 --- a/src/Auth/Eloquent/User.php +++ b/src/Auth/Eloquent/User.php @@ -231,7 +231,15 @@ public function permissions() public function hasPermission($permission) { - return $this->permissions()->contains($permission); + $permissions = $this->permissions(); + + if ($permissions->contains($permission)) { + return true; + } + + return $permissions->contains(function ($userPermission) use ($permission) { + return $this->matchesWildcard($userPermission, $permission); + }); } public function makeSuper() @@ -402,4 +410,17 @@ public function getCurrentDirtyStateAttributes(): array 'email' => $this->email(), ], $this->model()->attributesToArray()); } + + protected function matchesWildcard(string $wildcardPermission, string $requestedPermission): bool + { + if (! str_contains($wildcardPermission, '*')) { + return false; + } + + $pattern = preg_quote($wildcardPermission, '/'); + $pattern = str_replace('\*', '.*', $pattern); + $pattern = '/^'.$pattern.'$/'; + + return (bool) preg_match($pattern, $requestedPermission); + } } diff --git a/src/Auth/File/Role.php b/src/Auth/File/Role.php index c02279463aa..902943153e9 100644 --- a/src/Auth/File/Role.php +++ b/src/Auth/File/Role.php @@ -98,7 +98,13 @@ public function removePermission($permission) public function hasPermission(string $permission): bool { - return $this->permissions->contains($permission); + if ($this->permissions->contains($permission)) { + return true; + } + + return $this->permissions->contains(function ($rolePermission) use ($permission) { + return $this->matchesWildcard($rolePermission, $permission); + }); } public function isSuper(): bool @@ -125,4 +131,17 @@ public function delete() RoleDeleted::dispatch($this); } + + protected function matchesWildcard(string $wildcardPermission, string $requestedPermission): bool + { + if (! str_contains($wildcardPermission, '*')) { + return false; + } + + $pattern = preg_quote($wildcardPermission, '/'); + $pattern = str_replace('\*', '.*', $pattern); + $pattern = '/^'.$pattern.'$/'; + + return (bool) preg_match($pattern, $requestedPermission); + } } diff --git a/src/Auth/File/User.php b/src/Auth/File/User.php index b848fb21371..71c25475c48 100644 --- a/src/Auth/File/User.php +++ b/src/Auth/File/User.php @@ -290,7 +290,15 @@ public function permissions() public function hasPermission($permission) { - return $this->permissions()->contains($permission); + $permissions = $this->permissions(); + + if ($permissions->contains($permission)) { + return true; + } + + return $permissions->contains(function ($userPermission) use ($permission) { + return $this->matchesWildcard($userPermission, $permission); + }); } public function makeSuper() @@ -377,4 +385,17 @@ public function getCurrentDirtyStateAttributes(): array 'super' => $this->get('super', false), ], $this->data()->toArray()); } + + protected function matchesWildcard(string $wildcardPermission, string $requestedPermission): bool + { + if (! str_contains($wildcardPermission, '*')) { + return false; + } + + $pattern = preg_quote($wildcardPermission, '/'); + $pattern = str_replace('\*', '.*', $pattern); + $pattern = '/^'.$pattern.'$/'; + + return (bool) preg_match($pattern, $requestedPermission); + } } diff --git a/src/Auth/UserGroup.php b/src/Auth/UserGroup.php index 75cc314fabe..56821776c9d 100644 --- a/src/Auth/UserGroup.php +++ b/src/Auth/UserGroup.php @@ -133,9 +133,17 @@ public function hasRole($role): bool public function hasPermission($permission) { - return $this->roles->reduce(function ($carry, $role) { + $permissions = $this->roles->reduce(function ($carry, $role) { return $carry->merge($role->permissions()); - }, collect())->contains($permission); + }, collect()); + + if ($permissions->contains($permission)) { + return true; + } + + return $permissions->contains(function ($groupPermission) use ($permission) { + return $this->matchesWildcard($groupPermission, $permission); + }); } public function isSuper(): bool @@ -201,4 +209,17 @@ public function blueprint() { return Facades\UserGroup::blueprint(); } + + protected function matchesWildcard(string $wildcardPermission, string $requestedPermission): bool + { + if (! str_contains($wildcardPermission, '*')) { + return false; + } + + $pattern = preg_quote($wildcardPermission, '/'); + $pattern = str_replace('\*', '.*', $pattern); + $pattern = '/^'.$pattern.'$/'; + + return (bool) preg_match($pattern, $requestedPermission); + } } diff --git a/tests/Auth/PermissibleContractTests.php b/tests/Auth/PermissibleContractTests.php index 816036a03f7..9dd337b6f52 100644 --- a/tests/Auth/PermissibleContractTests.php +++ b/tests/Auth/PermissibleContractTests.php @@ -363,4 +363,85 @@ public function it_sets_all_the_groups() 'c' => 'c', ], $user->groups()->map->handle()->all()); } + + #[Test] + public function it_checks_wildcard_permission_with_asterisk_at_beginning() + { + $role = RoleAPI::make('test')->addPermission('* blog entries'); + RoleAPI::shouldReceive('find')->with('test')->andReturn($role); + RoleAPI::shouldReceive('all')->andReturn(collect([$role])); + + $user = $this->createPermissible()->assignRole($role); + $user->save(); + + $this->assertTrue($user->hasPermission('view blog entries')); + $this->assertTrue($user->hasPermission('edit blog entries')); + $this->assertTrue($user->hasPermission('delete blog entries')); + $this->assertFalse($user->hasPermission('view news entries')); + $this->assertFalse($user->hasPermission('view blog posts')); + } + + #[Test] + public function it_checks_wildcard_permission_with_asterisk_in_middle() + { + $role = RoleAPI::make('test')->addPermission('view * entries'); + RoleAPI::shouldReceive('find')->with('test')->andReturn($role); + RoleAPI::shouldReceive('all')->andReturn(collect([$role])); + + $user = $this->createPermissible()->assignRole($role); + $user->save(); + + $this->assertTrue($user->hasPermission('view blog entries')); + $this->assertTrue($user->hasPermission('view news entries')); + $this->assertTrue($user->hasPermission('view products entries')); + $this->assertFalse($user->hasPermission('edit blog entries')); + $this->assertFalse($user->hasPermission('view blog posts')); + } + + #[Test] + public function it_checks_wildcard_permission_through_user_group() + { + $role = RoleAPI::make('test')->addPermission('view * entries'); + $userGroup = (new UserGroup)->handle('testgroup')->assignRole($role); + + RoleAPI::shouldReceive('find')->with('test')->andReturn($role); + RoleAPI::shouldReceive('all')->andReturn(collect([$role])); + UserGroupAPI::shouldReceive('find')->with('testgroup')->andReturn($userGroup); + UserGroupAPI::shouldReceive('all')->andReturn(collect([$userGroup])); + + $user = $this->createPermissible()->addToGroup($userGroup); + $user->save(); + + $this->assertTrue($user->hasPermission('view blog entries')); + $this->assertTrue($user->hasPermission('view news entries')); + $this->assertTrue($user->hasPermission('view products entries')); + $this->assertFalse($user->hasPermission('edit blog entries')); + $this->assertFalse($user->hasPermission('view blog posts')); + } + + #[Test] + public function it_checks_multiple_wildcard_permissions_from_different_sources() + { + $directRole = RoleAPI::make('direct')->addPermission('view * entries'); + $groupRole = RoleAPI::make('grouprole')->addPermission('* blog entries'); + $userGroup = (new UserGroup)->handle('testgroup')->assignRole($groupRole); + + RoleAPI::shouldReceive('find')->with('direct')->andReturn($directRole); + RoleAPI::shouldReceive('find')->with('grouprole')->andReturn($groupRole); + RoleAPI::shouldReceive('all')->andReturn(collect([$directRole, $groupRole])); + UserGroupAPI::shouldReceive('find')->with('testgroup')->andReturn($userGroup); + UserGroupAPI::shouldReceive('all')->andReturn(collect([$userGroup])); + + $user = $this->createPermissible() + ->assignRole($directRole) + ->addToGroup($userGroup); + $user->save(); + + $this->assertTrue($user->hasPermission('view blog entries')); + $this->assertTrue($user->hasPermission('view news entries')); + $this->assertTrue($user->hasPermission('edit blog entries')); + $this->assertTrue($user->hasPermission('delete blog entries')); + $this->assertFalse($user->hasPermission('delete news entries')); + $this->assertFalse($user->hasPermission('view blog posts')); + } } diff --git a/tests/Auth/RoleTest.php b/tests/Auth/RoleTest.php index dd98910c6d5..13ccb76dc0b 100644 --- a/tests/Auth/RoleTest.php +++ b/tests/Auth/RoleTest.php @@ -94,6 +94,45 @@ public function it_checks_if_it_has_permission() $this->assertFalse($role->hasPermission('bar')); } + #[Test] + public function it_checks_wildcard_permission_with_asterisk_at_beginning() + { + $role = (new Role)->addPermission('* blog entries'); + + $this->assertTrue($role->hasPermission('view blog entries')); + $this->assertTrue($role->hasPermission('edit blog entries')); + $this->assertTrue($role->hasPermission('delete blog entries')); + $this->assertFalse($role->hasPermission('view news entries')); + $this->assertFalse($role->hasPermission('view blog posts')); + } + + #[Test] + public function it_checks_wildcard_permission_with_asterisk_in_middle() + { + $role = (new Role)->addPermission('view * entries'); + + $this->assertTrue($role->hasPermission('view blog entries')); + $this->assertTrue($role->hasPermission('view news entries')); + $this->assertTrue($role->hasPermission('view products entries')); + $this->assertFalse($role->hasPermission('edit blog entries')); + $this->assertFalse($role->hasPermission('view blog posts')); + } + + #[Test] + public function it_checks_multiple_wildcard_permissions() + { + $role = (new Role) + ->addPermission('view * entries') + ->addPermission('* blog entries'); + + $this->assertTrue($role->hasPermission('view blog entries')); + $this->assertTrue($role->hasPermission('view news entries')); + $this->assertTrue($role->hasPermission('edit blog entries')); + $this->assertTrue($role->hasPermission('delete blog entries')); + $this->assertFalse($role->hasPermission('delete news entries')); + $this->assertFalse($role->hasPermission('view blog posts')); + } + #[Test] public function it_checks_if_it_has_super_permissions() { diff --git a/tests/Auth/UserGroupTest.php b/tests/Auth/UserGroupTest.php index 2d48b1bce1f..5db9d93ac22 100644 --- a/tests/Auth/UserGroupTest.php +++ b/tests/Auth/UserGroupTest.php @@ -271,6 +271,87 @@ public function permissions($permissions = null) $this->assertFalse($group->hasPermission('two')); } + #[Test] + public function it_checks_wildcard_permission_with_asterisk_at_beginning() + { + $role = new class extends Role + { + public function permissions($permissions = null) + { + return collect(['* blog entries']); + } + }; + + $group = UserGroup::make()->assignRole($role); + + $this->assertTrue($group->hasPermission('view blog entries')); + $this->assertTrue($group->hasPermission('edit blog entries')); + $this->assertTrue($group->hasPermission('delete blog entries')); + $this->assertFalse($group->hasPermission('view news entries')); + $this->assertFalse($group->hasPermission('view blog posts')); + } + + #[Test] + public function it_checks_wildcard_permission_with_asterisk_in_middle() + { + $role = new class extends Role + { + public function permissions($permissions = null) + { + return collect(['view * entries']); + } + }; + + $group = UserGroup::make()->assignRole($role); + + $this->assertTrue($group->hasPermission('view blog entries')); + $this->assertTrue($group->hasPermission('view news entries')); + $this->assertTrue($group->hasPermission('view products entries')); + $this->assertFalse($group->hasPermission('edit blog entries')); + $this->assertFalse($group->hasPermission('view blog posts')); + } + + #[Test] + public function it_checks_multiple_roles_with_wildcard_permissions() + { + $roleOne = new class extends Role + { + public function handle(?string $handle = null) + { + return 'role_one'; + } + + public function permissions($permissions = null) + { + return collect(['view * entries']); + } + }; + + $roleTwo = new class extends Role + { + public function handle(?string $handle = null) + { + return 'role_two'; + } + + public function permissions($permissions = null) + { + return collect(['* blog entries']); + } + }; + + $group = UserGroup::make() + ->assignRole($roleOne) + ->assignRole($roleTwo); + + $this->assertTrue($group->hasPermission('view blog entries')); + $this->assertTrue($group->hasPermission('view news entries')); + $this->assertTrue($group->hasPermission('edit blog entries')); + $this->assertTrue($group->hasPermission('delete blog entries')); + $this->assertFalse($group->hasPermission('delete news entries')); + $this->assertFalse($group->hasPermission('view blog posts')); + } + #[Test] public function it_checks_if_it_has_super_permissions() {