From b3551c70554d7ac3a6f8c354c5d472762f96eabb Mon Sep 17 00:00:00 2001 From: Cyril de Wit Date: Thu, 27 Dec 2018 17:12:51 +0100 Subject: [PATCH 0001/1013] style: fix styling in permission config file --- config/permission.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/permission.php b/config/permission.php index c675dce77..fbc6fddb7 100644 --- a/config/permission.php +++ b/config/permission.php @@ -80,6 +80,7 @@ * For example, this would be nice if your primary keys are all UUIDs. In * that case, name this `model_uuid`. */ + 'model_morph_key' => 'model_id', ], @@ -122,6 +123,7 @@ * role caching using any of the `store` drivers listed in the cache.php config * file. Using 'default' here means to use the `default` set in cache.php. */ + 'store' => 'default', ], ]; From a82c0d656dd1c58fce0b9f2073f662301e2939c9 Mon Sep 17 00:00:00 2001 From: Carlos Campello <44181666+chbbc@users.noreply.github.com> Date: Fri, 18 Jan 2019 09:22:08 -0300 Subject: [PATCH 0002/1013] =?UTF-8?q?It's=20more=20global=20using=20app()?= =?UTF-8?q?=20fa=C3=A7ade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 36921dc49..4a11e5762 100644 --- a/README.md +++ b/README.md @@ -899,7 +899,7 @@ HOWEVER, if you manipulate permission/role data directly in the database instead ### Manual cache reset To manually reset the cache for this package, you can run the following in your app code: ```php -$this->app->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions(); +app()->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions(); ``` Or you can use an Artisan command: From 9023a2d9d906ef4c5acf9cf7f086b436a6d70c34 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 28 Jan 2019 16:32:11 -0500 Subject: [PATCH 0003/1013] Change cache time to DateInterval instead of integer for minutes This is in preparation for compatibility with Laravel 5.8's cache TTL change to seconds instead of minutes. NOTE: If you leave your existing `config/permission.php` file alone, then with Laravel 5.8 the `60 * 24` will change from being treated as 24 hours to just 24 minutes. Depending on your app, this may or may not make a significant difference. Updating your config file to a specific DateInterval will add specificity and insulate you from the TTL change in Laravel 5.8. Refs: https://laravel-news.com/cache-ttl-change-coming-to-laravel-5-8 https://github.com/laravel/framework/commit/fd6eb89b62ec09df1ffbee164831a827e83fa61d --- README.md | 7 ++++--- config/permission.php | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4a11e5762..a626f5432 100644 --- a/README.md +++ b/README.md @@ -198,11 +198,11 @@ return [ 'cache' => [ /* - * By default all permissions will be cached for 24 hours unless a permission or - * role is updated. Then the cache will be flushed immediately. + * By default all permissions are cached for 24 hours to speed up performance. + * When permissions or roles are updated the cache is flushed automatically. */ - 'expiration_time' => 60 * 24, + 'expiration_time' => \DateInterval::createFromDateString('24 hours'), /* * The key to use when tagging and prefixing entries in the cache. @@ -226,6 +226,7 @@ return [ * role caching using any of the `store` drivers listed in the cache.php config * file. Using 'default' here means to use the `default` set in cache.php. */ + 'store' => 'default', ], ]; diff --git a/config/permission.php b/config/permission.php index fbc6fddb7..fbf9b898f 100644 --- a/config/permission.php +++ b/config/permission.php @@ -95,11 +95,11 @@ 'cache' => [ /* - * By default all permissions will be cached for 24 hours unless a permission or - * role is updated. Then the cache will be flushed immediately. + * By default all permissions are cached for 24 hours to speed up performance. + * When permissions or roles are updated the cache is flushed automatically. */ - 'expiration_time' => 60 * 24, + 'expiration_time' => \DateInterval::createFromDateString('24 hours'), /* * The key to use when tagging and prefixing entries in the cache. From 5cbe439b0b10c57b0bea919f95698ef77e8a12ea Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 28 Jan 2019 16:34:56 -0500 Subject: [PATCH 0004/1013] Update CHANGELOG.md --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52a5e018f..c925bf4be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to `laravel-permission` will be documented in this file +## 2.30.0 - 2019-01-28 +- Change cache config time to DateInterval instead of integer + +This is in preparation for compatibility with Laravel 5.8's cache TTL change to seconds instead of minutes. + +NOTE: If you leave your existing `config/permission.php` file alone, then with Laravel 5.8 the `60 * 24` will change from being treated as 24 hours to just 24 minutes. Depending on your app, this may or may not make a significant difference. Updating your config file to a specific DateInterval will add specificity and insulate you from the TTL change in Laravel 5.8. + +Refs: + +https://laravel-news.com/cache-ttl-change-coming-to-laravel-5-8 +https://github.com/laravel/framework/commit/fd6eb89b62ec09df1ffbee164831a827e83fa61d + ## 2.29.0 - 2018-12-15 - Fix bound `saved` event from firing on all subsequent models when calling assignRole or givePermissionTo on unsaved models. However, it is preferable to save the model first, and then add roles/permissions after saving. See #971. From 39bc4c502beede040c9dc2d307d6b41b74927c02 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 28 Jan 2019 17:11:10 -0500 Subject: [PATCH 0005/1013] Backport to support Laravel 5.4 Since Carbon wasn't already part of L5.4, we can't depend on its presence. And, given that caching at this layer doesn't need to be micro-targeted, general approximate conversions are used for DateInterval translations. --- src/PermissionRegistrar.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 9e2c249d0..91fb6eeed 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -27,7 +27,7 @@ class PermissionRegistrar /** @var string */ protected $roleClass; - /** @var int */ + /** @var DateInterval|int */ public static $cacheExpirationTime; /** @var string */ @@ -58,6 +58,14 @@ public function __construct(Gate $gate, CacheManager $cacheManager) protected function initializeCache() { self::$cacheExpirationTime = config('permission.cache.expiration_time', config('permission.cache_expiration_time')); + + if (app()->version() <= '5.5') { + if (self::$cacheExpirationTime instanceof \DateInterval) { + $interval = self::$cacheExpirationTime; + self::$cacheExpirationTime = $interval->m * 30 * 60 * 24 + $interval->d * 60 * 24 + $interval->h * 60 + $interval->i; + } + } + self::$cacheKey = config('permission.cache.key'); self::$cacheModelKey = config('permission.cache.model_key'); From d221a947302f0557b45c9a19bfc42f05c07dc960 Mon Sep 17 00:00:00 2001 From: Te7a-Houdini Date: Fri, 1 Feb 2019 18:45:28 +0200 Subject: [PATCH 0006/1013] Modify using helper methods to facades --- src/Traits/HasPermissions.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 79c6fd632..1f1ab3495 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -2,6 +2,7 @@ namespace Spatie\Permission\Traits; +use Illuminate\Support\Arr; use Spatie\Permission\Guard; use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Builder; @@ -96,7 +97,7 @@ protected function convertToPermissionModels($permissions): array $permissions = $permissions->all(); } - $permissions = array_wrap($permissions); + $permissions = Arr::wrap($permissions); return array_map(function ($permission) { if ($permission instanceof Permission) { @@ -427,7 +428,8 @@ function ($object) use ($permissions, $model) { $object->permissions()->sync($permissions, false); $object->load('permissions'); $modelLastFiredOn = $object; - }); + } + ); } $this->forgetCachedPermissions(); From b9b97c6793858e06591d674215acd51b30b2e315 Mon Sep 17 00:00:00 2001 From: mohamed abdallah Date: Sun, 3 Feb 2019 09:09:06 +0200 Subject: [PATCH 0007/1013] add custom guard query to role scope get users with specific roles and specific guard not default guard. --- src/Traits/HasRoles.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index bdf13c539..74f74ae41 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -56,7 +56,7 @@ public function roles(): MorphToMany * * @return \Illuminate\Database\Eloquent\Builder */ - public function scopeRole(Builder $query, $roles): Builder + public function scopeRole(Builder $query, $roles, $guard = null): Builder { if ($roles instanceof Collection) { $roles = $roles->all(); @@ -66,14 +66,15 @@ public function scopeRole(Builder $query, $roles): Builder $roles = [$roles]; } - $roles = array_map(function ($role) { + $roles = array_map(function ($role) use ($guard) { if ($role instanceof Role) { return $role; } $method = is_numeric($role) ? 'findById' : 'findByName'; + $guard = $guard ?: $this->getDefaultGuardName(); - return $this->getRoleClass()->{$method}($role, $this->getDefaultGuardName()); + return $this->getRoleClass()->{$method}($role, $guard); }, $roles); return $query->whereHas('roles', function ($query) use ($roles) { From 8b740dd7d0a109ac245238884059e7226eeb8934 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 3 Feb 2019 21:29:00 -0500 Subject: [PATCH 0008/1013] Update HasPermissions.php Maintains L5.3 compatibility for the time being. (`Arr::wrap` didn't exist until 5.4) --- src/Traits/HasPermissions.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 1f1ab3495..dd44fb3f2 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -2,7 +2,6 @@ namespace Spatie\Permission\Traits; -use Illuminate\Support\Arr; use Spatie\Permission\Guard; use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Builder; @@ -97,7 +96,7 @@ protected function convertToPermissionModels($permissions): array $permissions = $permissions->all(); } - $permissions = Arr::wrap($permissions); + $permissions = is_array($permissions) ? $permissions : [$permissions]; return array_map(function ($permission) { if ($permission instanceof Permission) { From 0889a29ce52bc4e8078d86dea87fa48faa3243ef Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 3 Feb 2019 22:13:42 -0500 Subject: [PATCH 0009/1013] Add tests for #1017 --- src/Traits/HasRoles.php | 1 + tests/HasRolesTest.php | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 74f74ae41..1aa8d43ee 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -53,6 +53,7 @@ public function roles(): MorphToMany * * @param \Illuminate\Database\Eloquent\Builder $query * @param string|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles + * @param string $guard * * @return \Illuminate\Database\Eloquent\Builder */ diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 58cce0283..8f8a11ce0 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -339,6 +339,32 @@ public function it_can_scope_users_using_an_object() $this->assertEquals($scopedUsers3->count(), 1); } + /** @test */ + public function it_can_scope_against_a_specific_guard() + { + $user1 = User::create(['email' => 'user1@test.com']); + $user2 = User::create(['email' => 'user2@test.com']); + $user1->assignRole('testRole'); + $user2->assignRole('testRole2'); + + $scopedUsers1 = User::role('testRole', 'web')->get(); + + $this->assertEquals($scopedUsers1->count(), 1); + + $user3 = Admin::create(['email' => 'user1@test.com']); + $user4 = Admin::create(['email' => 'user1@test.com']); + $user5 = Admin::create(['email' => 'user2@test.com']); + $testAdminRole2 = app(Role::class)->create(['name' => 'testAdminRole2', 'guard_name' => 'admin']); + $user3->assignRole($this->testAdminRole); + $user4->assignRole($this->testAdminRole); + $user5->assignRole($testAdminRole2); + $scopedUsers2 = Admin::role('testAdminRole', 'admin')->get(); + $scopedUsers3 = Admin::role('testAdminRole2', 'admin')->get(); + + $this->assertEquals($scopedUsers2->count(), 2); + $this->assertEquals($scopedUsers3->count(), 1); + } + /** @test */ public function it_throws_an_exception_when_trying_to_scope_a_role_from_another_guard() { From 5f9960854295c6aec1c706aaa3b146962d6ee905 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 3 Feb 2019 22:56:54 -0500 Subject: [PATCH 0010/1013] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c925bf4be..28ef3e07a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to `laravel-permission` will be documented in this file +## 2.31.0 - 2019-02-03 +- Add custom guard query to role scope +- Remove use of array_wrap helper function due to future deprecation + ## 2.30.0 - 2019-01-28 - Change cache config time to DateInterval instead of integer From 3b48ba09d5709847f7d8ce6702b444f8547373e7 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 13 Feb 2019 11:03:33 -0500 Subject: [PATCH 0011/1013] Fix duplicate permissions being created through artisan command - Fixes #1021 --- src/Commands/CreatePermission.php | 5 +---- tests/CommandTest.php | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/Commands/CreatePermission.php b/src/Commands/CreatePermission.php index c93546485..f71a63240 100644 --- a/src/Commands/CreatePermission.php +++ b/src/Commands/CreatePermission.php @@ -17,10 +17,7 @@ public function handle() { $permissionClass = app(PermissionContract::class); - $permission = $permissionClass::create([ - 'name' => $this->argument('name'), - 'guard_name' => $this->argument('guard'), - ]); + $permission = $permissionClass::findOrCreate($this->argument('name'), $this->argument('guard')); $this->info("Permission `{$permission->name}` created"); } diff --git a/tests/CommandTest.php b/tests/CommandTest.php index 703de0760..af59b2870 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -64,4 +64,26 @@ public function it_can_create_a_role_and_permissions_at_same_time() $this->assertTrue($role->hasPermissionTo('first permission')); $this->assertTrue($role->hasPermissionTo('second permission')); } + + + /** @test */ + public function it_can_create_a_role_without_duplication() + { + Artisan::call('permission:create-role', ['name' => 'new-role']); + Artisan::call('permission:create-role', ['name' => 'new-role']); + + $this->assertCount(1, Role::where('name', 'new-role')->get()); + $this->assertCount(0, Role::where('name', 'new-role')->first()->permissions); + } + + /** @test */ + public function it_can_create_a_permission_without_duplication() + { + Artisan::call('permission:create-permission', ['name' => 'new-permission']); + Artisan::call('permission:create-permission', ['name' => 'new-permission']); + + $this->assertCount(1, Permission::where('name', 'new-permission')->get()); + } + + } From a6ddad06f2d8899178a909f72cddfea497e1e9cf Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 13 Feb 2019 11:05:27 -0500 Subject: [PATCH 0012/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28ef3e07a..38d8edc21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 2.32.0 - 2019-02-13 +- Fix duplicate permissions being created through artisan command + ## 2.31.0 - 2019-02-03 - Add custom guard query to role scope - Remove use of array_wrap helper function due to future deprecation From 2f280d7e6652e1ac9f21425e7ccfe52b89ab937a Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Wed, 13 Feb 2019 16:05:33 +0000 Subject: [PATCH 0013/1013] Apply fixes from StyleCI --- tests/CommandTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/CommandTest.php b/tests/CommandTest.php index af59b2870..a418504ed 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -65,7 +65,6 @@ public function it_can_create_a_role_and_permissions_at_same_time() $this->assertTrue($role->hasPermissionTo('second permission')); } - /** @test */ public function it_can_create_a_role_without_duplication() { @@ -84,6 +83,4 @@ public function it_can_create_a_permission_without_duplication() $this->assertCount(1, Permission::where('name', 'new-permission')->get()); } - - } From 279c6f650175af48b6e6c1d9af69ff6ffe6a8c0b Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 19 Feb 2019 11:31:00 -0500 Subject: [PATCH 0014/1013] Prepare for Laravel 5.8 --- composer.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 8c80cc07b..0e340d141 100644 --- a/composer.json +++ b/composer.json @@ -20,13 +20,13 @@ ], "require": { "php" : ">=7.0", - "illuminate/auth": "~5.3.0|~5.4.0|~5.5.0|~5.6.0|~5.7.0", - "illuminate/container": "~5.3.0|~5.4.0|~5.5.0|~5.6.0|~5.7.0", - "illuminate/contracts": "~5.3.0|~5.4.0|~5.5.0|~5.6.0|~5.7.0", - "illuminate/database": "~5.4.0|~5.5.0|~5.6.0|~5.7.0" + "illuminate/auth": "~5.3.0|~5.4.0|~5.5.0|~5.6.0|~5.7.0|~5.8.0", + "illuminate/container": "~5.3.0|~5.4.0|~5.5.0|~5.6.0|~5.7.0|~5.8.0", + "illuminate/contracts": "~5.3.0|~5.4.0|~5.5.0|~5.6.0|~5.7.0|~5.8.0", + "illuminate/database": "~5.4.0|~5.5.0|~5.6.0|~5.7.0|~5.8.0" }, "require-dev": { - "orchestra/testbench": "~3.4.2|~3.5.0|~3.6.0|~3.7.0", + "orchestra/testbench": "~3.4.2|~3.5.0|~3.6.0|~3.7.0|~3.8.0", "phpunit/phpunit": "^5.7|6.2|^7.0", "predis/predis": "^1.1" }, From b268c3f15113ff57e4ec4f563e951629edd6de5f Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 20 Feb 2019 09:57:36 -0500 Subject: [PATCH 0015/1013] Fix docblocks Fixes #1033 --- src/Traits/HasPermissions.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index dd44fb3f2..513c6374c 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -114,7 +114,7 @@ protected function convertToPermissionModels($permissions): array * @param string|null $guardName * * @return bool - * @throws \Exception + * @throws PermissionDoesNotExist */ public function hasPermissionTo($permission, $guardName = null): bool { @@ -145,6 +145,8 @@ function () use ($permission, $guardName) { * @param string|null $guardName * * @return bool + * + * @throws PermissionDoesNotExist */ public function hasUncachedPermissionTo($permission, $guardName = null): bool { @@ -178,8 +180,6 @@ public function hasUncachedPermissionTo($permission, $guardName = null): bool * @param string|null $guardName * * @return bool - * - * @throws \Exception */ public function checkPermissionTo($permission, $guardName = null): bool { From e9383168007a3c26713a3f1cbba93e60eb946b44 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 20 Feb 2019 09:58:00 -0500 Subject: [PATCH 0016/1013] Update phpunit.xml.dist --- phpunit.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index a83848104..ffd168cde 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -10,7 +10,7 @@ processIsolation="false" stopOnFailure="false"> - + tests From 17a98f3ae44060ac51180e007621affa5fa22142 Mon Sep 17 00:00:00 2001 From: reza Date: Wed, 20 Feb 2019 19:01:11 +0330 Subject: [PATCH 0017/1013] add some new tests --- tests/MiddlewareTest.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php index 87f922fd1..696f5597d 100644 --- a/tests/MiddlewareTest.php +++ b/tests/MiddlewareTest.php @@ -28,7 +28,16 @@ public function setUp() } /** @test */ - public function a_guest_cannot_access_a_route_protected_by_the_role_middleware() + public function a_guest_cannot_access_a_route_protected_by_the_role_or_permission_middleware() + { + $this->assertEquals( + $this->runMiddleware( + $this->roleOrPermissionMiddleware, 'testRole' + ), 403); + } + + /** @test */ + public function a_guest_cannot_access_a_route_protected_by_permission_or_role_middleware_middleware() { $this->assertEquals( $this->runMiddleware( @@ -193,6 +202,11 @@ public function a_user_can_access_a_route_protected_by_permission_or_role_middle $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles'), 200 ); + + $this->assertEquals( + $this->runMiddleware($this->roleOrPermissionMiddleware, ['testRole', 'edit-articles']), + 200 + ); } /** @test */ From 188b5d74ca675c2f49d6cc0378133c23aac1f58c Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 20 Feb 2019 10:54:20 -0500 Subject: [PATCH 0018/1013] Tests: clear cache after tests run --- tests/TestCase.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/TestCase.php b/tests/TestCase.php index 8414d8b94..9fe8ddd79 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -46,6 +46,12 @@ public function setUp() $this->testAdminPermission = app(Permission::class)->find(4); } + public function tearDown() + { + $this->reloadPermissions(); + + parent::tearDown(); + } /** * @param \Illuminate\Foundation\Application $app * From 9da0a1618b0c85f71ef7e11b942d5f6f92d2c063 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 20 Feb 2019 10:56:22 -0500 Subject: [PATCH 0019/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38d8edc21..cef53c23c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 2.33.0 - 2019-02-20 +- Laravel 5.8 compatibility + ## 2.32.0 - 2019-02-13 - Fix duplicate permissions being created through artisan command From 7a60eb051f64d9bc8d1c80935252f5dd16b7bc64 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Wed, 20 Feb 2019 15:56:32 +0000 Subject: [PATCH 0020/1013] Apply fixes from StyleCI --- tests/TestCase.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/TestCase.php b/tests/TestCase.php index 9fe8ddd79..5bda595c6 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -52,6 +52,7 @@ public function tearDown() parent::tearDown(); } + /** * @param \Illuminate\Foundation\Application $app * From 73e461f88acc245dbf082e45a54ec9aab49bbd44 Mon Sep 17 00:00:00 2001 From: Matthew Goslett Date: Thu, 21 Feb 2019 10:30:19 +0200 Subject: [PATCH 0021/1013] add in-memory permission caching, switch to single cache key + fix bug where permissions with spaces aren't cached --- src/PermissionRegistrar.php | 34 ++++++++++------------------------ src/Traits/HasPermissions.php | 2 +- tests/CacheTest.php | 10 +++++++--- tests/TestCase.php | 1 + 4 files changed, 19 insertions(+), 28 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 91fb6eeed..8e623ec02 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -27,6 +27,9 @@ class PermissionRegistrar /** @var string */ protected $roleClass; + /** @var \Illuminate\Support\Collection */ + protected $permissions; + /** @var DateInterval|int */ public static $cacheExpirationTime; @@ -118,6 +121,7 @@ public function registerPermissions(): bool */ public function forgetCachedPermissions() { + $this->permissions = null; self::$cacheIsTaggable ? $this->cache->flush() : $this->cache->forget(self::$cacheKey); } @@ -130,39 +134,21 @@ public function forgetCachedPermissions() */ public function getPermissions(array $params = []): Collection { - $permissions = $this->cache->remember($this->getKey($params), self::$cacheExpirationTime, - function () use ($params) { + if ($this->permissions === null) { + $this->permissions = $this->cache->remember(self::$cacheKey, self::$cacheExpirationTime, function () { return $this->getPermissionClass() - ->when($params && self::$cacheIsTaggable, function ($query) use ($params) { - return $query->where($params); - }) ->with('roles') ->get(); }); - - if (! self::$cacheIsTaggable) { - foreach ($params as $attr => $value) { - $permissions = $permissions->where($attr, $value); - } } - return $permissions; - } + $permissions = clone $this->permissions; - /** - * Get the key for caching. - * - * @param $params - * - * @return string - */ - public function getKey(array $params): string - { - if ($params && self::$cacheIsTaggable) { - return self::$cacheKey.'.'.implode('.', array_values($params)); + foreach ($params as $attr => $value) { + $permissions = $permissions->where($attr, $value); } - return self::$cacheKey; + return $permissions; } /** diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 513c6374c..52f2a3cf9 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -205,7 +205,7 @@ protected function getPermissionCacheKey($permission = null) $key .= $this->getPermissionCacheString($permission); } - return $key; + return md5($key); } /** diff --git a/tests/CacheTest.php b/tests/CacheTest.php index 368673d0e..2e350ce7e 100644 --- a/tests/CacheTest.php +++ b/tests/CacheTest.php @@ -150,7 +150,7 @@ public function it_flushes_the_cache_when_giving_a_permission_to_a_role() /** @test */ public function has_permission_to_should_use_the_cache() { - $this->testUserRole->givePermissionTo(['edit-articles', 'edit-news']); + $this->testUserRole->givePermissionTo(['edit-articles', 'edit-news', 'Edit News']); $this->testUser->assignRole('testRole'); $this->resetQueryCount(); @@ -159,11 +159,15 @@ public function has_permission_to_should_use_the_cache() $this->resetQueryCount(); $this->assertTrue($this->testUser->hasPermissionTo('edit-news')); - $this->assertQueryCount($this->cache_run_count + $this->cache_untagged_count); + $this->assertQueryCount(0); $this->resetQueryCount(); $this->assertTrue($this->testUser->hasPermissionTo('edit-articles')); - $this->assertQueryCount($this->cache_init_count); + $this->assertQueryCount(0); + + $this->resetQueryCount(); + $this->assertTrue($this->testUser->hasPermissionTo('Edit News')); + $this->assertQueryCount(0); } /** @test */ diff --git a/tests/TestCase.php b/tests/TestCase.php index 5bda595c6..3df5822ac 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -129,6 +129,7 @@ protected function setUpDatabase($app) $app[Permission::class]->create(['name' => 'edit-news']); $app[Permission::class]->create(['name' => 'edit-blog']); $app[Permission::class]->create(['name' => 'admin-permission', 'guard_name' => 'admin']); + $app[Permission::class]->create(['name' => 'Edit News']); } /** From d22841e98ddf2c2c8a008688f26b69e4f45f2a69 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 26 Feb 2019 16:16:43 -0500 Subject: [PATCH 0022/1013] Add explicit pivotKeys to roles/permissions BelongsToMany relationships Fixes problem identified by @robjbrain in #615 where the Roles model is called UserRole, but the pivot self-detected as user_role_id instead of role_id --- src/Models/Permission.php | 4 +++- src/Models/Role.php | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index a12c2854f..7ef492e2a 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -54,7 +54,9 @@ public function roles(): BelongsToMany { return $this->belongsToMany( config('permission.models.role'), - config('permission.table_names.role_has_permissions') + config('permission.table_names.role_has_permissions'), + 'permission_id', + 'role_id' ); } diff --git a/src/Models/Role.php b/src/Models/Role.php index 893f44223..950a046a9 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -51,7 +51,9 @@ public function permissions(): BelongsToMany { return $this->belongsToMany( config('permission.models.permission'), - config('permission.table_names.role_has_permissions') + config('permission.table_names.role_has_permissions'), + 'role_id', + 'permission_id' ); } From 10f6b2e19cce79936e42b65f08549cbf564c052e Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 26 Feb 2019 16:25:08 -0500 Subject: [PATCH 0023/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cef53c23c..1e8c9a3cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 2.34.0 - 2019-02-26 +- Add explicit pivotKeys to roles/permissions BelongsToMany relationships + ## 2.33.0 - 2019-02-20 - Laravel 5.8 compatibility From 607f750686850bb7d5ee6898cb432d4fed6c8603 Mon Sep 17 00:00:00 2001 From: Nathan Giesbrecht Date: Tue, 26 Feb 2019 16:22:31 -0600 Subject: [PATCH 0024/1013] Fix whitespace --- database/migrations/create_permission_tables.php.stub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/create_permission_tables.php.stub b/database/migrations/create_permission_tables.php.stub index 33b1e6693..a1cafc5f2 100644 --- a/database/migrations/create_permission_tables.php.stub +++ b/database/migrations/create_permission_tables.php.stub @@ -77,7 +77,7 @@ class CreatePermissionTables extends Migration ->onDelete('cascade'); $table->primary(['permission_id', 'role_id']); - + app('cache') ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null) ->forget(config('permission.cache.key')); From d65c3ca3c0297f73cc59ea5504004130d843d213 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 26 Feb 2019 17:46:49 -0500 Subject: [PATCH 0025/1013] Update MiddlewareTest.php --- tests/MiddlewareTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php index 696f5597d..d2015522b 100644 --- a/tests/MiddlewareTest.php +++ b/tests/MiddlewareTest.php @@ -37,7 +37,7 @@ public function a_guest_cannot_access_a_route_protected_by_the_role_or_permissio } /** @test */ - public function a_guest_cannot_access_a_route_protected_by_permission_or_role_middleware_middleware() + public function a_guest_cannot_access_a_route_protected_by_rolemiddleware() { $this->assertEquals( $this->runMiddleware( From a41245c835e16896fe8956b555bf5e99b7aa5487 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 27 Feb 2019 10:41:50 -0500 Subject: [PATCH 0026/1013] TravisCI tests --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 0e340d141..918087375 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "illuminate/database": "~5.4.0|~5.5.0|~5.6.0|~5.7.0|~5.8.0" }, "require-dev": { - "orchestra/testbench": "~3.4.2|~3.5.0|~3.6.0|~3.7.0|~3.8.0", + "orchestra/testbench": "~3.4.2|~3.5.0|~3.6.0|~3.7.0", "phpunit/phpunit": "^5.7|6.2|^7.0", "predis/predis": "^1.1" }, From 35a85e4c7b787ebfc1e700640ff8c39c3d6571e7 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 27 Feb 2019 11:27:09 -0500 Subject: [PATCH 0027/1013] Remove duplicate cache flush from tests ... and un-nest it from the table-create call it was in https://github.com/spatie/laravel-permission/commit/188b5d74ca675c2f49d6cc0378133c23aac1f58c#commitcomment-32503362 --- database/migrations/create_permission_tables.php.stub | 8 ++++---- tests/TestCase.php | 8 +------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/database/migrations/create_permission_tables.php.stub b/database/migrations/create_permission_tables.php.stub index a1cafc5f2..0fe3cac63 100644 --- a/database/migrations/create_permission_tables.php.stub +++ b/database/migrations/create_permission_tables.php.stub @@ -77,11 +77,11 @@ class CreatePermissionTables extends Migration ->onDelete('cascade'); $table->primary(['permission_id', 'role_id']); - - app('cache') - ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null) - ->forget(config('permission.cache.key')); }); + + app('cache') + ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null) + ->forget(config('permission.cache.key')); } /** diff --git a/tests/TestCase.php b/tests/TestCase.php index 5bda595c6..66ad83267 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -35,6 +35,7 @@ public function setUp() { parent::setUp(); + // Note: this also flushes the cache from within the migration $this->setUpDatabase($this->app); $this->testUser = User::first(); @@ -46,13 +47,6 @@ public function setUp() $this->testAdminPermission = app(Permission::class)->find(4); } - public function tearDown() - { - $this->reloadPermissions(); - - parent::tearDown(); - } - /** * @param \Illuminate\Foundation\Application $app * From b14f84fb0def7d16fb31a218c199d1ae7ef9f031 Mon Sep 17 00:00:00 2001 From: Matthew Goslett Date: Fri, 1 Mar 2019 13:54:27 +0200 Subject: [PATCH 0028/1013] remove cache tags --- config/permission.php | 2 +- src/PermissionRegistrar.php | 11 ++----- src/Traits/HasPermissions.php | 59 +++-------------------------------- tests/CacheTest.php | 14 --------- 4 files changed, 8 insertions(+), 78 deletions(-) diff --git a/config/permission.php b/config/permission.php index fbf9b898f..1a0b35a2f 100644 --- a/config/permission.php +++ b/config/permission.php @@ -102,7 +102,7 @@ 'expiration_time' => \DateInterval::createFromDateString('24 hours'), /* - * The key to use when tagging and prefixing entries in the cache. + * The cache key used to store all permissions. */ 'key' => 'spatie.permission.cache', diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 8e623ec02..965cbffff 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -39,9 +39,6 @@ class PermissionRegistrar /** @var string */ public static $cacheModelKey; - /** @var bool */ - public static $cacheIsTaggable = false; - /** * PermissionRegistrar constructor. * @@ -72,11 +69,7 @@ protected function initializeCache() self::$cacheKey = config('permission.cache.key'); self::$cacheModelKey = config('permission.cache.model_key'); - $cache = $this->getCacheStoreFromConfig(); - - self::$cacheIsTaggable = ($cache->getStore() instanceof \Illuminate\Cache\TaggableStore); - - $this->cache = self::$cacheIsTaggable ? $cache->tags(self::$cacheKey) : $cache; + $this->cache = $this->getCacheStoreFromConfig(); } protected function getCacheStoreFromConfig(): \Illuminate\Contracts\Cache\Repository @@ -122,7 +115,7 @@ public function registerPermissions(): bool public function forgetCachedPermissions() { $this->permissions = null; - self::$cacheIsTaggable ? $this->cache->flush() : $this->cache->forget(self::$cacheKey); + $this->cache->forget(self::$cacheKey); } /** diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 52f2a3cf9..c5e11e302 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -122,20 +122,7 @@ public function hasPermissionTo($permission, $guardName = null): bool throw new PermissionDoesNotExist; } - $registrar = app(PermissionRegistrar::class); - if (! $registrar::$cacheIsTaggable) { - return $this->hasUncachedPermissionTo($permission, $guardName); - } - - return $registrar->getCacheStore() - ->tags($this->getCacheTags($permission)) - ->remember( - $this->getPermissionCacheKey($permission), - $registrar::$cacheExpirationTime, - function () use ($permission, $guardName) { - return $this->hasUncachedPermissionTo($permission, $guardName); - } - ); + return $this->hasUncachedPermissionTo($permission, $guardName); } /** @@ -208,27 +195,6 @@ protected function getPermissionCacheKey($permission = null) return md5($key); } - /** - * Construct the tags for the cache entry. - * - * @param null|string|int|\Spatie\Permission\Contracts\Permission $permission - * - * @return array - */ - protected function getCacheTags($permission = null) - { - $tags = [ - PermissionRegistrar::$cacheKey, - $this->getClassCacheString(), - ]; - - if ($permission !== null) { - $tags[] = $this->getPermissionCacheString($permission); - } - - return $tags; - } - /** * Get the key to cache the model by. * @@ -363,28 +329,13 @@ public function getPermissionsViaRoles(): Collection */ public function getAllPermissions(): Collection { - $functionGetAllPermissions = function () { - $permissions = $this->permissions; - - if ($this->roles) { - $permissions = $permissions->merge($this->getPermissionsViaRoles()); - } + $permissions = $this->permissions; - return $permissions->sort()->values(); - }; - - $registrar = app(PermissionRegistrar::class); - if ($registrar::$cacheIsTaggable) { - return $registrar->getCacheStore() - ->tags($this->getCacheTags()) - ->remember( - $this->getPermissionCacheKey(), - $registrar::$cacheExpirationTime, - $functionGetAllPermissions - ); + if ($this->roles) { + $permissions = $permissions->merge($this->getPermissionsViaRoles()); } - return $functionGetAllPermissions(); + return $permissions->sort()->values(); } /** diff --git a/tests/CacheTest.php b/tests/CacheTest.php index 2e350ce7e..c1bc4a818 100644 --- a/tests/CacheTest.php +++ b/tests/CacheTest.php @@ -14,7 +14,6 @@ class CacheTest extends TestCase protected $cache_load_count = 0; protected $cache_run_count = 2; protected $cache_reload_count = 0; - protected $cache_untagged_count = 0; protected $cache_relations_count = 1; protected $registrar; @@ -36,20 +35,7 @@ public function setUp() $this->cache_init_count = 1; $this->cache_load_count = 1; $this->cache_reload_count = 1; - $this->cache_untagged_count = -1; break; - case $cacheStore instanceof \Illuminate\Cache\FileStore: - $this->cache_untagged_count = -2; - break; - case $cacheStore instanceof \Illuminate\Cache\RedisStore: - $this->cache_untagged_count = 0; - break; - case $cacheStore instanceof \Illuminate\Cache\MemcachedStore: - $this->cache_untagged_count = 0; - break; - case $cacheStore instanceof \Illuminate\Cache\ArrayStore: - $this->cache_untagged_count = 0; - default: } } From ec0a57c2321b717064738f7911e13ed4ae1d71f1 Mon Sep 17 00:00:00 2001 From: Matthew Goslett Date: Fri, 1 Mar 2019 14:13:45 +0200 Subject: [PATCH 0029/1013] remove redundant code --- src/Traits/HasPermissions.php | 44 ----------------------------------- 1 file changed, 44 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index c5e11e302..5b078c904 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -177,50 +177,6 @@ public function checkPermissionTo($permission, $guardName = null): bool } } - /** - * Construct the key for the cache entry. - * - * @param null|string|int|\Spatie\Permission\Contracts\Permission $permission - * - * @return string - */ - protected function getPermissionCacheKey($permission = null) - { - $key = PermissionRegistrar::$cacheKey.'.'.$this->getClassCacheString(); - - if ($permission !== null) { - $key .= $this->getPermissionCacheString($permission); - } - - return md5($key); - } - - /** - * Get the key to cache the model by. - * - * @return string - */ - private function getClassCacheString() - { - return str_replace('\\', '.', get_class($this)).'.'.$this->getKey(); - } - - /** - * Get the key to cache the permission by. - * - * @param string|int|\Spatie\Permission\Contracts\Permission $permission - * - * @return mixed - */ - protected function getPermissionCacheString($permission) - { - if ($permission instanceof Permission) { - $permission = $permission[PermissionRegistrar::$cacheModelKey]; - } - - return str_replace('\\', '.', Permission::class).'.'.$permission; - } - /** * Determine if the model has any of the given permissions. * From faeb13ca3639f250879409cafdf6cccb3412c220 Mon Sep 17 00:00:00 2001 From: Flavius Date: Fri, 1 Mar 2019 15:40:44 +0200 Subject: [PATCH 0030/1013] fix inconsistency with role --- src/Traits/HasPermissions.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 513c6374c..b039d75f0 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -399,6 +399,10 @@ public function givePermissionTo(...$permissions) $permissions = collect($permissions) ->flatten() ->map(function ($permission) { + if (empty($permission)) { + return false; + } + return $this->getStoredPermission($permission); }) ->filter(function ($permission) { From f92c855d6cfa2b26a132fadd81aa8a9df15f26fb Mon Sep 17 00:00:00 2001 From: Flavius Date: Fri, 1 Mar 2019 18:51:07 +0200 Subject: [PATCH 0031/1013] new getPermissionNames() function (#1048) add `getPermissionNames` function --- src/Traits/HasPermissions.php | 5 +++++ src/Traits/HasRoles.php | 2 +- tests/HasPermissionsTest.php | 10 ++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index b039d75f0..81043b9e5 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -472,6 +472,11 @@ public function revokePermissionTo($permission) return $this; } + public function getPermissionNames(): Collection + { + return $this->permissions->pluck('name'); + } + /** * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions * diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 1aa8d43ee..962cbe818 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -240,7 +240,7 @@ public function hasAllRoles($roles): bool return $role instanceof Role ? $role->name : $role; }); - return $roles->intersect($this->roles->pluck('name')) == $roles; + return $roles->intersect($this->getRoleNames()) == $roles; } /** diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index 47bdf4591..ef5e07758 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -479,4 +479,14 @@ public function calling_syncPermissions_before_saving_object_doesnt_interfere_wi $this->assertTrue($user2->fresh()->hasPermissionTo('edit-articles')); $this->assertFalse($user2->fresh()->hasPermissionTo('edit-news')); } + + /** @test */ + public function it_can_retrieve_permission_names() + { + $this->testUser->givePermissionTo('edit-news', 'edit-articles'); + $this->assertEquals( + collect(['edit-news', 'edit-articles']), + $this->testUser->getPermissionNames() + ); + } } From 238f2cd5a57ef7abda9dd35644be9e74e7f6d8d0 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 1 Mar 2019 14:27:26 -0500 Subject: [PATCH 0032/1013] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a626f5432..ec3e44bd0 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ You can install the package via composer: composer require spatie/laravel-permission ``` -In Laravel 5.5 the service provider will automatically get registered. In older versions of the framework just add the service provider in `config/app.php` file: +The service provider will automatically get registered. Or you may manually add the service provider in your `config/app.php` file: ```php 'providers' => [ From 45f77b3c87ae2678cf19fa5387701494ff8c4a10 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 1 Mar 2019 14:29:33 -0500 Subject: [PATCH 0033/1013] Updates to PR #1039 for db cache driver tests --- README.md | 2 +- tests/CacheTest.php | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ec3e44bd0..44ac75ce5 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ return [ 'expiration_time' => \DateInterval::createFromDateString('24 hours'), /* - * The key to use when tagging and prefixing entries in the cache. + * The cache key used to store all permissions. */ 'key' => 'spatie.permission.cache', diff --git a/tests/CacheTest.php b/tests/CacheTest.php index c1bc4a818..248bedd71 100644 --- a/tests/CacheTest.php +++ b/tests/CacheTest.php @@ -12,8 +12,7 @@ class CacheTest extends TestCase { protected $cache_init_count = 0; protected $cache_load_count = 0; - protected $cache_run_count = 2; - protected $cache_reload_count = 0; + protected $cache_run_count = 2; // roles lookup, permissions lookup protected $cache_relations_count = 1; protected $registrar; @@ -34,8 +33,7 @@ public function setUp() case $cacheStore instanceof \Illuminate\Cache\DatabaseStore: $this->cache_init_count = 1; $this->cache_load_count = 1; - $this->cache_reload_count = 1; - break; + default: } } @@ -47,10 +45,6 @@ public function it_can_cache_the_permissions() $this->registrar->getPermissions(); $this->assertQueryCount($this->cache_init_count + $this->cache_load_count + $this->cache_run_count); - - $this->registrar->getPermissions(); - - $this->assertQueryCount($this->cache_init_count + $this->cache_load_count + $this->cache_run_count + $this->cache_reload_count); } /** @test */ @@ -118,7 +112,8 @@ public function user_creation_should_not_flush_the_cache() $this->registrar->getPermissions(); - $this->assertQueryCount($this->cache_init_count); + // should all be in memory, so no init/load required + $this->assertQueryCount(0); } /** @test */ From a8398ba40bb93e93a1684776df3bbeb9383496c8 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 1 Mar 2019 14:30:07 -0500 Subject: [PATCH 0034/1013] Deprecate hasUncachedPermissionTo() --- src/Traits/HasPermissions.php | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index ae4608b04..2936b5021 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -117,25 +117,6 @@ protected function convertToPermissionModels($permissions): array * @throws PermissionDoesNotExist */ public function hasPermissionTo($permission, $guardName = null): bool - { - if (! is_string($permission) && ! is_int($permission) && ! $permission instanceof Permission) { - throw new PermissionDoesNotExist; - } - - return $this->hasUncachedPermissionTo($permission, $guardName); - } - - /** - * Check the uncached permissions for the model. - * - * @param string|int|Permission $permission - * @param string|null $guardName - * - * @return bool - * - * @throws PermissionDoesNotExist - */ - public function hasUncachedPermissionTo($permission, $guardName = null): bool { $permissionClass = $this->getPermissionClass(); @@ -160,6 +141,15 @@ public function hasUncachedPermissionTo($permission, $guardName = null): bool return $this->hasDirectPermission($permission) || $this->hasPermissionViaRole($permission); } + /** + * @deprecated since 2.35.0 + * @alias of hasPermissionTo() + */ + public function hasUncachedPermissionTo($permission, $guardName = null): bool + { + return $this->hasPermissionTo($permission, $guardName); + } + /** * An alias to hasPermissionTo(), but avoids throwing an exception. * From 19fd471f353a237269fc3e669e3b043286b8c9cc Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 1 Mar 2019 14:47:26 -0500 Subject: [PATCH 0035/1013] Update CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e8c9a3cc..3958bf4e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to `laravel-permission` will be documented in this file +## 2.35.0 - 2019-03-01 +- overhaul internal caching strategy for better performance and fix cache miss when permission names contained spaces +- deprecated hasUncachedPermissionTo() (use hasPermissionTo() instead) +- added getPermissionNames() method + ## 2.34.0 - 2019-02-26 - Add explicit pivotKeys to roles/permissions BelongsToMany relationships From c29eaa782e222299bd669feb37a39285c04f2ee4 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 1 Mar 2019 14:52:03 -0500 Subject: [PATCH 0036/1013] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 44ac75ce5..898621100 100644 --- a/README.md +++ b/README.md @@ -349,7 +349,8 @@ The `HasRoles` trait adds Eloquent relationships to your models, which can be ac ```php // get a list of all permissions directly assigned to the user -$permissions = $user->permissions; +$permissionNames = $user->getPermissionNames(); // collection of name strings +$permissions = $user->permissions; // collection of permission objects // get all permissions for the user, either directly, or from roles, or from both $permissions = $user->getDirectPermissions(); From b09cddc776076f0af72ed2b2313f875590a56760 Mon Sep 17 00:00:00 2001 From: Andrew Savetchuk Date: Sat, 2 Mar 2019 22:05:31 +0200 Subject: [PATCH 0037/1013] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 898621100..b69f4acfa 100644 --- a/README.md +++ b/README.md @@ -673,7 +673,7 @@ $user->hasPermissionTo('publish articles', 'admin'); > **Note**: When using other than the default `web` guard, you will need to declare which `guard_name` you wish each model to use by setting the `$guard_name` property in your model. One per model is simplest. -> **Note**: If your app uses only a single guard, but is not `web` then change the order of your listed guards in your `config/app.php` to list your primary guard as the default and as the first in the list of defined guards. +> **Note**: If your app uses only a single guard, but is not `web` then change the order of your listed guards in your `config/auth.php` to list your primary guard as the default and as the first in the list of defined guards. ### Assigning permissions and roles to guard users From f4d390897155856cde8f80aa58008af255b8b60f Mon Sep 17 00:00:00 2001 From: Vaios Karampinis Date: Sun, 3 Mar 2019 12:50:10 +0200 Subject: [PATCH 0038/1013] avoid to iterate the list twice and returns on first found permission --- src/PermissionRegistrar.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 965cbffff..f4db8ed12 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -137,11 +137,15 @@ public function getPermissions(array $params = []): Collection $permissions = clone $this->permissions; - foreach ($params as $attr => $value) { - $permissions = $permissions->where($attr, $value); - } + return collect([$permissions->first(function ($item) use ($params) { + foreach ($params as $key => $value) { + if ($item[$key] !== $value) { + return false; + } + } + return true; + })]); - return $permissions; } /** From 5a76d55d9cbf4f4e1ee1c04eb839b12ad7eb1c00 Mon Sep 17 00:00:00 2001 From: Vaios Karampinis Date: Sun, 3 Mar 2019 13:05:54 +0200 Subject: [PATCH 0039/1013] style fixes --- src/PermissionRegistrar.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index f4db8ed12..eb976f365 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -143,9 +143,9 @@ public function getPermissions(array $params = []): Collection return false; } } + return true; })]); - } /** From 5bd1e3f212f97d21645ad730c35ec6b81f292d49 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 4 Mar 2019 14:19:31 -0500 Subject: [PATCH 0040/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3958bf4e6..ccc5bca02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 2.36.0 - 2019-03-04 +- improve performance by reducing another iteration in processing query results and returning earlier + ## 2.35.0 - 2019-03-01 - overhaul internal caching strategy for better performance and fix cache miss when permission names contained spaces - deprecated hasUncachedPermissionTo() (use hasPermissionTo() instead) From fbf7458fb164ae11f66218afb1b000f46bc412a0 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 4 Mar 2019 14:33:28 -0500 Subject: [PATCH 0041/1013] Add test to ensure that `hasPermissionTo()` and its cache honours different guards --- tests/CacheTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/CacheTest.php b/tests/CacheTest.php index 248bedd71..177a9afe6 100644 --- a/tests/CacheTest.php +++ b/tests/CacheTest.php @@ -5,6 +5,7 @@ use Illuminate\Support\Facades\DB; use Spatie\Permission\Contracts\Role; use Illuminate\Support\Facades\Artisan; +use Spatie\Permission\Exceptions\PermissionDoesNotExist; use Spatie\Permission\PermissionRegistrar; use Spatie\Permission\Contracts\Permission; @@ -151,6 +152,23 @@ public function has_permission_to_should_use_the_cache() $this->assertQueryCount(0); } + /** @test */ + public function the_cache_should_differentiate_by_guard_name() + { + $this->expectException(PermissionDoesNotExist::class); + + $this->testUserRole->givePermissionTo(['edit-articles', 'web']); + $this->testUser->assignRole('testRole'); + + $this->resetQueryCount(); + $this->assertTrue($this->testUser->hasPermissionTo('edit-articles', 'web')); + $this->assertQueryCount($this->cache_init_count + $this->cache_load_count + $this->cache_run_count + $this->cache_relations_count); + + $this->resetQueryCount(); + $this->assertFalse($this->testUser->hasPermissionTo('edit-articles', 'admin')); + $this->assertQueryCount(1); // 1 for first lookup of this permission with this guard + } + /** @test */ public function get_all_permissions_should_use_the_cache() { From 66a7553f5b93da3f90bb22577691fc9550def4a0 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Mon, 4 Mar 2019 19:33:36 +0000 Subject: [PATCH 0042/1013] Apply fixes from StyleCI --- tests/CacheTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CacheTest.php b/tests/CacheTest.php index 177a9afe6..5159fc7c0 100644 --- a/tests/CacheTest.php +++ b/tests/CacheTest.php @@ -5,9 +5,9 @@ use Illuminate\Support\Facades\DB; use Spatie\Permission\Contracts\Role; use Illuminate\Support\Facades\Artisan; -use Spatie\Permission\Exceptions\PermissionDoesNotExist; use Spatie\Permission\PermissionRegistrar; use Spatie\Permission\Contracts\Permission; +use Spatie\Permission\Exceptions\PermissionDoesNotExist; class CacheTest extends TestCase { From e51759813d2a70f17641517b837c7d9cca594a02 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 5 Mar 2019 09:53:07 -0500 Subject: [PATCH 0043/1013] Revert "[Performance] avoid to iterate the list twice and returns on first found permission" --- src/PermissionRegistrar.php | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index eb976f365..965cbffff 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -137,15 +137,11 @@ public function getPermissions(array $params = []): Collection $permissions = clone $this->permissions; - return collect([$permissions->first(function ($item) use ($params) { - foreach ($params as $key => $value) { - if ($item[$key] !== $value) { - return false; - } - } + foreach ($params as $attr => $value) { + $permissions = $permissions->where($attr, $value); + } - return true; - })]); + return $permissions; } /** From 386b13e3a05aea16e3b45989eb8ae2fbba1ad40f Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 5 Mar 2019 09:57:44 -0500 Subject: [PATCH 0044/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccc5bca02..fb855cbb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 2.36.1 - 2019-03-05 +- reverts the changes made in 2.36.0 due to some reported breaks. + ## 2.36.0 - 2019-03-04 - improve performance by reducing another iteration in processing query results and returning earlier From b6bb1d56599f73b9c7985d37e5522a0926a38af8 Mon Sep 17 00:00:00 2001 From: Tom Lankhorst Date: Tue, 12 Mar 2019 22:04:27 +0100 Subject: [PATCH 0045/1013] Add permission:table --- src/Commands/Table.php | 47 +++++++++++++++++++++++++++++++ src/PermissionServiceProvider.php | 1 + tests/CommandTest.php | 27 ++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 src/Commands/Table.php diff --git a/src/Commands/Table.php b/src/Commands/Table.php new file mode 100644 index 000000000..a89c96a45 --- /dev/null +++ b/src/Commands/Table.php @@ -0,0 +1,47 @@ +argument('guard'); + + if($guard){ + $guards = Collection::make([$guard]); + } else { + $guards = Permission::pluck('guard_name')->merge(Role::pluck('guard_name'))->unique(); + } + + foreach ($guards as $guard) { + $this->info("Guard: $guard"); + + $permissions = Permission::whereGuardName($guard)->orderBy('name')->pluck('name'); + + $roles = Role::whereGuardName($guard)->orderBy('name')->get()->mapWithKeys(function (Role $role) { + return [$role->name => $role->permissions->pluck('name')]; + }); + + $header = $roles->keys()->prepend(''); + + $body = $permissions->map(function ($permission) use ($roles) { + return $roles->map(function (Collection $role_permissions) use ($permission) { + return $role_permissions->contains($permission) ? ' ✔' : ' ·'; + })->prepend($permission); + }); + + $this->table($header->toArray(), $body->toArray()); + } + } +} diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index fe22444a7..0a13f00fa 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -33,6 +33,7 @@ public function boot(PermissionRegistrar $permissionLoader, Filesystem $filesyst Commands\CacheReset::class, Commands\CreateRole::class, Commands\CreatePermission::class, + Commands\Table::class, ]); } diff --git a/tests/CommandTest.php b/tests/CommandTest.php index a418504ed..b028e7765 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -83,4 +83,31 @@ public function it_can_create_a_permission_without_duplication() $this->assertCount(1, Permission::where('name', 'new-permission')->get()); } + + /** @test */ + public function it_can_show_a_permission_table() + { + Artisan::call('permission:table'); + + $output = Artisan::output(); + + $this->assertStringContainsString('Guard: web', $output); + $this->assertStringContainsString('Guard: admin', $output); + + // | | testRole | testRole2 | + $this->assertRegExp('/\|\s+\|\s+testRole\s+\|\s+testRole2\s+\|/', $output); + + // | edit-articles | · | · | + $this->assertRegExp('/\|\s+edit-articles\s+\|\s+·\s+\|\s+·\s+\|/', $output); + + Role::findByName('testRole')->givePermissionTo('edit-articles'); + $this->reloadPermissions(); + + Artisan::call('permission:table'); + + $output = Artisan::output(); + + // | edit-articles | · | · | + $this->assertRegExp('/\|\s+edit-articles\s+\|\s+✔\s+\|\s+·\s+\|/', $output); + } } From ceb91bb23f8d358e95230b565e6ea502d5733ce7 Mon Sep 17 00:00:00 2001 From: Tom Lankhorst Date: Tue, 12 Mar 2019 22:07:36 +0100 Subject: [PATCH 0046/1013] Test single guard --- tests/CommandTest.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/CommandTest.php b/tests/CommandTest.php index b028e7765..ce830f25a 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -110,4 +110,15 @@ public function it_can_show_a_permission_table() // | edit-articles | · | · | $this->assertRegExp('/\|\s+edit-articles\s+\|\s+✔\s+\|\s+·\s+\|/', $output); } + + /** @test */ + public function it_shows_one_table() + { + Artisan::call('permission:table', ['guard' => 'web']); + + $output = Artisan::output(); + + $this->assertStringContainsString('Guard: web', $output); + $this->assertStringNotContainsString('Guard: admin', $output); + } } From d73359b8eb101f5bbfc1d14024f99785bdf906d3 Mon Sep 17 00:00:00 2001 From: Tom Lankhorst Date: Tue, 12 Mar 2019 22:09:59 +0100 Subject: [PATCH 0047/1013] CS --- src/Commands/Table.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Commands/Table.php b/src/Commands/Table.php index a89c96a45..638aa29e8 100644 --- a/src/Commands/Table.php +++ b/src/Commands/Table.php @@ -4,8 +4,8 @@ use Illuminate\Console\Command; use Illuminate\Support\Collection; -use Spatie\Permission\Models\Permission; use Spatie\Permission\Models\Role; +use Spatie\Permission\Models\Permission; class Table extends Command { @@ -18,7 +18,7 @@ public function handle() { $guard = $this->argument('guard'); - if($guard){ + if ($guard) { $guards = Collection::make([$guard]); } else { $guards = Permission::pluck('guard_name')->merge(Role::pluck('guard_name'))->unique(); From c76ae6f6ec468b88258c982aaf20cb0c96c2e28a Mon Sep 17 00:00:00 2001 From: Tom Lankhorst Date: Tue, 12 Mar 2019 22:13:15 +0100 Subject: [PATCH 0048/1013] Reorder for better readability --- src/Commands/Table.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Commands/Table.php b/src/Commands/Table.php index 638aa29e8..2f550df55 100644 --- a/src/Commands/Table.php +++ b/src/Commands/Table.php @@ -27,13 +27,11 @@ public function handle() foreach ($guards as $guard) { $this->info("Guard: $guard"); - $permissions = Permission::whereGuardName($guard)->orderBy('name')->pluck('name'); - $roles = Role::whereGuardName($guard)->orderBy('name')->get()->mapWithKeys(function (Role $role) { return [$role->name => $role->permissions->pluck('name')]; }); - $header = $roles->keys()->prepend(''); + $permissions = Permission::whereGuardName($guard)->orderBy('name')->pluck('name'); $body = $permissions->map(function ($permission) use ($roles) { return $roles->map(function (Collection $role_permissions) use ($permission) { @@ -41,7 +39,10 @@ public function handle() })->prepend($permission); }); - $this->table($header->toArray(), $body->toArray()); + $this->table( + $roles->keys()->prepend('')->toArray(), + $body->toArray() + ); } } } From bb640c0f2864c40774ed57b0c07d89894a10dd0d Mon Sep 17 00:00:00 2001 From: Tom Lankhorst Date: Tue, 12 Mar 2019 22:20:57 +0100 Subject: [PATCH 0049/1013] Refactor permission:table to permission:show --- src/Commands/{Table.php => Show.php} | 4 ++-- src/PermissionServiceProvider.php | 2 +- tests/CommandTest.php | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) rename src/Commands/{Table.php => Show.php} (95%) diff --git a/src/Commands/Table.php b/src/Commands/Show.php similarity index 95% rename from src/Commands/Table.php rename to src/Commands/Show.php index 2f550df55..863eb8f44 100644 --- a/src/Commands/Table.php +++ b/src/Commands/Show.php @@ -7,9 +7,9 @@ use Spatie\Permission\Models\Role; use Spatie\Permission\Models\Permission; -class Table extends Command +class Show extends Command { - protected $signature = 'permission:table + protected $signature = 'permission:show {guard? : The name of the guard}'; protected $description = 'Show a table of roles and permissions per guard'; diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 0a13f00fa..5dbf0dec0 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -33,7 +33,7 @@ public function boot(PermissionRegistrar $permissionLoader, Filesystem $filesyst Commands\CacheReset::class, Commands\CreateRole::class, Commands\CreatePermission::class, - Commands\Table::class, + Commands\Show::class, ]); } diff --git a/tests/CommandTest.php b/tests/CommandTest.php index ce830f25a..7fe1feeca 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -85,9 +85,9 @@ public function it_can_create_a_permission_without_duplication() } /** @test */ - public function it_can_show_a_permission_table() + public function it_can_show_permission_tables() { - Artisan::call('permission:table'); + Artisan::call('permission:show'); $output = Artisan::output(); @@ -103,7 +103,7 @@ public function it_can_show_a_permission_table() Role::findByName('testRole')->givePermissionTo('edit-articles'); $this->reloadPermissions(); - Artisan::call('permission:table'); + Artisan::call('permission:show'); $output = Artisan::output(); @@ -112,9 +112,9 @@ public function it_can_show_a_permission_table() } /** @test */ - public function it_shows_one_table() + public function it_can_show_permissions_for_guard() { - Artisan::call('permission:table', ['guard' => 'web']); + Artisan::call('permission:show', ['guard' => 'web']); $output = Artisan::output(); From f64b96b29cc6b3870a0f1759827f85ed3d47a5f8 Mon Sep 17 00:00:00 2001 From: Tom Lankhorst Date: Wed, 13 Mar 2019 09:57:27 +0100 Subject: [PATCH 0050/1013] Use strpos instead of assertStringContainsString --- tests/CommandTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/CommandTest.php b/tests/CommandTest.php index 7fe1feeca..bd5e911f6 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -91,8 +91,8 @@ public function it_can_show_permission_tables() $output = Artisan::output(); - $this->assertStringContainsString('Guard: web', $output); - $this->assertStringContainsString('Guard: admin', $output); + $this->assertTrue(strpos($output, 'Guard: web') !== false); + $this->assertTrue(strpos($output, 'Guard: admin') !== false); // | | testRole | testRole2 | $this->assertRegExp('/\|\s+\|\s+testRole\s+\|\s+testRole2\s+\|/', $output); @@ -118,7 +118,7 @@ public function it_can_show_permissions_for_guard() $output = Artisan::output(); - $this->assertStringContainsString('Guard: web', $output); - $this->assertStringNotContainsString('Guard: admin', $output); + $this->assertTrue(strpos($output, 'Guard: web') !== false); + $this->assertTrue(strpos($output, 'Guard: admin') === false); } } From dcd6238d0b3d02262acc883a5206ec14861a23e0 Mon Sep 17 00:00:00 2001 From: nachmanrosen Date: Tue, 19 Mar 2019 10:54:57 -0400 Subject: [PATCH 0051/1013] return roles after removing role --- src/Traits/HasRoles.php | 2 ++ tests/HasRolesTest.php | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 962cbe818..4695fcba1 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -149,6 +149,8 @@ public function removeRole($role) $this->roles()->detach($this->getStoredRole($role)); $this->load('roles'); + + return $this; } /** diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 8f8a11ce0..0cda20724 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -28,6 +28,23 @@ public function it_can_assign_and_remove_a_role() $this->assertFalse($this->testUser->hasRole('testRole')); } + /** @test */ + public function it_removes_a_role_and_returns_roles() + { + $this->testUser->assignRole('testRole'); + + $this->testUser->assignRole('testRole2'); + + $this->assertTrue($this->testUser->hasRole(['testRole','testRole2'])); + + $roles = $this->testUser->removeRole('testRole'); + + $this->assertFalse($roles->hasRole('testRole')); + + $this->assertTrue($roles->hasRole('testRole2')); + + } + /** @test */ public function it_can_assign_and_remove_a_role_on_a_permission() { From e166527767efed0dd3e5d913aa2dc29fc2df4ddc Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 19 Mar 2019 16:00:59 -0400 Subject: [PATCH 0052/1013] StyleCI --- tests/HasRolesTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 0cda20724..d105746a4 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -35,7 +35,7 @@ public function it_removes_a_role_and_returns_roles() $this->testUser->assignRole('testRole2'); - $this->assertTrue($this->testUser->hasRole(['testRole','testRole2'])); + $this->assertTrue($this->testUser->hasRole(['testRole', 'testRole2'])); $roles = $this->testUser->removeRole('testRole'); From 51d525552ac3cb3ce8cda105c9908ca257caa94c Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 19 Mar 2019 16:01:51 -0400 Subject: [PATCH 0053/1013] StyleCI --- tests/HasRolesTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index d105746a4..71bd3b6a4 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -42,7 +42,6 @@ public function it_removes_a_role_and_returns_roles() $this->assertFalse($roles->hasRole('testRole')); $this->assertTrue($roles->hasRole('testRole2')); - } /** @test */ From bf26e5af34831cf6c9b5d937eb76c9fca9a3143b Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 3 Apr 2019 09:45:40 -0400 Subject: [PATCH 0054/1013] TravisCI - change nightly(8.0) PHP to 7.4snapshot The former approach of using `nightly` to grab the latest in-dev PHP version isn't working well now that the `nightly` build is an alias for 8.0 ... and 8.0 isn't resolving composer packages since most haven't tagged anything compatible with it yet. So switching to `7.4snapshot` allows testing against the next announced PHP version set to release later this year. --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index b23d91dca..05d4b7ee3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ php: - 7.1 - 7.2 - 7.3 - - nightly + - 7.4snapshot matrix: include: @@ -13,7 +13,7 @@ matrix: - php: 7.1 env: COMPOSER_FLAGS="--prefer-lowest" allow_failures: - - php: nightly + - php: 7.4snapshot before_script: - travis_retry composer self-update From 0cef4e63634f8232f12d401a4b34433f42f9863b Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 7 Apr 2019 13:34:03 -0400 Subject: [PATCH 0055/1013] Add model policy example to README --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index b69f4acfa..9178f260e 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ * [Using Blade directives](#using-blade-directives) * [Defining a Super-Admin](#defining-a-super-admin) * [Best Practices -- roles vs permissions](#best-practices----roles-vs-permissions) + * [Best Practices -- Using Policies](#best-practices----using-policies) * [Using multiple guards](#using-multiple-guards) * [Using a middleware](#using-a-middleware) * [Using artisan commands](#using-artisan-commands) @@ -641,6 +642,10 @@ It is generally best to code your app around `permissions` only. That way you ca Roles can still be used to group permissions for easy assignment, and you can still use the role-based helper methods if truly necessary. But most app-related logic can usually be best controlled using the `can` methods, which allows Laravel's Gate layer to do all the heavy lifting. +## Best Practices -- Using Policies + +The best way to incorporate access control for access to app features is with Model Policies. This way your application logic can be combined with your permission rules, keeping your implementation simpler. You can find an example of implementing a model policy with this Laravel Permissions package in this demo app: https://github.com/drbyte/spatie-permissions-demo/blob/master/app/Policies/PostPolicy.php + ## Using multiple guards From b6583f5bdd14e2908eb6054c9338b18c3bfa279c Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 7 Apr 2019 14:53:28 -0400 Subject: [PATCH 0056/1013] Add optional display-style argument --- src/Commands/Show.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Commands/Show.php b/src/Commands/Show.php index 863eb8f44..ad5023d08 100644 --- a/src/Commands/Show.php +++ b/src/Commands/Show.php @@ -10,12 +10,14 @@ class Show extends Command { protected $signature = 'permission:show - {guard? : The name of the guard}'; + {guard? : The name of the guard} + {style? : The display style (default|borderless|compact|box)}'; protected $description = 'Show a table of roles and permissions per guard'; public function handle() { + $style = $this->argument('style') ?? 'default'; $guard = $this->argument('guard'); if ($guard) { @@ -41,7 +43,8 @@ public function handle() $this->table( $roles->keys()->prepend('')->toArray(), - $body->toArray() + $body->toArray(), + $style ); } } From f54e4136460e6989dc19711620d7b1970c1ef50a Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 9 Apr 2019 08:39:51 -0400 Subject: [PATCH 0057/1013] Set $guarded properties to protected Closes #1080 --- src/Models/Permission.php | 2 +- src/Models/Role.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 7ef492e2a..83521a503 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -19,7 +19,7 @@ class Permission extends Model implements PermissionContract use HasRoles; use RefreshesPermissionCache; - public $guarded = ['id']; + protected $guarded = ['id']; public function __construct(array $attributes = []) { diff --git a/src/Models/Role.php b/src/Models/Role.php index 950a046a9..3a4c5bd98 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -18,7 +18,7 @@ class Role extends Model implements RoleContract use HasPermissions; use RefreshesPermissionCache; - public $guarded = ['id']; + protected $guarded = ['id']; public function __construct(array $attributes = []) { From 81dbe9d372d70c255b66a2727a235076509f8d45 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 9 Apr 2019 08:45:17 -0400 Subject: [PATCH 0058/1013] Update CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb855cbb5..7c613cfeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to `laravel-permission` will be documented in this file +## 2.37.0 - 2019-04-09 +- Added `permission:show` CLI command to display a table of roles/permissions +- `removeRole` now returns the model, consistent with other methods +- model `$guarded` properties updated to `protected` +- README updates + ## 2.36.1 - 2019-03-05 - reverts the changes made in 2.36.0 due to some reported breaks. From 0a95369ddd2d11a3ba5ab9f52dcf937a69d51754 Mon Sep 17 00:00:00 2001 From: Sergej Date: Tue, 21 May 2019 14:53:52 +0200 Subject: [PATCH 0059/1013] Load roles relationship only when missing --- src/Traits/HasPermissions.php | 15 +++++++++++---- tests/CacheTest.php | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 2936b5021..65add1ee0 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -262,10 +262,17 @@ public function hasDirectPermission($permission): bool */ public function getPermissionsViaRoles(): Collection { - return $this->load('roles', 'roles.permissions') - ->roles->flatMap(function ($role) { - return $role->permissions; - })->sort()->values(); + $relationships = ['roles', 'roles.permissions']; + + if (method_exists($this, 'loadMissing')) { + $this->loadMissing($relationships); + } else { + $this->load($relationships); + } + + return $this->roles->flatMap(function ($role) { + return $role->permissions; + })->sort()->values(); } /** diff --git a/tests/CacheTest.php b/tests/CacheTest.php index 5159fc7c0..c2990713e 100644 --- a/tests/CacheTest.php +++ b/tests/CacheTest.php @@ -183,7 +183,7 @@ public function get_all_permissions_should_use_the_cache() $actual = $this->testUser->getAllPermissions()->pluck('name'); $this->assertEquals($actual, collect($expected)); - $this->assertQueryCount(3); + $this->assertQueryCount(method_exists($this->testUser, 'loadMissing') ? 2 : 3); } /** @test */ From fede577fb71b512019d80a16975971ca98a537ae Mon Sep 17 00:00:00 2001 From: Sergej Date: Thu, 23 May 2019 01:35:29 +0200 Subject: [PATCH 0060/1013] Fix hasDirectPermission DocBlock --- src/Traits/HasPermissions.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 2936b5021..51eb69da9 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -231,6 +231,7 @@ protected function hasPermissionViaRole(Permission $permission): bool * @param string|int|\Spatie\Permission\Contracts\Permission $permission * * @return bool + * @throws PermissionDoesNotExist */ public function hasDirectPermission($permission): bool { From 9b646c4c65f6d67fc84f030dff73b33a9d56580c Mon Sep 17 00:00:00 2001 From: Ciaran Bernard Date: Thu, 6 Jun 2019 12:41:16 +0100 Subject: [PATCH 0061/1013] Explain using package with laravel's authorize method --- README.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/README.md b/README.md index d1d1fb4ee..93a843b03 100644 --- a/README.md +++ b/README.md @@ -506,6 +506,49 @@ All these responses are collections of `Spatie\Permission\Models\Permission` obj If we follow the previous example, the first response will be a collection with the `delete article` permission and the second will be a collection with the `edit article` permission and the third will contain both. +### Using in Controllers with Laravel's authorize method + +Laravel's `Controller` base class has an `authorize` method that will check a user's authorization and return a HTTP 403 if the user is not authorized. This is a convenient way to keep your controller methods short, and it can work nicely with this package. + +However, when used with this package you need to be aware of the order in which the authorizations methods are attempted. + +With the code below, if the user has the permission `show`, then they will be authorized and the `PostPolicy` `show` method will not be executed. + +If the user does **not** have the permission `show`, then the `PostPolicy` `show` method will be executed, and in this example the user will be authorized if they own the post. + +```php +class AuthServiceProvider extends ServiceProvider +{ + protected $policies = [ + \App\Post::class => \App\Policies\Post::class, + ]; +} + +class PostController extends Controller +{ + public function show(Post $post) + { + $this->authorize('show', $post); + + return view('post.show',compact($post)); + } +} + +class PostPolicy +{ + use HandlesAuthorization; + + public function show(User $user, $post) + { + if ($user->id === $post->user_id) { + return true; + } + + return false; + } +} +``` + ### Using Blade directives This package also adds Blade directives to verify whether the currently logged in user has all or any of a given list of roles. From 7abc4a2616b7c12e3914f38a06f407de56e29c62 Mon Sep 17 00:00:00 2001 From: hn_ryanb Date: Thu, 20 Jun 2019 21:01:20 +0100 Subject: [PATCH 0062/1013] Flush permissions cache when removing a role from a model --- src/Traits/HasRoles.php | 2 ++ tests/CacheTest.php | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 4695fcba1..ec78e296c 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -150,6 +150,8 @@ public function removeRole($role) $this->load('roles'); + $this->forgetCachedPermissions(); + return $this; } diff --git a/tests/CacheTest.php b/tests/CacheTest.php index 5159fc7c0..d729c0902 100644 --- a/tests/CacheTest.php +++ b/tests/CacheTest.php @@ -102,6 +102,22 @@ public function it_flushes_the_cache_when_updating_a_role() $this->assertQueryCount($this->cache_init_count + $this->cache_load_count + $this->cache_run_count); } + /** @test */ + public function it_flushes_the_cache_when_removing_a_role_from_a_user() + { + $this->testUser->assignRole('testRole'); + + $this->registrar->getPermissions(); + + $this->testUser->removeRole('testRole'); + + $this->resetQueryCount(); + + $this->registrar->getPermissions(); + + $this->assertQueryCount($this->cache_init_count + $this->cache_load_count + $this->cache_run_count); + } + /** @test */ public function user_creation_should_not_flush_the_cache() { From b5611570869263f670aa36fc2bcd2f3b2ff65b20 Mon Sep 17 00:00:00 2001 From: Ivan Villareal Date: Sat, 29 Jun 2019 21:10:18 -0700 Subject: [PATCH 0063/1013] Show error when permission:cache-reset command fails (#1097) Show error when cache:reset command fails --- src/Commands/CacheReset.php | 8 +++++--- src/PermissionRegistrar.php | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Commands/CacheReset.php b/src/Commands/CacheReset.php index cd5eb41e3..180d16972 100644 --- a/src/Commands/CacheReset.php +++ b/src/Commands/CacheReset.php @@ -13,8 +13,10 @@ class CacheReset extends Command public function handle() { - app(PermissionRegistrar::class)->forgetCachedPermissions(); - - $this->info('Permission cache flushed.'); + if (app(PermissionRegistrar::class)->forgetCachedPermissions()) { + $this->info('Permission cache flushed.'); + } else { + $this->error('Unable to flush cache.'); + } } } diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 965cbffff..f22deba33 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -115,7 +115,8 @@ public function registerPermissions(): bool public function forgetCachedPermissions() { $this->permissions = null; - $this->cache->forget(self::$cacheKey); + + return $this->cache->forget(self::$cacheKey); } /** From 8affad7bab1817403bf6cbbac6f14b76a3d9b040 Mon Sep 17 00:00:00 2001 From: Moeen Date: Sun, 30 Jun 2019 08:16:21 +0400 Subject: [PATCH 0064/1013] Add check on helper functions (#1105) Check if helper methods already exist, to avoid Lumen clash. --- src/helpers.php | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/helpers.php b/src/helpers.php index bb6e8983c..e9f3c80ce 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -1,23 +1,32 @@ map(function ($guard) { - if (! isset($guard['provider'])) { - return; - } +if (! function_exists('getModelForGuard')) { + /** + * @param string $guard + * + * @return string|null + */ + function getModelForGuard(string $guard) + { + return collect(config('auth.guards')) + ->map(function ($guard) { + if (! isset($guard['provider'])) { + return; + } - return config("auth.providers.{$guard['provider']}.model"); - })->get($guard); + return config("auth.providers.{$guard['provider']}.model"); + })->get($guard); + } } -function isNotLumen() : bool -{ - return ! preg_match('/lumen/i', app()->version()); +if (! function_exists('isNotLumen')) { + /** + * check if application is lumen. + * + * @return bool + */ + function isNotLumen(): bool + { + return ! preg_match('/lumen/i', app()->version()); + } } From 2b0dec90416af0d2cdbee03c322cbf4a2302bf03 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 1 Jul 2019 14:05:57 -0400 Subject: [PATCH 0065/1013] Update README.md Simplified the explanation of policy method name clash risks --- README.md | 43 ++----------------------------------------- 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 93a843b03..a8f9503b8 100644 --- a/README.md +++ b/README.md @@ -506,48 +506,9 @@ All these responses are collections of `Spatie\Permission\Models\Permission` obj If we follow the previous example, the first response will be a collection with the `delete article` permission and the second will be a collection with the `edit article` permission and the third will contain both. -### Using in Controllers with Laravel's authorize method +### NOTE about using permission names in policies -Laravel's `Controller` base class has an `authorize` method that will check a user's authorization and return a HTTP 403 if the user is not authorized. This is a convenient way to keep your controller methods short, and it can work nicely with this package. - -However, when used with this package you need to be aware of the order in which the authorizations methods are attempted. - -With the code below, if the user has the permission `show`, then they will be authorized and the `PostPolicy` `show` method will not be executed. - -If the user does **not** have the permission `show`, then the `PostPolicy` `show` method will be executed, and in this example the user will be authorized if they own the post. - -```php -class AuthServiceProvider extends ServiceProvider -{ - protected $policies = [ - \App\Post::class => \App\Policies\Post::class, - ]; -} - -class PostController extends Controller -{ - public function show(Post $post) - { - $this->authorize('show', $post); - - return view('post.show',compact($post)); - } -} - -class PostPolicy -{ - use HandlesAuthorization; - - public function show(User $user, $post) - { - if ($user->id === $post->user_id) { - return true; - } - - return false; - } -} -``` +When calling `authorize()` for a policy method, if you have a permission named the same as one of those policy methods, your permission "name" will take precedence and not fire the policy. For this reason it may be wise to avoid naming your permissions the same as the methods in your policy. While you can define your own method names, you can read more about the defaults Laravel offers in Laravel's documentation at https://laravel.com/docs/5.8/authorization#writing-policies ### Using Blade directives This package also adds Blade directives to verify whether the currently logged in user has all or any of a given list of roles. From 499124d98f319f284fe161147295e886aa36cf6e Mon Sep 17 00:00:00 2001 From: silen Date: Tue, 2 Jul 2019 14:00:47 +0800 Subject: [PATCH 0066/1013] Update Permission.php --- src/Models/Permission.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 83521a503..a4adb4416 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -142,6 +142,8 @@ public static function findOrCreate(string $name, $guardName = null): Permission */ protected static function getPermissions(array $params = []): Collection { - return app(PermissionRegistrar::class)->getPermissions($params); + return app(PermissionRegistrar::class) + ->setPermissionClass(static::class) + ->getPermissions($params); } } From dd6babad9c52f6c93c4ce99f3ec588ed1ee40e6c Mon Sep 17 00:00:00 2001 From: silen Date: Tue, 2 Jul 2019 14:02:43 +0800 Subject: [PATCH 0067/1013] Update PermissionRegistrar.php --- src/PermissionRegistrar.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index f22deba33..85f90f2e2 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -154,6 +154,13 @@ public function getPermissionClass(): Permission { return app($this->permissionClass); } + + public function setPermissionClass($permissionClass) + { + $this->permissionClass = $permissionClass; + + return $this; + } /** * Get an instance of the role class. From 3b389db5d6d4b462545ff3787566b5bcea1dbe1f Mon Sep 17 00:00:00 2001 From: silen Date: Tue, 2 Jul 2019 14:03:52 +0800 Subject: [PATCH 0068/1013] Update PermissionRegistrar.php --- src/PermissionRegistrar.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 85f90f2e2..414f9f262 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -171,6 +171,13 @@ public function getRoleClass(): Role { return app($this->roleClass); } + + public function setRoleClass($roleClass) + { + $this->roleClass = $roleClass; + + return $this; + } /** * Get the instance of the Cache Store. From fdd78668f3ad0df360a8a79b70f5ed56e75dd90b Mon Sep 17 00:00:00 2001 From: silen Date: Tue, 2 Jul 2019 14:59:30 +0800 Subject: [PATCH 0069/1013] Update PermissionRegistrar.php --- src/PermissionRegistrar.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 414f9f262..85f90f2e2 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -171,13 +171,6 @@ public function getRoleClass(): Role { return app($this->roleClass); } - - public function setRoleClass($roleClass) - { - $this->roleClass = $roleClass; - - return $this; - } /** * Get the instance of the Cache Store. From de166252a929238772ec9f8991aae6a917dbc8a7 Mon Sep 17 00:00:00 2001 From: silen Date: Tue, 2 Jul 2019 15:01:11 +0800 Subject: [PATCH 0070/1013] Update PermissionRegistrar.php --- src/PermissionRegistrar.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 85f90f2e2..a956ccd24 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -158,7 +158,7 @@ public function getPermissionClass(): Permission public function setPermissionClass($permissionClass) { $this->permissionClass = $permissionClass; - + return $this; } From 70f27e090b378ced15bb767d2dbd82f7b8c4e962 Mon Sep 17 00:00:00 2001 From: silen Date: Tue, 2 Jul 2019 15:02:38 +0800 Subject: [PATCH 0071/1013] Update PermissionRegistrar.php --- src/PermissionRegistrar.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index a956ccd24..d4bdbd62d 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -154,7 +154,7 @@ public function getPermissionClass(): Permission { return app($this->permissionClass); } - + public function setPermissionClass($permissionClass) { $this->permissionClass = $permissionClass; From c52db4f711a9430d807ce52e9c937506bf59ec5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=80=E9=A5=AD=E4=B8=8D=E5=8A=A0=E7=B3=96?= Date: Sun, 7 Jul 2019 01:30:34 +0800 Subject: [PATCH 0072/1013] define index name --- database/migrations/create_permission_tables.php.stub | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/database/migrations/create_permission_tables.php.stub b/database/migrations/create_permission_tables.php.stub index 0fe3cac63..be416fcba 100644 --- a/database/migrations/create_permission_tables.php.stub +++ b/database/migrations/create_permission_tables.php.stub @@ -35,7 +35,7 @@ class CreatePermissionTables extends Migration $table->string('model_type'); $table->unsignedBigInteger($columnNames['model_morph_key']); - $table->index([$columnNames['model_morph_key'], 'model_type', ]); + $table->index([$columnNames['model_morph_key'], 'model_type', ], 'model_has_permissions_model_id_model_type_index'); $table->foreign('permission_id') ->references('id') @@ -51,7 +51,7 @@ class CreatePermissionTables extends Migration $table->string('model_type'); $table->unsignedBigInteger($columnNames['model_morph_key']); - $table->index([$columnNames['model_morph_key'], 'model_type', ]); + $table->index([$columnNames['model_morph_key'], 'model_type', ], 'model_has_roles_model_id_model_type_index'); $table->foreign('role_id') ->references('id') @@ -76,7 +76,7 @@ class CreatePermissionTables extends Migration ->on($tableNames['roles']) ->onDelete('cascade'); - $table->primary(['permission_id', 'role_id']); + $table->primary(['permission_id', 'role_id'], 'role_has_permissions_permission_id_role_id_primary'); }); app('cache') From 4de2adf0d05b380982b0d62df212953573770184 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Fri, 12 Jul 2019 18:05:23 +0200 Subject: [PATCH 0073/1013] Create FUNDING.yml --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..2bec40976 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +patreon: spatie From 05809d855dbc3982b424fcae0c51e22cd8a7e715 Mon Sep 17 00:00:00 2001 From: Rias Date: Mon, 15 Jul 2019 09:33:43 +0200 Subject: [PATCH 0074/1013] Add docs --- docs/_index.md | 6 + docs/about-us.md | 16 ++ docs/advanced-usage/_index.md | 4 + docs/advanced-usage/cache.md | 44 +++++ docs/advanced-usage/extending.md | 22 +++ docs/advanced-usage/seeding.md | 40 +++++ docs/advanced-usage/unit-testing.md | 18 ++ docs/basic-usage/_index.md | 4 + docs/basic-usage/artisan.md | 30 ++++ docs/basic-usage/basic-usage.md | 101 ++++++++++++ docs/basic-usage/blade-directives.md | 81 +++++++++ docs/basic-usage/direct-permissions.md | 67 ++++++++ docs/basic-usage/middleware.md | 85 ++++++++++ docs/basic-usage/multiple-guards.md | 51 ++++++ docs/basic-usage/role-permissions.md | 113 +++++++++++++ docs/basic-usage/super-admin.md | 10 ++ docs/best-practices/_index.md | 4 + docs/best-practices/roles-vs-permissions.md | 8 + docs/best-practices/using-policies.md | 6 + docs/changelog.md | 6 + docs/installation-laravel.md | 173 ++++++++++++++++++++ docs/installation-lumen.md | 48 ++++++ docs/introduction.md | 26 +++ docs/postcardware.md | 10 ++ docs/questions-issues.md | 8 + docs/upgrading.md | 6 + 26 files changed, 987 insertions(+) create mode 100644 docs/_index.md create mode 100644 docs/about-us.md create mode 100644 docs/advanced-usage/_index.md create mode 100644 docs/advanced-usage/cache.md create mode 100644 docs/advanced-usage/extending.md create mode 100644 docs/advanced-usage/seeding.md create mode 100644 docs/advanced-usage/unit-testing.md create mode 100644 docs/basic-usage/_index.md create mode 100644 docs/basic-usage/artisan.md create mode 100644 docs/basic-usage/basic-usage.md create mode 100644 docs/basic-usage/blade-directives.md create mode 100644 docs/basic-usage/direct-permissions.md create mode 100644 docs/basic-usage/middleware.md create mode 100644 docs/basic-usage/multiple-guards.md create mode 100644 docs/basic-usage/role-permissions.md create mode 100644 docs/basic-usage/super-admin.md create mode 100644 docs/best-practices/_index.md create mode 100644 docs/best-practices/roles-vs-permissions.md create mode 100644 docs/best-practices/using-policies.md create mode 100644 docs/changelog.md create mode 100644 docs/installation-laravel.md create mode 100644 docs/installation-lumen.md create mode 100644 docs/introduction.md create mode 100644 docs/postcardware.md create mode 100644 docs/questions-issues.md create mode 100644 docs/upgrading.md diff --git a/docs/_index.md b/docs/_index.md new file mode 100644 index 000000000..b5f18dacc --- /dev/null +++ b/docs/_index.md @@ -0,0 +1,6 @@ +--- +title: v2 +slogan: Associate users with roles and permissions +githubUrl: https://github.com/spatie/laravel-permission +branch: master +--- diff --git a/docs/about-us.md b/docs/about-us.md new file mode 100644 index 000000000..58a49ee06 --- /dev/null +++ b/docs/about-us.md @@ -0,0 +1,16 @@ +--- +title: About us +--- + +[Spatie](https://spatie.be) is a webdesign agency based in Antwerp, Belgium. + +Open source software is used in all projects we deliver. Laravel, Nginx, Ubuntu are just a few +of the free pieces of software we use every single day. For this, we are very grateful. +When we feel we have solved a problem in a way that can help other developers, +we release our code as open source software [on GitHub](https://spatie.be/opensource). + +This package is heavily based on [Jeffrey Way](https://twitter.com/jeffrey_way)'s awesome [Laracasts](https://laracasts.com) lessons +on [permissions and roles](https://laracasts.com/series/whats-new-in-laravel-5-1/episodes/16). His original code +can be found [in this repo on GitHub](https://github.com/laracasts/laravel-5-roles-and-permissions-demo). + +Special thanks to [Alex Vanderbist](https://github.com/AlexVanderbist) who greatly helped with `v2`, and to [Chris Brown](https://github.com/drbyte) for his longtime support helping us maintain the package. diff --git a/docs/advanced-usage/_index.md b/docs/advanced-usage/_index.md new file mode 100644 index 000000000..31fa54b6a --- /dev/null +++ b/docs/advanced-usage/_index.md @@ -0,0 +1,4 @@ +--- +title: Advanced usage +weight: 3 +--- diff --git a/docs/advanced-usage/cache.md b/docs/advanced-usage/cache.md new file mode 100644 index 000000000..f979e3e08 --- /dev/null +++ b/docs/advanced-usage/cache.md @@ -0,0 +1,44 @@ +--- +title: Cache +weight: 4 +--- + +Role and Permission data are cached to speed up performance. + +While we recommend not changing the cache "key" name, if you wish to alter the expiration time you may do so in the `config/permission.php` file, in the `cache` array. Note that as of v2.26.0 the `cache` entry here is now an array, and `expiration_time` is a sub-array entry. + +When you use the built-in functions for manipulating roles and permissions, the cache is automatically reset for you, and relations are automatically reloaded for the current model record: + +```php +$user->assignRole('writer'); +$user->removeRole('writer'); +$user->syncRoles(params); +$role->givePermissionTo('edit articles'); +$role->revokePermissionTo('edit articles'); +$role->syncPermissions(params); +$permission->assignRole('writer'); +$permission->removeRole('writer'); +$permission->syncRoles(params); +``` + +HOWEVER, if you manipulate permission/role data directly in the database instead of calling the supplied methods, then you will not see the changes reflected in the application unless you manually reset the cache. + +### Manual cache reset +To manually reset the cache for this package, you can run the following in your app code: +```php +app()->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions(); +``` + +Or you can use an Artisan command: +```bash +php artisan permission:cache-reset +``` + + +### Cache Identifier + +TIP: If you are leveraging a caching service such as `redis` or `memcached` and there are other sites +running on your server, you could run into cache clashes between apps. It is prudent to set your own +cache `prefix` in Laravel's `/config/cache.php` to something unique for each application. +This will prevent other applications from accidentally using/changing your cached data. + diff --git a/docs/advanced-usage/extending.md b/docs/advanced-usage/extending.md new file mode 100644 index 000000000..52a2b3ae3 --- /dev/null +++ b/docs/advanced-usage/extending.md @@ -0,0 +1,22 @@ +--- +title: Extending +weight: 3 +--- + +If you need to EXTEND the existing `Role` or `Permission` models note that: + +- Your `Role` model needs to extend the `Spatie\Permission\Models\Role` model +- Your `Permission` model needs to extend the `Spatie\Permission\Models\Permission` model + +If you need to REPLACE the existing `Role` or `Permission` models you need to keep the +following things in mind: + +- Your `Role` model needs to implement the `Spatie\Permission\Contracts\Role` contract +- Your `Permission` model needs to implement the `Spatie\Permission\Contracts\Permission` contract + +In BOTH cases, whether extending or replacing, you will need to specify your new models in the configuration. To do this you must update the `models.role` and `models.permission` values in the configuration file after publishing the configuration with this command: + +```bash +php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" --tag="config" +``` + diff --git a/docs/advanced-usage/seeding.md b/docs/advanced-usage/seeding.md new file mode 100644 index 000000000..2b3b548c7 --- /dev/null +++ b/docs/advanced-usage/seeding.md @@ -0,0 +1,40 @@ +--- +title: Database Seeding +weight: 2 +--- + +You may discover that it is best to flush this package's cache before seeding, to avoid cache conflict errors. This can be done directly in a seeder class. Here is a sample seeder, which first clears the cache, creates permissions and then assigns permissions to roles (the order of these steps is intentional): + +```php +use Illuminate\Database\Seeder; +use Spatie\Permission\Models\Role; +use Spatie\Permission\Models\Permission; + +class RolesAndPermissionsSeeder extends Seeder +{ + public function run() + { + // Reset cached roles and permissions + app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions(); + + // create permissions + Permission::create(['name' => 'edit articles']); + Permission::create(['name' => 'delete articles']); + Permission::create(['name' => 'publish articles']); + Permission::create(['name' => 'unpublish articles']); + + // create roles and assign created permissions + + // this can be done as separate statements + $role = Role::create(['name' => 'writer']); + $role->givePermissionTo('edit articles'); + + // or may be done by chaining + $role = Role::create(['name' => 'moderator']) + ->givePermissionTo(['publish articles', 'unpublish articles']); + + $role = Role::create(['name' => 'super-admin']); + $role->givePermissionTo(Permission::all()); + } +} +``` diff --git a/docs/advanced-usage/unit-testing.md b/docs/advanced-usage/unit-testing.md new file mode 100644 index 000000000..881a927b0 --- /dev/null +++ b/docs/advanced-usage/unit-testing.md @@ -0,0 +1,18 @@ +--- +title: Unit testing +weight: 1 +--- + +In your application's tests, if you are not seeding roles and permissions as part of your test `setUp()` then you may run into a chicken/egg situation where roles and permissions aren't registered with the gate (because your tests create them after that gate registration is done). Working around this is simple: In your tests simply add a `setUp()` instruction to re-register the permissions, like this: + +```php + public function setUp() + { + // first include all the normal setUp operations + parent::setUp(); + + // now re-register all the roles and permissions + $this->app->make(\Spatie\Permission\PermissionRegistrar::class)->registerPermissions(); + } +``` + diff --git a/docs/basic-usage/_index.md b/docs/basic-usage/_index.md new file mode 100644 index 000000000..682ba7ffa --- /dev/null +++ b/docs/basic-usage/_index.md @@ -0,0 +1,4 @@ +--- +title: Basic Usage +weight: 1 +--- diff --git a/docs/basic-usage/artisan.md b/docs/basic-usage/artisan.md new file mode 100644 index 000000000..53dec0814 --- /dev/null +++ b/docs/basic-usage/artisan.md @@ -0,0 +1,30 @@ +--- +title: Using artisan commands +weight: 7 +--- + +You can create a role or permission from a console with artisan commands. + +```bash +php artisan permission:create-role writer +``` + +```bash +php artisan permission:create-permission "edit articles" +``` + +When creating permissions/roles for specific guards you can specify the guard names as a second argument: + +```bash +php artisan permission:create-role writer web +``` + +```bash +php artisan permission:create-permission "edit articles" web +``` + +When creating roles you can also create and link permissions at the same time: + +```bash +php artisan permission:create-role writer web "create articles|edit articles" +``` diff --git a/docs/basic-usage/basic-usage.md b/docs/basic-usage/basic-usage.md new file mode 100644 index 000000000..7fb933f09 --- /dev/null +++ b/docs/basic-usage/basic-usage.md @@ -0,0 +1,101 @@ +--- +title: Basic Usage +weight: 1 +--- + +First, add the `Spatie\Permission\Traits\HasRoles` trait to your `User` model(s): + +```php +use Illuminate\Foundation\Auth\User as Authenticatable; +use Spatie\Permission\Traits\HasRoles; + +class User extends Authenticatable +{ + use HasRoles; + + // ... +} +``` + +> - note that if you need to use `HasRoles` trait with another model ex.`Page` you will also need to add `protected $guard_name = 'web';` as well to that model or you would get an error +> +>```php +>use Illuminate\Database\Eloquent\Model; +>use Spatie\Permission\Traits\HasRoles; +> +>class Page extends Model +>{ +> use HasRoles; +> +> protected $guard_name = 'web'; // or whatever guard you want to use +> +> // ... +>} +>``` + +This package allows for users to be associated with permissions and roles. Every role is associated with multiple permissions. +A `Role` and a `Permission` are regular Eloquent models. They require a `name` and can be created like this: + +```php +use Spatie\Permission\Models\Role; +use Spatie\Permission\Models\Permission; + +$role = Role::create(['name' => 'writer']); +$permission = Permission::create(['name' => 'edit articles']); +``` + + +A permission can be assigned to a role using 1 of these methods: + +```php +$role->givePermissionTo($permission); +$permission->assignRole($role); +``` + +Multiple permissions can be synced to a role using 1 of these methods: + +```php +$role->syncPermissions($permissions); +$permission->syncRoles($roles); +``` + +A permission can be removed from a role using 1 of these methods: + +```php +$role->revokePermissionTo($permission); +$permission->removeRole($role); +``` + +If you're using multiple guards the `guard_name` attribute needs to be set as well. Read about it in the [using multiple guards](#using-multiple-guards) section of the readme. + +The `HasRoles` trait adds Eloquent relationships to your models, which can be accessed directly or used as a base query: + +```php +// get a list of all permissions directly assigned to the user +$permissionNames = $user->getPermissionNames(); // collection of name strings +$permissions = $user->permissions; // collection of permission objects + +// get all permissions for the user, either directly, or from roles, or from both +$permissions = $user->getDirectPermissions(); +$permissions = $user->getPermissionsViaRoles(); +$permissions = $user->getAllPermissions(); + +// get the names of the user's roles +$roles = $user->getRoleNames(); // Returns a collection +``` + +The `HasRoles` trait also adds a `role` scope to your models to scope the query to certain roles or permissions: + +```php +$users = User::role('writer')->get(); // Returns only users with the role 'writer' +``` + +The `role` scope can accept a string, a `\Spatie\Permission\Models\Role` object or an `\Illuminate\Support\Collection` object. + +The same trait also adds a scope to only get users that have a certain permission. + +```php +$users = User::permission('edit articles')->get(); // Returns only users with the permission 'edit articles' (inherited or directly) +``` + +The scope can accept a string, a `\Spatie\Permission\Models\Permission` object or an `\Illuminate\Support\Collection` object. diff --git a/docs/basic-usage/blade-directives.md b/docs/basic-usage/blade-directives.md new file mode 100644 index 000000000..543e249e9 --- /dev/null +++ b/docs/basic-usage/blade-directives.md @@ -0,0 +1,81 @@ +--- +title: Using Blade directives +weight: 4 +--- + +This package also adds Blade directives to verify whether the currently logged in user has all or any of a given list of roles. + +Optionally you can pass in the `guard` that the check will be performed on as a second argument. + +#### Blade and Roles +Check for a specific role: +```php +@role('writer') + I am a writer! +@else + I am not a writer... +@endrole +``` +is the same as +```php +@hasrole('writer') + I am a writer! +@else + I am not a writer... +@endhasrole +``` + +Check for any role in a list: +```php +@hasanyrole($collectionOfRoles) + I have one or more of these roles! +@else + I have none of these roles... +@endhasanyrole +// or +@hasanyrole('writer|admin') + I am either a writer or an admin or both! +@else + I have none of these roles... +@endhasanyrole +``` +Check for all roles: + +```php +@hasallroles($collectionOfRoles) + I have all of these roles! +@else + I do not have all of these roles... +@endhasallroles +// or +@hasallroles('writer|admin') + I am both a writer and an admin! +@else + I do not have all of these roles... +@endhasallroles +``` + +Alternatively, `@unlessrole` gives the reverse for checking a singular role, like this: + +```php +@unlessrole('does not have this role') + I do not have the role +@else + I do have the role +@endunlessrole +``` + +#### Blade and Permissions +This package doesn't add any permission-specific Blade directives. Instead, use Laravel's native `@can` directive to check if a user has a certain permission. + +```php +@can('edit articles') + // +@endcan +``` +or +```php +@if(auth()->user()->can('edit articles') && $some_other_condition) + // +@endif +``` diff --git a/docs/basic-usage/direct-permissions.md b/docs/basic-usage/direct-permissions.md new file mode 100644 index 000000000..cd6a49faa --- /dev/null +++ b/docs/basic-usage/direct-permissions.md @@ -0,0 +1,67 @@ +--- +title: Direct Permissions +weight: 2 +--- + +A permission can be given to any user: + +```php +$user->givePermissionTo('edit articles'); + +// You can also give multiple permission at once +$user->givePermissionTo('edit articles', 'delete articles'); + +// You may also pass an array +$user->givePermissionTo(['edit articles', 'delete articles']); +``` + +A permission can be revoked from a user: + +```php +$user->revokePermissionTo('edit articles'); +``` + +Or revoke & add new permissions in one go: + +```php +$user->syncPermissions(['edit articles', 'delete articles']); +``` + +You can check if a user has a permission: + +```php +$user->hasPermissionTo('edit articles'); +``` + +Or you may pass an integer representing the permission id + +```php +$user->hasPermissionTo('1'); +$user->hasPermissionTo(Permission::find(1)->id); +$user->hasPermissionTo($somePermission->id); +``` + +You can check if a user has Any of an array of permissions: + +```php +$user->hasAnyPermission(['edit articles', 'publish articles', 'unpublish articles']); +``` + +...or if a user has All of an array of permissions: + +```php +$user->hasAllPermissions(['edit articles', 'publish articles', 'unpublish articles']); +``` + +You may also pass integers to lookup by permission id + +```php +$user->hasAnyPermission(['edit articles', 1, 5]); +``` + +Saved permissions will be registered with the `Illuminate\Auth\Access\Gate` class for the default guard. So you can +check if a user has a permission with Laravel's default `can` function: + +```php +$user->can('edit articles'); +``` diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md new file mode 100644 index 000000000..2c777d1f0 --- /dev/null +++ b/docs/basic-usage/middleware.md @@ -0,0 +1,85 @@ +--- +title: Using a middleware +weight: 7 +--- + +This package comes with `RoleMiddleware`, `PermissionMiddleware` and `RoleOrPermissionMiddleware` middleware. You can add them inside your `app/Http/Kernel.php` file. + +```php +protected $routeMiddleware = [ + // ... + 'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class, + 'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class, + 'role_or_permission' => \Spatie\Permission\Middlewares\RoleOrPermissionMiddleware::class, +]; +``` + +Then you can protect your routes using middleware rules: + +```php +Route::group(['middleware' => ['role:super-admin']], function () { + // +}); + +Route::group(['middleware' => ['permission:publish articles']], function () { + // +}); + +Route::group(['middleware' => ['role:super-admin','permission:publish articles']], function () { + // +}); + +Route::group(['middleware' => ['role_or_permission:super-admin']], function () { + // +}); + +Route::group(['middleware' => ['role_or_permission:publish articles']], function () { + // +}); +``` + +Alternatively, you can separate multiple roles or permission with a `|` (pipe) character: + +```php +Route::group(['middleware' => ['role:super-admin|writer']], function () { + // +}); + +Route::group(['middleware' => ['permission:publish articles|edit articles']], function () { + // +}); + +Route::group(['middleware' => ['role_or_permission:super-admin|edit articles']], function () { + // +}); +``` + +You can protect your controllers similarly, by setting desired middleware in the constructor: + +```php +public function __construct() +{ + $this->middleware(['role:super-admin','permission:publish articles|edit articles']); +} +``` + +```php +public function __construct() +{ + $this->middleware(['role_or_permission:super-admin|edit articles']); +} +``` + +### Catching role and permission failures +If you want to override the default `403` response, you can catch the `UnauthorizedException` using your app's exception handler: + +```php +public function render($request, Exception $exception) +{ + if ($exception instanceof \Spatie\Permission\Exceptions\UnauthorizedException) { + // Code here ... + } + + return parent::render($request, $exception); +} +``` diff --git a/docs/basic-usage/multiple-guards.md b/docs/basic-usage/multiple-guards.md new file mode 100644 index 000000000..69db92425 --- /dev/null +++ b/docs/basic-usage/multiple-guards.md @@ -0,0 +1,51 @@ +--- +title: Using multiple guards +weight: 6 +--- + +When using the default Laravel auth configuration all of the above methods will work out of the box, no extra configuration required. + +However, when using multiple guards they will act like namespaces for your permissions and roles. Meaning every guard has its own set of permissions and roles that can be assigned to their user model. + +### Using permissions and roles with multiple guards + +When creating new permissions and roles, if no guard is specified, then the **first** defined guard in `auth.guards` config array will be used. When creating permissions and roles for specific guards you'll have to specify their `guard_name` on the model: + +```php +// Create a superadmin role for the admin users +$role = Role::create(['guard_name' => 'admin', 'name' => 'superadmin']); + +// Define a `publish articles` permission for the admin users belonging to the admin guard +$permission = Permission::create(['guard_name' => 'admin', 'name' => 'publish articles']); + +// Define a *different* `publish articles` permission for the regular users belonging to the web guard +$permission = Permission::create(['guard_name' => 'web', 'name' => 'publish articles']); +``` + +To check if a user has permission for a specific guard: + +```php +$user->hasPermissionTo('publish articles', 'admin'); +``` + +> **Note**: When determining whether a role/permission is valid on a given model, it chooses the guard in this order: first the `$guard_name` property of the model; then the guard in the config (through a provider); then the first-defined guard in the `auth.guards` config array; then the `auth.defaults.guard` config. + +> **Note**: When using other than the default `web` guard, you will need to declare which `guard_name` you wish each model to use by setting the `$guard_name` property in your model. One per model is simplest. + +> **Note**: If your app uses only a single guard, but is not `web` then change the order of your listed guards in your `config/auth.php` to list your primary guard as the default and as the first in the list of defined guards. + +### Assigning permissions and roles to guard users + +You can use the same methods to assign permissions and roles to users as described above in [using permissions via roles](#using-permissions-via-roles). Just make sure the `guard_name` on the permission or role matches the guard of the user, otherwise a `GuardDoesNotMatch` exception will be thrown. + +### Using blade directives with multiple guards + +You can use all of the blade directives listed in [using blade directives](#using-blade-directives) by passing in the guard you wish to use as the second argument to the directive: + +```php +@role('super-admin', 'admin') + I am a super-admin! +@else + I am not a super-admin... +@endrole +``` diff --git a/docs/basic-usage/role-permissions.md b/docs/basic-usage/role-permissions.md new file mode 100644 index 000000000..ee6dfbb75 --- /dev/null +++ b/docs/basic-usage/role-permissions.md @@ -0,0 +1,113 @@ +--- +title: Using permissions via roles +weight: 3 +--- + +A role can be assigned to any user: + +```php +$user->assignRole('writer'); + +// You can also assign multiple roles at once +$user->assignRole('writer', 'admin'); +// or as an array +$user->assignRole(['writer', 'admin']); +``` + +A role can be removed from a user: + +```php +$user->removeRole('writer'); +``` + +Roles can also be synced: + +```php +// All current roles will be removed from the user and replaced by the array given +$user->syncRoles(['writer', 'admin']); +``` + +You can determine if a user has a certain role: + +```php +$user->hasRole('writer'); +``` + +You can also determine if a user has any of a given list of roles: + +```php +$user->hasAnyRole(Role::all()); +``` + +You can also determine if a user has all of a given list of roles: + +```php +$user->hasAllRoles(Role::all()); +``` + +The `assignRole`, `hasRole`, `hasAnyRole`, `hasAllRoles` and `removeRole` functions can accept a + string, a `\Spatie\Permission\Models\Role` object or an `\Illuminate\Support\Collection` object. + +A permission can be given to a role: + +```php +$role->givePermissionTo('edit articles'); +``` + +You can determine if a role has a certain permission: + +```php +$role->hasPermissionTo('edit articles'); +``` + +A permission can be revoked from a role: + +```php +$role->revokePermissionTo('edit articles'); +``` + +The `givePermissionTo` and `revokePermissionTo` functions can accept a +string or a `Spatie\Permission\Models\Permission` object. + + +Permissions are inherited from roles automatically. +Additionally, individual permissions can be assigned to the user too. +For instance: + +```php +$role = Role::findByName('writer'); +$role->givePermissionTo('edit articles'); + +$user->assignRole('writer'); + +$user->givePermissionTo('delete articles'); +``` + +In the above example, a role is given permission to edit articles and this role is assigned to a user. +Now the user can edit articles and additionally delete articles. The permission of 'delete articles' is the user's direct permission because it is assigned directly to them. +When we call `$user->hasDirectPermission('delete articles')` it returns `true`, +but `false` for `$user->hasDirectPermission('edit articles')`. + +This method is useful if one builds a form for setting permissions for roles and users in an application and wants to restrict or change inherited permissions of roles of the user, i.e. allowing to change only direct permissions of the user. + +You can list all of these permissions: + +```php +// Direct permissions +$user->getDirectPermissions() // Or $user->permissions; + +// Permissions inherited from the user's roles +$user->getPermissionsViaRoles(); + +// All permissions which apply on the user (inherited and direct) +$user->getAllPermissions(); +``` + +All these responses are collections of `Spatie\Permission\Models\Permission` objects. + +If we follow the previous example, the first response will be a collection with the `delete article` permission and +the second will be a collection with the `edit article` permission and the third will contain both. + +### NOTE about using permission names in policies + +When calling `authorize()` for a policy method, if you have a permission named the same as one of those policy methods, your permission "name" will take precedence and not fire the policy. For this reason it may be wise to avoid naming your permissions the same as the methods in your policy. While you can define your own method names, you can read more about the defaults Laravel offers in Laravel's documentation at https://laravel.com/docs/5.8/authorization#writing-policies diff --git a/docs/basic-usage/super-admin.md b/docs/basic-usage/super-admin.md new file mode 100644 index 000000000..41fe4e166 --- /dev/null +++ b/docs/basic-usage/super-admin.md @@ -0,0 +1,10 @@ +--- +title: Defining a Super-Admin +weight: 5 +--- + +We strongly recommend that a Super-Admin be handled by setting a global `Gate::before` rule which checks for the desired role. + +Then you can implement the best-practice of primarily using permission-based controls throughout your app, without always having to check for "is this a super-admin" everywhere. + +See this wiki article on [Defining a Super-Admin Gate rule](https://github.com/spatie/laravel-permission/wiki/Global-%22Admin%22-role) in your app. diff --git a/docs/best-practices/_index.md b/docs/best-practices/_index.md new file mode 100644 index 000000000..1e46c30d5 --- /dev/null +++ b/docs/best-practices/_index.md @@ -0,0 +1,4 @@ +--- +title: Best Practices +weight: 2 +--- diff --git a/docs/best-practices/roles-vs-permissions.md b/docs/best-practices/roles-vs-permissions.md new file mode 100644 index 000000000..f94a16c0e --- /dev/null +++ b/docs/best-practices/roles-vs-permissions.md @@ -0,0 +1,8 @@ +--- +title: Roles vs Permissions +weight: 1 +--- + +It is generally best to code your app around `permissions` only. That way you can always use the native Laravel `@can` and `can()` directives everywhere in your app. + +Roles can still be used to group permissions for easy assignment, and you can still use the role-based helper methods if truly necessary. But most app-related logic can usually be best controlled using the `can` methods, which allows Laravel's Gate layer to do all the heavy lifting. diff --git a/docs/best-practices/using-policies.md b/docs/best-practices/using-policies.md new file mode 100644 index 000000000..c8f8f9695 --- /dev/null +++ b/docs/best-practices/using-policies.md @@ -0,0 +1,6 @@ +--- +title: Using policies +weight: 2 +--- + +The best way to incorporate access control for access to app features is with Model Policies. This way your application logic can be combined with your permission rules, keeping your implementation simpler. You can find an example of implementing a model policy with this Laravel Permissions package in this demo app: https://github.com/drbyte/spatie-permissions-demo/blob/master/app/Policies/PostPolicy.php diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 000000000..ab29d7cf2 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,6 @@ +--- +title: Changelog +weight: 6 +--- + +All notable changes to laravel-medialibrary are documented [on GitHub](https://github.com/spatie/laravel-permission/blob/master/CHANGELOG.md) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md new file mode 100644 index 000000000..15edf0346 --- /dev/null +++ b/docs/installation-laravel.md @@ -0,0 +1,173 @@ +--- +title: Installation in Laravel +weight: 4 +--- + +This package can be used in Laravel 5.4 or higher. If you are using an older version of Laravel, take a look at [the v1 branch of this package](https://github.com/spatie/laravel-permission/tree/v1). + +You can install the package via composer: + +``` bash +composer require spatie/laravel-permission +``` + +The service provider will automatically get registered. Or you may manually add the service provider in your `config/app.php` file: + +```php +'providers' => [ + // ... + Spatie\Permission\PermissionServiceProvider::class, +]; +``` + +You can publish [the migration](https://github.com/spatie/laravel-permission/blob/master/database/migrations/create_permission_tables.php.stub) with: + +```bash +php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" --tag="migrations" +``` + +If you're using UUIDs or GUIDs for your `User` models you can update the `create_permission_tables.php` migration and replace `$table->unsignedBigInteger($columnNames['model_morph_key'])` with `$table->uuid($columnNames['model_morph_key'])`. +For consistency, you can also update the package configuration file to use the `model_uuid` column name instead of the default `model_id` column. + +After the migration has been published you can create the role- and permission-tables by running the migrations: + +```bash +php artisan migrate +``` + +You can publish the config file with: + +```bash +php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" --tag="config" +``` + +When published, [the `config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) contains: + +```php +return [ + + 'models' => [ + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * Eloquent model should be used to retrieve your permissions. Of course, it + * is often just the "Permission" model but you may use whatever you like. + * + * The model you want to use as a Permission model needs to implement the + * `Spatie\Permission\Contracts\Permission` contract. + */ + + 'permission' => Spatie\Permission\Models\Permission::class, + + /* + * When using the "HasRoles" trait from this package, we need to know which + * Eloquent model should be used to retrieve your roles. Of course, it + * is often just the "Role" model but you may use whatever you like. + * + * The model you want to use as a Role model needs to implement the + * `Spatie\Permission\Contracts\Role` contract. + */ + + 'role' => Spatie\Permission\Models\Role::class, + + ], + + 'table_names' => [ + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'roles' => 'roles', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your permissions. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'permissions' => 'permissions', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your models permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_permissions' => 'model_has_permissions', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your models roles. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_roles' => 'model_has_roles', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'role_has_permissions' => 'role_has_permissions', + ], + + 'column_names' => [ + + /* + * Change this if you want to name the related model primary key other than + * `model_id`. + * + * For example, this would be nice if your primary keys are all UUIDs. In + * that case, name this `model_uuid`. + */ + 'model_morph_key' => 'model_id', + ], + + /* + * When set to true, the required permission/role names are added to the exception + * message. This could be considered an information leak in some contexts, so + * the default setting is false here for optimum safety. + */ + + 'display_permission_in_exception' => false, + + 'cache' => [ + + /* + * By default all permissions are cached for 24 hours to speed up performance. + * When permissions or roles are updated the cache is flushed automatically. + */ + + 'expiration_time' => \DateInterval::createFromDateString('24 hours'), + + /* + * The cache key used to store all permissions. + */ + + 'key' => 'spatie.permission.cache', + + /* + * When checking for a permission against a model by passing a Permission + * instance to the check, this key determines what attribute on the + * Permissions model is used to cache against. + * + * Ideally, this should match your preferred way of checking permissions, eg: + * `$user->can('view-posts')` would be 'name'. + */ + + 'model_key' => 'name', + + /* + * You may optionally indicate a specific cache driver to use for permission and + * role caching using any of the `store` drivers listed in the cache.php config + * file. Using 'default' here means to use the `default` set in cache.php. + */ + + 'store' => 'default', + ], +]; +``` diff --git a/docs/installation-lumen.md b/docs/installation-lumen.md new file mode 100644 index 000000000..f2d1a4b85 --- /dev/null +++ b/docs/installation-lumen.md @@ -0,0 +1,48 @@ +--- +title: Installation in Lumen +weight: 4 +--- + +You can install the package via Composer: + +``` bash +composer require spatie/laravel-permission +``` + +Copy the required files: + +```bash +mkdir -p config +cp vendor/spatie/laravel-permission/config/permission.php config/permission.php +cp vendor/spatie/laravel-permission/database/migrations/create_permission_tables.php.stub database/migrations/2018_01_01_000000_create_permission_tables.php +``` + +You will also need to create another configuration file at `config/auth.php`. Get it on the Laravel repository or just run the following command: + +```bash +curl -Ls https://raw.githubusercontent.com/laravel/lumen-framework/5.7/config/auth.php -o config/auth.php +``` + +Then, in `bootstrap/app.php`, register the middlewares: + +```php +$app->routeMiddleware([ + 'auth' => App\Http\Middleware\Authenticate::class, + 'permission' => Spatie\Permission\Middlewares\PermissionMiddleware::class, + 'role' => Spatie\Permission\Middlewares\RoleMiddleware::class, +]); +``` + +As well as the config file, service provider, and cache alias: + +```php +$app->configure('permission'); +$app->alias('cache', \Illuminate\Cache\CacheManager::class); // if you don't have this already +$app->register(Spatie\Permission\PermissionServiceProvider::class); +``` + +Now, run your migrations: + +```bash +php artisan migrate +``` diff --git a/docs/introduction.md b/docs/introduction.md new file mode 100644 index 000000000..66868b645 --- /dev/null +++ b/docs/introduction.md @@ -0,0 +1,26 @@ +--- +title: Introduction +weight: 1 +--- + +This package allows you to manage user permissions and roles in a database. + +Once installed you can do stuff like this: + +```php +// Adding permissions to a user +$user->givePermissionTo('edit articles'); + +// Adding permissions via a role +$user->assignRole('writer'); + +$role->givePermissionTo('edit articles'); +``` + +If you're using multiple guards we've got you covered as well. Every guard will have its own set of permissions and roles that can be assigned to the guard's users. Read about it in the [using multiple guards](#using-multiple-guards) section of the readme. + +Because all permissions will be registered on [Laravel's gate](https://laravel.com/docs/5.5/authorization), you can check if a user has a permission with Laravel's default `can` function: + +```php +$user->can('edit articles'); +``` diff --git a/docs/postcardware.md b/docs/postcardware.md new file mode 100644 index 000000000..348a79f0c --- /dev/null +++ b/docs/postcardware.md @@ -0,0 +1,10 @@ +--- +title: Postcardware +weight: 2 +--- + +You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. + +Our address is: Spatie, Samberstraat 69D, 2060 Antwerp, Belgium. + +The best postcards will get published on the [open source section](https://spatie.be/en/opensource/postcards) on our website. diff --git a/docs/questions-issues.md b/docs/questions-issues.md new file mode 100644 index 000000000..3edd96406 --- /dev/null +++ b/docs/questions-issues.md @@ -0,0 +1,8 @@ +--- +title: Questions and issues +weight: 5 +--- + +Find yourself stuck using the package? Found a bug? Do you have general questions or suggestions for improving the media library? Feel free to [create an issue on GitHub](https://github.com/spatie/laravel-permission/issues), we'll try to address it as soon as possible. + +If you've found a bug regarding security please mail [freek@spatie.be](mailto:freek@spatie.be) instead of using the issue tracker. diff --git a/docs/upgrading.md b/docs/upgrading.md new file mode 100644 index 000000000..6165ba562 --- /dev/null +++ b/docs/upgrading.md @@ -0,0 +1,6 @@ +--- +title: Upgrading +weight: 4 +--- + +If you're upgrading from v1 to v2, @fabricecw prepared [a gist which may make your data migration easier](https://gist.github.com/fabricecw/58ee93dd4f99e78724d8acbb851658a4). You will also need to remove your old `laravel-permission.php` config file and publish the new one `permission.php`, and edit accordingly. From 663801f22c8ad860be1796baf837d697fc43dac3 Mon Sep 17 00:00:00 2001 From: Rias Date: Mon, 15 Jul 2019 09:39:30 +0200 Subject: [PATCH 0075/1013] Don't ignore docs in export --- .gitattributes | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 6c5391333..7742c9ae4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,5 +9,4 @@ /.scrutinizer.yml export-ignore /tests export-ignore /.editorconfig export-ignore -/docs export-ignore /.styleci.yml export-ignore From 06966f3e71a0a81247a28ccd93c7b53665bedf30 Mon Sep 17 00:00:00 2001 From: Rias Date: Mon, 15 Jul 2019 09:42:29 +0200 Subject: [PATCH 0076/1013] Add docs to Spatie docs site --- README.md | 893 +----------------------------------------------------- 1 file changed, 2 insertions(+), 891 deletions(-) diff --git a/README.md b/README.md index f3f31a25a..7706362ef 100644 --- a/README.md +++ b/README.md @@ -16,22 +16,6 @@ [![StyleCI](https://styleci.io/repos/42480275/shield)](https://styleci.io/repos/42480275) [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-permission.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-permission) -* [Installation](#installation) -* [Usage](#usage) - * [Using "direct" permissions](#using-direct-permissions-see-below-to-use-both-roles-and-permissions) - * [Using permissions via roles](#using-permissions-via-roles) - * [Using Blade directives](#using-blade-directives) - * [Defining a Super-Admin](#defining-a-super-admin) - * [Best Practices -- roles vs permissions](#best-practices----roles-vs-permissions) - * [Best Practices -- Using Policies](#best-practices----using-policies) - * [Using multiple guards](#using-multiple-guards) - * [Using a middleware](#using-a-middleware) - * [Using artisan commands](#using-artisan-commands) -* [Unit Testing](#unit-testing) -* [Database Seeding](#database-seeding) -* [Extending](#extending) -* [Cache](#cache) - This package allows you to manage user permissions and roles in a database. Once installed you can do stuff like this: @@ -54,878 +38,9 @@ Because all permissions will be registered on [Laravel's gate](https://laravel.c $user->can('edit articles'); ``` -Spatie is a web design agency in Antwerp, Belgium. You'll find an overview of all -our open source projects [on our website](https://spatie.be/opensource). - -## Installation - -- [Laravel](#laravel) -- [Lumen](#lumen) - -### Laravel - -This package can be used in Laravel 5.4 or higher. If you are using an older version of Laravel, take a look at [the v1 branch of this package](https://github.com/spatie/laravel-permission/tree/v1). - -You can install the package via composer: - -``` bash -composer require spatie/laravel-permission -``` - -The service provider will automatically get registered. Or you may manually add the service provider in your `config/app.php` file: - -```php -'providers' => [ - // ... - Spatie\Permission\PermissionServiceProvider::class, -]; -``` - -You can publish [the migration](https://github.com/spatie/laravel-permission/blob/master/database/migrations/create_permission_tables.php.stub) with: - -```bash -php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" --tag="migrations" -``` - -If you're using UUIDs or GUIDs for your `User` models you can update the `create_permission_tables.php` migration and replace `$table->unsignedBigInteger($columnNames['model_morph_key'])` with `$table->uuid($columnNames['model_morph_key'])`. -For consistency, you can also update the package configuration file to use the `model_uuid` column name instead of the default `model_id` column. - -After the migration has been published you can create the role- and permission-tables by running the migrations: - -```bash -php artisan migrate -``` - -You can publish the config file with: - -```bash -php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" --tag="config" -``` - -When published, [the `config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) contains: - -```php -return [ - - 'models' => [ - - /* - * When using the "HasPermissions" trait from this package, we need to know which - * Eloquent model should be used to retrieve your permissions. Of course, it - * is often just the "Permission" model but you may use whatever you like. - * - * The model you want to use as a Permission model needs to implement the - * `Spatie\Permission\Contracts\Permission` contract. - */ - - 'permission' => Spatie\Permission\Models\Permission::class, - - /* - * When using the "HasRoles" trait from this package, we need to know which - * Eloquent model should be used to retrieve your roles. Of course, it - * is often just the "Role" model but you may use whatever you like. - * - * The model you want to use as a Role model needs to implement the - * `Spatie\Permission\Contracts\Role` contract. - */ - - 'role' => Spatie\Permission\Models\Role::class, - - ], - - 'table_names' => [ - - /* - * When using the "HasRoles" trait from this package, we need to know which - * table should be used to retrieve your roles. We have chosen a basic - * default value but you may easily change it to any table you like. - */ - - 'roles' => 'roles', - - /* - * When using the "HasPermissions" trait from this package, we need to know which - * table should be used to retrieve your permissions. We have chosen a basic - * default value but you may easily change it to any table you like. - */ - - 'permissions' => 'permissions', - - /* - * When using the "HasPermissions" trait from this package, we need to know which - * table should be used to retrieve your models permissions. We have chosen a - * basic default value but you may easily change it to any table you like. - */ - - 'model_has_permissions' => 'model_has_permissions', - - /* - * When using the "HasRoles" trait from this package, we need to know which - * table should be used to retrieve your models roles. We have chosen a - * basic default value but you may easily change it to any table you like. - */ - - 'model_has_roles' => 'model_has_roles', - - /* - * When using the "HasRoles" trait from this package, we need to know which - * table should be used to retrieve your roles permissions. We have chosen a - * basic default value but you may easily change it to any table you like. - */ - - 'role_has_permissions' => 'role_has_permissions', - ], - - 'column_names' => [ - - /* - * Change this if you want to name the related model primary key other than - * `model_id`. - * - * For example, this would be nice if your primary keys are all UUIDs. In - * that case, name this `model_uuid`. - */ - 'model_morph_key' => 'model_id', - ], - - /* - * When set to true, the required permission/role names are added to the exception - * message. This could be considered an information leak in some contexts, so - * the default setting is false here for optimum safety. - */ - - 'display_permission_in_exception' => false, - - 'cache' => [ - - /* - * By default all permissions are cached for 24 hours to speed up performance. - * When permissions or roles are updated the cache is flushed automatically. - */ - - 'expiration_time' => \DateInterval::createFromDateString('24 hours'), - - /* - * The cache key used to store all permissions. - */ - - 'key' => 'spatie.permission.cache', - - /* - * When checking for a permission against a model by passing a Permission - * instance to the check, this key determines what attribute on the - * Permissions model is used to cache against. - * - * Ideally, this should match your preferred way of checking permissions, eg: - * `$user->can('view-posts')` would be 'name'. - */ - - 'model_key' => 'name', - - /* - * You may optionally indicate a specific cache driver to use for permission and - * role caching using any of the `store` drivers listed in the cache.php config - * file. Using 'default' here means to use the `default` set in cache.php. - */ - - 'store' => 'default', - ], -]; -``` - -### Lumen - -You can install the package via Composer: - -``` bash -composer require spatie/laravel-permission -``` - -Copy the required files: - -```bash -mkdir -p config -cp vendor/spatie/laravel-permission/config/permission.php config/permission.php -cp vendor/spatie/laravel-permission/database/migrations/create_permission_tables.php.stub database/migrations/2018_01_01_000000_create_permission_tables.php -``` - -You will also need to create another configuration file at `config/auth.php`. Get it on the Laravel repository or just run the following command: - -```bash -curl -Ls https://raw.githubusercontent.com/laravel/lumen-framework/5.7/config/auth.php -o config/auth.php -``` - -Then, in `bootstrap/app.php`, register the middlewares: - -```php -$app->routeMiddleware([ - 'auth' => App\Http\Middleware\Authenticate::class, - 'permission' => Spatie\Permission\Middlewares\PermissionMiddleware::class, - 'role' => Spatie\Permission\Middlewares\RoleMiddleware::class, -]); -``` - -As well as the config file, service provider, and cache alias: - -```php -$app->configure('permission'); -$app->alias('cache', \Illuminate\Cache\CacheManager::class); // if you don't have this already -$app->register(Spatie\Permission\PermissionServiceProvider::class); -``` - -Now, run your migrations: - -```bash -php artisan migrate -``` - -## Usage - -First, add the `Spatie\Permission\Traits\HasRoles` trait to your `User` model(s): - -```php -use Illuminate\Foundation\Auth\User as Authenticatable; -use Spatie\Permission\Traits\HasRoles; - -class User extends Authenticatable -{ - use HasRoles; - - // ... -} -``` - -> - note that if you need to use `HasRoles` trait with another model ex.`Page` you will also need to add `protected $guard_name = 'web';` as well to that model or you would get an error -> ->```php ->use Illuminate\Database\Eloquent\Model; ->use Spatie\Permission\Traits\HasRoles; -> ->class Page extends Model ->{ -> use HasRoles; -> -> protected $guard_name = 'web'; // or whatever guard you want to use -> -> // ... ->} ->``` - -This package allows for users to be associated with permissions and roles. Every role is associated with multiple permissions. -A `Role` and a `Permission` are regular Eloquent models. They require a `name` and can be created like this: - -```php -use Spatie\Permission\Models\Role; -use Spatie\Permission\Models\Permission; - -$role = Role::create(['name' => 'writer']); -$permission = Permission::create(['name' => 'edit articles']); -``` - - -A permission can be assigned to a role using 1 of these methods: - -```php -$role->givePermissionTo($permission); -$permission->assignRole($role); -``` - -Multiple permissions can be synced to a role using 1 of these methods: - -```php -$role->syncPermissions($permissions); -$permission->syncRoles($roles); -``` - -A permission can be removed from a role using 1 of these methods: - -```php -$role->revokePermissionTo($permission); -$permission->removeRole($role); -``` - -If you're using multiple guards the `guard_name` attribute needs to be set as well. Read about it in the [using multiple guards](#using-multiple-guards) section of the readme. - -The `HasRoles` trait adds Eloquent relationships to your models, which can be accessed directly or used as a base query: - -```php -// get a list of all permissions directly assigned to the user -$permissionNames = $user->getPermissionNames(); // collection of name strings -$permissions = $user->permissions; // collection of permission objects - -// get all permissions for the user, either directly, or from roles, or from both -$permissions = $user->getDirectPermissions(); -$permissions = $user->getPermissionsViaRoles(); -$permissions = $user->getAllPermissions(); - -// get the names of the user's roles -$roles = $user->getRoleNames(); // Returns a collection -``` - -The `HasRoles` trait also adds a `role` scope to your models to scope the query to certain roles or permissions: - -```php -$users = User::role('writer')->get(); // Returns only users with the role 'writer' -``` - -The `role` scope can accept a string, a `\Spatie\Permission\Models\Role` object or an `\Illuminate\Support\Collection` object. - -The same trait also adds a scope to only get users that have a certain permission. - -```php -$users = User::permission('edit articles')->get(); // Returns only users with the permission 'edit articles' (inherited or directly) -``` - -The scope can accept a string, a `\Spatie\Permission\Models\Permission` object or an `\Illuminate\Support\Collection` object. - -### Using "direct" permissions (see below to use both roles and permissions) - -A permission can be given to any user: - -```php -$user->givePermissionTo('edit articles'); - -// You can also give multiple permission at once -$user->givePermissionTo('edit articles', 'delete articles'); - -// You may also pass an array -$user->givePermissionTo(['edit articles', 'delete articles']); -``` - -A permission can be revoked from a user: - -```php -$user->revokePermissionTo('edit articles'); -``` - -Or revoke & add new permissions in one go: - -```php -$user->syncPermissions(['edit articles', 'delete articles']); -``` - -You can check if a user has a permission: - -```php -$user->hasPermissionTo('edit articles'); -``` - -Or you may pass an integer representing the permission id - -```php -$user->hasPermissionTo('1'); -$user->hasPermissionTo(Permission::find(1)->id); -$user->hasPermissionTo($somePermission->id); -``` - -You can check if a user has Any of an array of permissions: - -```php -$user->hasAnyPermission(['edit articles', 'publish articles', 'unpublish articles']); -``` - -...or if a user has All of an array of permissions: - -```php -$user->hasAllPermissions(['edit articles', 'publish articles', 'unpublish articles']); -``` - -You may also pass integers to lookup by permission id - -```php -$user->hasAnyPermission(['edit articles', 1, 5]); -``` - -Saved permissions will be registered with the `Illuminate\Auth\Access\Gate` class for the default guard. So you can -check if a user has a permission with Laravel's default `can` function: - -```php -$user->can('edit articles'); -``` - -### Using permissions via roles - -A role can be assigned to any user: - -```php -$user->assignRole('writer'); - -// You can also assign multiple roles at once -$user->assignRole('writer', 'admin'); -// or as an array -$user->assignRole(['writer', 'admin']); -``` - -A role can be removed from a user: - -```php -$user->removeRole('writer'); -``` - -Roles can also be synced: - -```php -// All current roles will be removed from the user and replaced by the array given -$user->syncRoles(['writer', 'admin']); -``` - -You can determine if a user has a certain role: - -```php -$user->hasRole('writer'); -``` - -You can also determine if a user has any of a given list of roles: - -```php -$user->hasAnyRole(Role::all()); -``` - -You can also determine if a user has all of a given list of roles: - -```php -$user->hasAllRoles(Role::all()); -``` - -The `assignRole`, `hasRole`, `hasAnyRole`, `hasAllRoles` and `removeRole` functions can accept a - string, a `\Spatie\Permission\Models\Role` object or an `\Illuminate\Support\Collection` object. - -A permission can be given to a role: - -```php -$role->givePermissionTo('edit articles'); -``` - -You can determine if a role has a certain permission: - -```php -$role->hasPermissionTo('edit articles'); -``` - -A permission can be revoked from a role: - -```php -$role->revokePermissionTo('edit articles'); -``` - -The `givePermissionTo` and `revokePermissionTo` functions can accept a -string or a `Spatie\Permission\Models\Permission` object. - - -Permissions are inherited from roles automatically. -Additionally, individual permissions can be assigned to the user too. -For instance: - -```php -$role = Role::findByName('writer'); -$role->givePermissionTo('edit articles'); - -$user->assignRole('writer'); - -$user->givePermissionTo('delete articles'); -``` - -In the above example, a role is given permission to edit articles and this role is assigned to a user. -Now the user can edit articles and additionally delete articles. The permission of 'delete articles' is the user's direct permission because it is assigned directly to them. -When we call `$user->hasDirectPermission('delete articles')` it returns `true`, -but `false` for `$user->hasDirectPermission('edit articles')`. - -This method is useful if one builds a form for setting permissions for roles and users in an application and wants to restrict or change inherited permissions of roles of the user, i.e. allowing to change only direct permissions of the user. - -You can list all of these permissions: - -```php -// Direct permissions -$user->getDirectPermissions() // Or $user->permissions; - -// Permissions inherited from the user's roles -$user->getPermissionsViaRoles(); - -// All permissions which apply on the user (inherited and direct) -$user->getAllPermissions(); -``` - -All these responses are collections of `Spatie\Permission\Models\Permission` objects. - -If we follow the previous example, the first response will be a collection with the `delete article` permission and -the second will be a collection with the `edit article` permission and the third will contain both. - -### NOTE about using permission names in policies - -When calling `authorize()` for a policy method, if you have a permission named the same as one of those policy methods, your permission "name" will take precedence and not fire the policy. For this reason it may be wise to avoid naming your permissions the same as the methods in your policy. While you can define your own method names, you can read more about the defaults Laravel offers in Laravel's documentation at https://laravel.com/docs/5.8/authorization#writing-policies - -### Using Blade directives -This package also adds Blade directives to verify whether the currently logged in user has all or any of a given list of roles. - -Optionally you can pass in the `guard` that the check will be performed on as a second argument. - -#### Blade and Roles -Check for a specific role: -```php -@role('writer') - I am a writer! -@else - I am not a writer... -@endrole -``` -is the same as -```php -@hasrole('writer') - I am a writer! -@else - I am not a writer... -@endhasrole -``` - -Check for any role in a list: -```php -@hasanyrole($collectionOfRoles) - I have one or more of these roles! -@else - I have none of these roles... -@endhasanyrole -// or -@hasanyrole('writer|admin') - I am either a writer or an admin or both! -@else - I have none of these roles... -@endhasanyrole -``` -Check for all roles: - -```php -@hasallroles($collectionOfRoles) - I have all of these roles! -@else - I do not have all of these roles... -@endhasallroles -// or -@hasallroles('writer|admin') - I am both a writer and an admin! -@else - I do not have all of these roles... -@endhasallroles -``` - -Alternatively, `@unlessrole` gives the reverse for checking a singular role, like this: - -```php -@unlessrole('does not have this role') - I do not have the role -@else - I do have the role -@endunlessrole -``` - -#### Blade and Permissions -This package doesn't add any permission-specific Blade directives. Instead, use Laravel's native `@can` directive to check if a user has a certain permission. - -```php -@can('edit articles') - // -@endcan -``` -or -```php -@if(auth()->user()->can('edit articles') && $some_other_condition) - // -@endif -``` - -## Defining a Super-Admin - -We strongly recommend that a Super-Admin be handled by setting a global `Gate::before` rule which checks for the desired role. - -Then you can implement the best-practice of primarily using permission-based controls throughout your app, without always having to check for "is this a super-admin" everywhere. - -See this wiki article on [Defining a Super-Admin Gate rule](https://github.com/spatie/laravel-permission/wiki/Global-%22Admin%22-role) in your app. - -## Best Practices -- roles vs permissions - -It is generally best to code your app around `permissions` only. That way you can always use the native Laravel `@can` and `can()` directives everywhere in your app. - -Roles can still be used to group permissions for easy assignment, and you can still use the role-based helper methods if truly necessary. But most app-related logic can usually be best controlled using the `can` methods, which allows Laravel's Gate layer to do all the heavy lifting. - -## Best Practices -- Using Policies - -The best way to incorporate access control for access to app features is with Model Policies. This way your application logic can be combined with your permission rules, keeping your implementation simpler. You can find an example of implementing a model policy with this Laravel Permissions package in this demo app: https://github.com/drbyte/spatie-permissions-demo/blob/master/app/Policies/PostPolicy.php - - -## Using multiple guards - -When using the default Laravel auth configuration all of the above methods will work out of the box, no extra configuration required. - -However, when using multiple guards they will act like namespaces for your permissions and roles. Meaning every guard has its own set of permissions and roles that can be assigned to their user model. - -### Using permissions and roles with multiple guards - -When creating new permissions and roles, if no guard is specified, then the **first** defined guard in `auth.guards` config array will be used. When creating permissions and roles for specific guards you'll have to specify their `guard_name` on the model: - -```php -// Create a superadmin role for the admin users -$role = Role::create(['guard_name' => 'admin', 'name' => 'superadmin']); - -// Define a `publish articles` permission for the admin users belonging to the admin guard -$permission = Permission::create(['guard_name' => 'admin', 'name' => 'publish articles']); - -// Define a *different* `publish articles` permission for the regular users belonging to the web guard -$permission = Permission::create(['guard_name' => 'web', 'name' => 'publish articles']); -``` - -To check if a user has permission for a specific guard: - -```php -$user->hasPermissionTo('publish articles', 'admin'); -``` - -> **Note**: When determining whether a role/permission is valid on a given model, it chooses the guard in this order: first the `$guard_name` property of the model; then the guard in the config (through a provider); then the first-defined guard in the `auth.guards` config array; then the `auth.defaults.guard` config. - -> **Note**: When using other than the default `web` guard, you will need to declare which `guard_name` you wish each model to use by setting the `$guard_name` property in your model. One per model is simplest. - -> **Note**: If your app uses only a single guard, but is not `web` then change the order of your listed guards in your `config/auth.php` to list your primary guard as the default and as the first in the list of defined guards. - -### Assigning permissions and roles to guard users - -You can use the same methods to assign permissions and roles to users as described above in [using permissions via roles](#using-permissions-via-roles). Just make sure the `guard_name` on the permission or role matches the guard of the user, otherwise a `GuardDoesNotMatch` exception will be thrown. - -### Using blade directives with multiple guards - -You can use all of the blade directives listed in [using blade directives](#using-blade-directives) by passing in the guard you wish to use as the second argument to the directive: - -```php -@role('super-admin', 'admin') - I am a super-admin! -@else - I am not a super-admin... -@endrole -``` - -## Using a middleware - -This package comes with `RoleMiddleware`, `PermissionMiddleware` and `RoleOrPermissionMiddleware` middleware. You can add them inside your `app/Http/Kernel.php` file. - -```php -protected $routeMiddleware = [ - // ... - 'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class, - 'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class, - 'role_or_permission' => \Spatie\Permission\Middlewares\RoleOrPermissionMiddleware::class, -]; -``` - -Then you can protect your routes using middleware rules: - -```php -Route::group(['middleware' => ['role:super-admin']], function () { - // -}); - -Route::group(['middleware' => ['permission:publish articles']], function () { - // -}); - -Route::group(['middleware' => ['role:super-admin','permission:publish articles']], function () { - // -}); - -Route::group(['middleware' => ['role_or_permission:super-admin']], function () { - // -}); - -Route::group(['middleware' => ['role_or_permission:publish articles']], function () { - // -}); -``` - -Alternatively, you can separate multiple roles or permission with a `|` (pipe) character: - -```php -Route::group(['middleware' => ['role:super-admin|writer']], function () { - // -}); - -Route::group(['middleware' => ['permission:publish articles|edit articles']], function () { - // -}); - -Route::group(['middleware' => ['role_or_permission:super-admin|edit articles']], function () { - // -}); -``` - -You can protect your controllers similarly, by setting desired middleware in the constructor: - -```php -public function __construct() -{ - $this->middleware(['role:super-admin','permission:publish articles|edit articles']); -} -``` - -```php -public function __construct() -{ - $this->middleware(['role_or_permission:super-admin|edit articles']); -} -``` - -### Catching role and permission failures -If you want to override the default `403` response, you can catch the `UnauthorizedException` using your app's exception handler: - -```php -public function render($request, Exception $exception) -{ - if ($exception instanceof \Spatie\Permission\Exceptions\UnauthorizedException) { - // Code here ... - } - - return parent::render($request, $exception); -} -``` - -## Using artisan commands - -You can create a role or permission from a console with artisan commands. - -```bash -php artisan permission:create-role writer -``` - -```bash -php artisan permission:create-permission "edit articles" -``` - -When creating permissions/roles for specific guards you can specify the guard names as a second argument: - -```bash -php artisan permission:create-role writer web -``` - -```bash -php artisan permission:create-permission "edit articles" web -``` - -When creating roles you can also create and link permissions at the same time: - -```bash -php artisan permission:create-role writer web "create articles|edit articles" -``` - - -## Unit Testing - -In your application's tests, if you are not seeding roles and permissions as part of your test `setUp()` then you may run into a chicken/egg situation where roles and permissions aren't registered with the gate (because your tests create them after that gate registration is done). Working around this is simple: In your tests simply add a `setUp()` instruction to re-register the permissions, like this: - -```php - public function setUp() - { - // first include all the normal setUp operations - parent::setUp(); - - // now re-register all the roles and permissions - $this->app->make(\Spatie\Permission\PermissionRegistrar::class)->registerPermissions(); - } -``` - -## Database Seeding - -You may discover that it is best to flush this package's cache before seeding, to avoid cache conflict errors. This can be done directly in a seeder class. Here is a sample seeder, which first clears the cache, creates permissions and then assigns permissions to roles (the order of these steps is intentional): - -```php -use Illuminate\Database\Seeder; -use Spatie\Permission\Models\Role; -use Spatie\Permission\Models\Permission; - -class RolesAndPermissionsSeeder extends Seeder -{ - public function run() - { - // Reset cached roles and permissions - app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions(); - - // create permissions - Permission::create(['name' => 'edit articles']); - Permission::create(['name' => 'delete articles']); - Permission::create(['name' => 'publish articles']); - Permission::create(['name' => 'unpublish articles']); - - // create roles and assign created permissions - - // this can be done as separate statements - $role = Role::create(['name' => 'writer']); - $role->givePermissionTo('edit articles'); - - // or may be done by chaining - $role = Role::create(['name' => 'moderator']) - ->givePermissionTo(['publish articles', 'unpublish articles']); - - $role = Role::create(['name' => 'super-admin']); - $role->givePermissionTo(Permission::all()); - } -} -``` - -## Extending - -If you need to EXTEND the existing `Role` or `Permission` models note that: - -- Your `Role` model needs to extend the `Spatie\Permission\Models\Role` model -- Your `Permission` model needs to extend the `Spatie\Permission\Models\Permission` model - -If you need to REPLACE the existing `Role` or `Permission` models you need to keep the -following things in mind: - -- Your `Role` model needs to implement the `Spatie\Permission\Contracts\Role` contract -- Your `Permission` model needs to implement the `Spatie\Permission\Contracts\Permission` contract - -In BOTH cases, whether extending or replacing, you will need to specify your new models in the configuration. To do this you must update the `models.role` and `models.permission` values in the configuration file after publishing the configuration with this command: - -```bash -php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" --tag="config" -``` - - -## Cache - -Role and Permission data are cached to speed up performance. - -While we recommend not changing the cache "key" name, if you wish to alter the expiration time you may do so in the `config/permission.php` file, in the `cache` array. Note that as of v2.26.0 the `cache` entry here is now an array, and `expiration_time` is a sub-array entry. - -When you use the built-in functions for manipulating roles and permissions, the cache is automatically reset for you, and relations are automatically reloaded for the current model record: - -```php -$user->assignRole('writer'); -$user->removeRole('writer'); -$user->syncRoles(params); -$role->givePermissionTo('edit articles'); -$role->revokePermissionTo('edit articles'); -$role->syncPermissions(params); -$permission->assignRole('writer'); -$permission->removeRole('writer'); -$permission->syncRoles(params); -``` - -HOWEVER, if you manipulate permission/role data directly in the database instead of calling the supplied methods, then you will not see the changes reflected in the application unless you manually reset the cache. - -### Manual cache reset -To manually reset the cache for this package, you can run the following in your app code: -```php -app()->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions(); -``` - -Or you can use an Artisan command: -```bash -php artisan permission:cache-reset -``` - - -### Cache Identifier - -TIP: If you are leveraging a caching service such as `redis` or `memcached` and there are other sites -running on your server, you could run into cache clashes between apps. It is prudent to set your own -cache `prefix` in Laravel's `/config/cache.php` to something unique for each application. -This will prevent other applications from accidentally using/changing your cached data. +## Documentation +You can find installation instructions and detailed instructions on how to use this package at [the dedicated documentation site](https://docs.spatie.be/laravel-permission/v2/introduction/). ## Need a UI? @@ -946,10 +61,6 @@ The package doesn't come with any screens out of the box, you should build that composer test ``` -### Upgrading -If you're upgrading from v1 to v2, @fabricecw prepared [a gist which may make your data migration easier](https://gist.github.com/fabricecw/58ee93dd4f99e78724d8acbb851658a4). -You will also need to remove your old `laravel-permission.php` config file and publish the new one `permission.php`, and edit accordingly. - ### Changelog Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. From f30ab829a0bb819ac094de37c38fd55ffed21ff5 Mon Sep 17 00:00:00 2001 From: Rias Date: Mon, 15 Jul 2019 09:47:42 +0200 Subject: [PATCH 0077/1013] permission* --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index ab29d7cf2..e78bf2db3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -3,4 +3,4 @@ title: Changelog weight: 6 --- -All notable changes to laravel-medialibrary are documented [on GitHub](https://github.com/spatie/laravel-permission/blob/master/CHANGELOG.md) +All notable changes to laravel-permission are documented [on GitHub](https://github.com/spatie/laravel-permission/blob/master/CHANGELOG.md) From 371e6000c439e8f44e3bc89e8e51211374a5bb3d Mon Sep 17 00:00:00 2001 From: Rias Date: Mon, 15 Jul 2019 09:48:03 +0200 Subject: [PATCH 0078/1013] copy pasing is bad --- docs/questions-issues.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/questions-issues.md b/docs/questions-issues.md index 3edd96406..d4efccdc8 100644 --- a/docs/questions-issues.md +++ b/docs/questions-issues.md @@ -3,6 +3,6 @@ title: Questions and issues weight: 5 --- -Find yourself stuck using the package? Found a bug? Do you have general questions or suggestions for improving the media library? Feel free to [create an issue on GitHub](https://github.com/spatie/laravel-permission/issues), we'll try to address it as soon as possible. +Find yourself stuck using the package? Found a bug? Do you have general questions or suggestions for improving the package? Feel free to [create an issue on GitHub](https://github.com/spatie/laravel-permission/issues), we'll try to address it as soon as possible. If you've found a bug regarding security please mail [freek@spatie.be](mailto:freek@spatie.be) instead of using the issue tracker. From 13df3be9688db9214653104148ee2ff1b52f5296 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Mon, 15 Jul 2019 10:57:27 +0200 Subject: [PATCH 0079/1013] Update postcardware.md --- docs/postcardware.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/postcardware.md b/docs/postcardware.md index 348a79f0c..d7d1bb697 100644 --- a/docs/postcardware.md +++ b/docs/postcardware.md @@ -7,4 +7,4 @@ You're free to use this package, but if it makes it to your production environme Our address is: Spatie, Samberstraat 69D, 2060 Antwerp, Belgium. -The best postcards will get published on the [open source section](https://spatie.be/en/opensource/postcards) on our website. +All postcards will get published on the [open source section](https://spatie.be/en/opensource/postcards) on our website. From 119ac8440a643cb3b2e955441bd250f15d4abf51 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Mon, 15 Jul 2019 10:58:54 +0200 Subject: [PATCH 0080/1013] Update basic-usage.md --- docs/basic-usage/basic-usage.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/basic-usage/basic-usage.md b/docs/basic-usage/basic-usage.md index 7fb933f09..18133cf40 100644 --- a/docs/basic-usage/basic-usage.md +++ b/docs/basic-usage/basic-usage.md @@ -19,19 +19,19 @@ class User extends Authenticatable > - note that if you need to use `HasRoles` trait with another model ex.`Page` you will also need to add `protected $guard_name = 'web';` as well to that model or you would get an error > ->```php ->use Illuminate\Database\Eloquent\Model; ->use Spatie\Permission\Traits\HasRoles; -> ->class Page extends Model ->{ -> use HasRoles; -> -> protected $guard_name = 'web'; // or whatever guard you want to use -> -> // ... ->} ->``` +```php +use Illuminate\Database\Eloquent\Model; +use Spatie\Permission\Traits\HasRoles; + +class Page extends Model +{ + use HasRoles; + + protected $guard_name = 'web'; // or whatever guard you want to use + + // ... +} +``` This package allows for users to be associated with permissions and roles. Every role is associated with multiple permissions. A `Role` and a `Permission` are regular Eloquent models. They require a `name` and can be created like this: From c7bb3d1bb6090006ee75bc312ff70e96cf3fb01f Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Mon, 15 Jul 2019 10:59:43 +0200 Subject: [PATCH 0081/1013] Update super-admin.md --- docs/basic-usage/super-admin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/super-admin.md b/docs/basic-usage/super-admin.md index 41fe4e166..f3d00447c 100644 --- a/docs/basic-usage/super-admin.md +++ b/docs/basic-usage/super-admin.md @@ -3,7 +3,7 @@ title: Defining a Super-Admin weight: 5 --- -We strongly recommend that a Super-Admin be handled by setting a global `Gate::before` rule which checks for the desired role. +We strongly recommend that a Super-Admin be handled by setting a global `Gate::before` or `Gate::after` rule which checks for the desired role. Then you can implement the best-practice of primarily using permission-based controls throughout your app, without always having to check for "is this a super-admin" everywhere. From dc83932d2be3d3d306ed9b2e183011d5e2472f54 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Wed, 31 Jul 2019 09:18:54 +0200 Subject: [PATCH 0082/1013] Apply fixes from StyleCI (#1174) --- src/PermissionServiceProvider.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 5dbf0dec0..4659d0f39 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -70,12 +70,12 @@ protected function registerBladeExtensions() { $this->app->afterResolving('blade.compiler', function (BladeCompiler $bladeCompiler) { $bladeCompiler->directive('role', function ($arguments) { - list($role, $guard) = explode(',', $arguments.','); + [$role, $guard] = explode(',', $arguments.','); return "check() && auth({$guard})->user()->hasRole({$role})): ?>"; }); $bladeCompiler->directive('elserole', function ($arguments) { - list($role, $guard) = explode(',', $arguments.','); + [$role, $guard] = explode(',', $arguments.','); return "check() && auth({$guard})->user()->hasRole({$role})): ?>"; }); @@ -84,7 +84,7 @@ protected function registerBladeExtensions() }); $bladeCompiler->directive('hasrole', function ($arguments) { - list($role, $guard) = explode(',', $arguments.','); + [$role, $guard] = explode(',', $arguments.','); return "check() && auth({$guard})->user()->hasRole({$role})): ?>"; }); @@ -93,7 +93,7 @@ protected function registerBladeExtensions() }); $bladeCompiler->directive('hasanyrole', function ($arguments) { - list($roles, $guard) = explode(',', $arguments.','); + [$roles, $guard] = explode(',', $arguments.','); return "check() && auth({$guard})->user()->hasAnyRole({$roles})): ?>"; }); @@ -102,7 +102,7 @@ protected function registerBladeExtensions() }); $bladeCompiler->directive('hasallroles', function ($arguments) { - list($roles, $guard) = explode(',', $arguments.','); + [$roles, $guard] = explode(',', $arguments.','); return "check() && auth({$guard})->user()->hasAllRoles({$roles})): ?>"; }); @@ -111,7 +111,7 @@ protected function registerBladeExtensions() }); $bladeCompiler->directive('unlessrole', function ($arguments) { - list($role, $guard) = explode(',', $arguments.','); + [$role, $guard] = explode(',', $arguments.','); return "check() || ! auth({$guard})->user()->hasRole({$role})): ?>"; }); From 3f23ed7f79bad653f7d124ea07bc51aaaa495bea Mon Sep 17 00:00:00 2001 From: Kyle Date: Wed, 31 Jul 2019 14:56:45 +0200 Subject: [PATCH 0083/1013] Use PHP 7.0 compatible destructuring - Respect the given PHP constraint from composer.json "php" : ">=7.0" - Fix Travis-CI build --- src/PermissionServiceProvider.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 4659d0f39..fc3086b0d 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -70,12 +70,12 @@ protected function registerBladeExtensions() { $this->app->afterResolving('blade.compiler', function (BladeCompiler $bladeCompiler) { $bladeCompiler->directive('role', function ($arguments) { - [$role, $guard] = explode(',', $arguments.','); + list($role, $guard) = explode(',', $arguments.','); return "check() && auth({$guard})->user()->hasRole({$role})): ?>"; }); $bladeCompiler->directive('elserole', function ($arguments) { - [$role, $guard] = explode(',', $arguments.','); + list($role, $guard) = explode(',', $arguments.','); return "check() && auth({$guard})->user()->hasRole({$role})): ?>"; }); @@ -84,7 +84,7 @@ protected function registerBladeExtensions() }); $bladeCompiler->directive('hasrole', function ($arguments) { - [$role, $guard] = explode(',', $arguments.','); + list($role, $guard) = explode(',', $arguments.','); return "check() && auth({$guard})->user()->hasRole({$role})): ?>"; }); @@ -93,7 +93,7 @@ protected function registerBladeExtensions() }); $bladeCompiler->directive('hasanyrole', function ($arguments) { - [$roles, $guard] = explode(',', $arguments.','); + list($role, $guard) = explode(',', $arguments.','); return "check() && auth({$guard})->user()->hasAnyRole({$roles})): ?>"; }); @@ -102,7 +102,7 @@ protected function registerBladeExtensions() }); $bladeCompiler->directive('hasallroles', function ($arguments) { - [$roles, $guard] = explode(',', $arguments.','); + list($role, $guard) = explode(',', $arguments.','); return "check() && auth({$guard})->user()->hasAllRoles({$roles})): ?>"; }); @@ -111,7 +111,7 @@ protected function registerBladeExtensions() }); $bladeCompiler->directive('unlessrole', function ($arguments) { - [$role, $guard] = explode(',', $arguments.','); + list($role, $guard) = explode(',', $arguments.','); return "check() || ! auth({$guard})->user()->hasRole({$role})): ?>"; }); From c3dcc72efa75e6880418d63ab0aac3d02a81bb30 Mon Sep 17 00:00:00 2001 From: Kyle Date: Wed, 31 Jul 2019 15:02:47 +0200 Subject: [PATCH 0084/1013] Align rules to PHP 7.0 level --- .styleci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.styleci.yml b/.styleci.yml index 0285f1790..977d0ec6e 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -1 +1,7 @@ preset: laravel + +enabled: +- long_list_syntax + +disabled: +- short_list_syntax From b37f7b010a05c1c49f387bbfe74be0470e0a0636 Mon Sep 17 00:00:00 2001 From: Kyle Date: Wed, 31 Jul 2019 15:10:15 +0200 Subject: [PATCH 0085/1013] Fix typo --- src/PermissionServiceProvider.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index fc3086b0d..5dbf0dec0 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -93,7 +93,7 @@ protected function registerBladeExtensions() }); $bladeCompiler->directive('hasanyrole', function ($arguments) { - list($role, $guard) = explode(',', $arguments.','); + list($roles, $guard) = explode(',', $arguments.','); return "check() && auth({$guard})->user()->hasAnyRole({$roles})): ?>"; }); @@ -102,7 +102,7 @@ protected function registerBladeExtensions() }); $bladeCompiler->directive('hasallroles', function ($arguments) { - list($role, $guard) = explode(',', $arguments.','); + list($roles, $guard) = explode(',', $arguments.','); return "check() && auth({$guard})->user()->hasAllRoles({$roles})): ?>"; }); From e547fac8014b9bcf18d06d881fd25d3448e10675 Mon Sep 17 00:00:00 2001 From: Muhammad Akbar Date: Thu, 1 Aug 2019 14:34:50 +0500 Subject: [PATCH 0086/1013] The example was confusing so made it more clear There is no point of using "role_or_permission" if we only have to define a role or a permission on it as they both have dedicated middlewares for that. --- docs/basic-usage/middleware.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index 2c777d1f0..54f9cff8f 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -29,7 +29,7 @@ Route::group(['middleware' => ['role:super-admin','permission:publish articles'] // }); -Route::group(['middleware' => ['role_or_permission:super-admin']], function () { +Route::group(['middleware' => ['role_or_permission:super-admin|edit articles']], function () { // }); From f9fd8f6b90d486b53914c684d293be90bdf84d64 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 2 Aug 2019 16:16:17 -0400 Subject: [PATCH 0087/1013] Update upgrading.md --- docs/upgrading.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index 6165ba562..25aa5208a 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -3,4 +3,10 @@ title: Upgrading weight: 4 --- -If you're upgrading from v1 to v2, @fabricecw prepared [a gist which may make your data migration easier](https://gist.github.com/fabricecw/58ee93dd4f99e78724d8acbb851658a4). You will also need to remove your old `laravel-permission.php` config file and publish the new one `permission.php`, and edit accordingly. +### Upgrading from v1 to v2 +If you're upgrading from v1 to v2, there's no built-in automatic migration/conversion of your data. +You will need to carefully adapt your code and your data manually. + +Tip: @fabricecw prepared [a gist which may make your data migration easier](https://gist.github.com/fabricecw/58ee93dd4f99e78724d8acbb851658a4). + +You will also need to remove your old `laravel-permission.php` config file and publish the new one `permission.php`, and edit accordingly (setting up your custom settings again in the new file, where relevant). From 26775cfe262dcb0bb2313d93ac77d8859c457dd4 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 2 Aug 2019 16:23:15 -0400 Subject: [PATCH 0088/1013] Update basic-usage.md --- docs/basic-usage/basic-usage.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/basic-usage/basic-usage.md b/docs/basic-usage/basic-usage.md index 18133cf40..02b5471e0 100644 --- a/docs/basic-usage/basic-usage.md +++ b/docs/basic-usage/basic-usage.md @@ -99,3 +99,14 @@ $users = User::permission('edit articles')->get(); // Returns only users with th ``` The scope can accept a string, a `\Spatie\Permission\Models\Permission` object or an `\Illuminate\Support\Collection` object. + + +### Eloquent +Since Role and Permission models are extended from Eloquent models, basic Eloquent calls can be used as well: + +```php +$all_users_with_all_their_roles = User::with('roles')->get(); +$all_users_with_all_direct_permissions = User::with('permissions')->get(); +$all_roles_in_database = Role::all()->pluck('name'); +``` + From 677d453a75c511f196a45057eebdfbf81acf7894 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 8 Aug 2019 09:30:54 -0400 Subject: [PATCH 0089/1013] Move super-admin info out of the wiki --- docs/basic-usage/super-admin.md | 41 +++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/docs/basic-usage/super-admin.md b/docs/basic-usage/super-admin.md index f3d00447c..beab973c2 100644 --- a/docs/basic-usage/super-admin.md +++ b/docs/basic-usage/super-admin.md @@ -5,6 +5,43 @@ weight: 5 We strongly recommend that a Super-Admin be handled by setting a global `Gate::before` or `Gate::after` rule which checks for the desired role. -Then you can implement the best-practice of primarily using permission-based controls throughout your app, without always having to check for "is this a super-admin" everywhere. +Then you can implement the best-practice of primarily using permission-based controls (@can and $user->can, etc) throughout your app, without always having to check for "is this a super-admin" everywhere. Best not to use role-checking (ie: `hasRole`) when you have Super Admin features like this. -See this wiki article on [Defining a Super-Admin Gate rule](https://github.com/spatie/laravel-permission/wiki/Global-%22Admin%22-role) in your app. + +## `Gate::before` +If you want a "Super Admin" role to respond `true` to all permissions, without needing to assign all those permissions to a role, you can use Laravel's `Gate::before()` method. For example: + +```php +use Illuminate\Support\Facades\Gate; + +class AuthServiceProvider extends ServiceProvider +{ + public function boot() + { + $this->registerPolicies(); + + // Implicitly grant "Super Admin" role all permissions + // This works in the app by using gate-related functions like auth()->user->can() and @can() + Gate::before(function ($user, $ability) { + return $user->hasRole('Super Admin') ? true : null; + }); + } +} +``` + +NOTE: `Gate::before` rules need to return `null` rather than `false`, else it will interfere with normal policy operation. [See more.](https://laracasts.com/discuss/channels/laravel/policy-gets-never-called#reply=492526) + + +## `Gate::after` + +Alternatively you might want to move the Super Admin check to the `Gate::after` phase instead, particularly if your Super Admin shouldn't be allowed to do things your app doesn't want "anyone" to do, such as writing more than 1 review, or bypassing unsubscribe rules, etc. + +The following code snippet is inspired from [Freek's blog article](https://murze.be/when-to-use-gateafter-in-laravel) where this topic is discussed further. + +```php +// somewhere in a service provider + +Gate::after(function ($user, $ability) { + return $user->hasRole('Super Admin'); // note this returns boolean +}); +``` From fb726cf0c4f566061fa5b17f84493a8f4b527a86 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 8 Aug 2019 09:38:37 -0400 Subject: [PATCH 0090/1013] Create phpstorm.md --- docs/advanced-usage/phpstorm.md | 68 +++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 docs/advanced-usage/phpstorm.md diff --git a/docs/advanced-usage/phpstorm.md b/docs/advanced-usage/phpstorm.md new file mode 100644 index 000000000..eed1ddfb9 --- /dev/null +++ b/docs/advanced-usage/phpstorm.md @@ -0,0 +1,68 @@ +--- +title: Extending PhpStorm +weight: 5 +--- + +# Extending PhpStorm to support Blade Directives of this package + +1. In PhpStorm, open Preferences, and navigate to **Languages and Frameworks -> PHP -> Blade** +(File | Settings | Languages & Frameworks | PHP | Blade) +2. Uncheck "Use default settings", then click on the `Directives` tab. +3. Add the following new directives for the laravel-permission package: + + +**role** +- has parameter = YES +- Prefix: `check() && auth()->user()->hasRole(` +- Suffix: `)); ?>` + +-- + +**endrole** +- has parameter = NO +- Prefix: blank +- Suffix: blank + +-- + +**hasrole** +- has parameter = YES +- Prefix: `check() && auth()->user()->hasRole(` +- Suffix: `)); ?>` + +-- + +**endhasrole** +- has parameter = NO +- Prefix: blank +- Suffix: blank + +-- + +**hasanyrole** +- has parameter = YES +- Prefix: `check() && auth()->user()->hasAnyRole(` +- Suffix: `)); ?>` + +-- + +**endhasanyrole** +- has parameter = NO +- Prefix: blank +- Suffix: blank + +-- + +**hasallroles** +- has parameter = YES +- Prefix: `check() && auth()->user()->hasAllRoles(` +- Suffix: `)); ?>` + +-- + +**endhasallroles** +- has parameter = NO +- Prefix: blank +- Suffix: blank + +-- From fc91913ec9d142ba5925061e96952323dc11d99f Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 8 Aug 2019 09:47:48 -0400 Subject: [PATCH 0091/1013] Update phpstorm.md --- docs/advanced-usage/phpstorm.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/advanced-usage/phpstorm.md b/docs/advanced-usage/phpstorm.md index eed1ddfb9..c4ae744b6 100644 --- a/docs/advanced-usage/phpstorm.md +++ b/docs/advanced-usage/phpstorm.md @@ -12,6 +12,7 @@ weight: 5 **role** + - has parameter = YES - Prefix: `check() && auth()->user()->hasRole(` - Suffix: `)); ?>` @@ -19,6 +20,7 @@ weight: 5 -- **endrole** + - has parameter = NO - Prefix: blank - Suffix: blank @@ -26,6 +28,7 @@ weight: 5 -- **hasrole** + - has parameter = YES - Prefix: `check() && auth()->user()->hasRole(` - Suffix: `)); ?>` @@ -33,6 +36,7 @@ weight: 5 -- **endhasrole** + - has parameter = NO - Prefix: blank - Suffix: blank @@ -40,6 +44,7 @@ weight: 5 -- **hasanyrole** + - has parameter = YES - Prefix: `check() && auth()->user()->hasAnyRole(` - Suffix: `)); ?>` @@ -47,6 +52,7 @@ weight: 5 -- **endhasanyrole** + - has parameter = NO - Prefix: blank - Suffix: blank @@ -54,6 +60,7 @@ weight: 5 -- **hasallroles** + - has parameter = YES - Prefix: `check() && auth()->user()->hasAllRoles(` - Suffix: `)); ?>` @@ -61,6 +68,7 @@ weight: 5 -- **endhasallroles** + - has parameter = NO - Prefix: blank - Suffix: blank From 3492758f047dc7e0fb68d9196a847966a6fc69d7 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 12 Aug 2019 15:01:07 -0400 Subject: [PATCH 0092/1013] Update .travis.yml --- .travis.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 05d4b7ee3..0041d35b2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,9 @@ language: php +cache: + directories: + - $HOME/.composer/cache/files + php: - 7.1 - 7.2 @@ -17,7 +21,7 @@ matrix: before_script: - travis_retry composer self-update - - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-source + - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction script: - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover From cf887b8083355e52a2893f1062c8bcc6d43c5616 Mon Sep 17 00:00:00 2001 From: mekaeil Date: Tue, 27 Aug 2019 03:58:53 +0430 Subject: [PATCH 0093/1013] Readme update, new UI for management --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 7706362ef..1369c4f23 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,8 @@ The package doesn't come with any screens out of the box, you should build that - [How to create a UI for managing the permissions and roles](http://www.qcode.in/easy-roles-and-permissions-in-laravel-5-4/) +- [Laravel User Management for managing users, roles, permissions, departments and authorization](https://github.com/Mekaeil/LaravelUserManagement) by [Mekaeil](https://github.com/Mekaeil) + ### Testing From ac3fc28d8d1f023fd9e0f3c14e62fa9b953dc4c1 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 2 Sep 2019 09:34:28 -0400 Subject: [PATCH 0094/1013] Update composer.json --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 918087375..93028e28a 100644 --- a/composer.json +++ b/composer.json @@ -1,10 +1,10 @@ { "name": "spatie/laravel-permission", - "description": "Permission handling for Laravel 5.4 and up", + "description": "Permission handling for Laravel 5.4 to 5.8", "keywords": [ "spatie", "laravel", - "permission", + "permission", "roles", "permissions", "rbac", "acl", "security" ], From 45826bd08b3aebc9e87a4bc7da4695eb6c59164d Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 2 Sep 2019 09:37:38 -0400 Subject: [PATCH 0095/1013] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1369c4f23..2bdd73aed 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,9 @@ Because all permissions will be registered on [Laravel's gate](https://laravel.c $user->can('edit articles'); ``` -## Documentation +## Documentation, Installation, and Usage Instructions -You can find installation instructions and detailed instructions on how to use this package at [the dedicated documentation site](https://docs.spatie.be/laravel-permission/v2/introduction/). +See the [documentation site](https://docs.spatie.be/laravel-permission/v2/introduction/) for detailed installation and usage instructions. ## Need a UI? From 5bac2326fe73c7da9a3ae07dcf5fb44224e45f6f Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 2 Sep 2019 10:57:53 -0400 Subject: [PATCH 0096/1013] [3.0] Add Laravel 6.0 support (drops L5.7 and older) Now requires Laravel 5.8 and newer, and PHP 7.2 and newer. Older Laravel versions can use prior versions of this package. --- .gitignore | 1 + CHANGELOG.md | 6 +++++ composer.json | 37 +++++++++++++++------------ docs/_index.md | 2 +- docs/advanced-usage/cache.md | 2 +- docs/advanced-usage/extending.md | 15 ++++++----- docs/advanced-usage/unit-testing.md | 2 +- docs/basic-usage/blade-directives.md | 38 +++++++++++++++++----------- docs/installation-laravel.md | 6 ++--- docs/installation-lumen.md | 6 +++-- docs/introduction.md | 2 +- docs/upgrading.md | 3 +++ tests/BladeTest.php | 2 +- tests/CacheTest.php | 2 +- tests/MiddlewareTest.php | 2 +- tests/RoleTest.php | 2 +- tests/RouteTest.php | 2 +- tests/TestCase.php | 2 +- 18 files changed, 80 insertions(+), 52 deletions(-) diff --git a/.gitignore b/.gitignore index 3a32e03ea..8ddc91365 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ composer.lock vendor tests/temp .idea +.phpunit.result.cache diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c613cfeb..ecd360ec5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to `laravel-permission` will be documented in this file +## 3.0.0 - 2019-09-02 +- Update dependencies to allow for Laravel 6.0 +- Drop support for Laravel 5.7 and older, and PHP 7.1 and older. (They can use v2 of this package until they upgrade.) +To be clear: v3 requires minimum Laravel 5.8 and PHP 7.2 + + ## 2.37.0 - 2019-04-09 - Added `permission:show` CLI command to display a table of roles/permissions - `removeRole` now returns the model, consistent with other methods diff --git a/composer.json b/composer.json index 93028e28a..781ab762c 100644 --- a/composer.json +++ b/composer.json @@ -1,11 +1,14 @@ { "name": "spatie/laravel-permission", - "description": "Permission handling for Laravel 5.4 to 5.8", + "description": "Permission handling for Laravel 5.8 and up", "keywords": [ "spatie", "laravel", - "permission", "roles", "permissions", "rbac", + "permission", + "permissions", + "roles", "acl", + "rbac", "security" ], "homepage": "https://github.com/spatie/laravel-permission", @@ -19,15 +22,15 @@ } ], "require": { - "php" : ">=7.0", - "illuminate/auth": "~5.3.0|~5.4.0|~5.5.0|~5.6.0|~5.7.0|~5.8.0", - "illuminate/container": "~5.3.0|~5.4.0|~5.5.0|~5.6.0|~5.7.0|~5.8.0", - "illuminate/contracts": "~5.3.0|~5.4.0|~5.5.0|~5.6.0|~5.7.0|~5.8.0", - "illuminate/database": "~5.4.0|~5.5.0|~5.6.0|~5.7.0|~5.8.0" + "php" : "^7.2", + "illuminate/auth": "^5.8|^6.0", + "illuminate/container": "^5.8|^6.0", + "illuminate/contracts": "^5.8|^6.0", + "illuminate/database": "^5.8|^6.0" }, "require-dev": { - "orchestra/testbench": "~3.4.2|~3.5.0|~3.6.0|~3.7.0", - "phpunit/phpunit": "^5.7|6.2|^7.0", + "orchestra/testbench": "^3.8", + "phpunit/phpunit": "^8.0", "predis/predis": "^1.1" }, "autoload": { @@ -43,17 +46,19 @@ "Spatie\\Permission\\Test\\": "tests" } }, - "scripts": { - "test": "phpunit" - }, - "config": { - "sort-packages": true - }, "extra": { "laravel": { "providers": [ "Spatie\\Permission\\PermissionServiceProvider" ] } - } + }, + "scripts": { + "test": "phpunit" + }, + "config": { + "sort-packages": true + }, + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/docs/_index.md b/docs/_index.md index b5f18dacc..f6aefce38 100644 --- a/docs/_index.md +++ b/docs/_index.md @@ -1,5 +1,5 @@ --- -title: v2 +title: v3 slogan: Associate users with roles and permissions githubUrl: https://github.com/spatie/laravel-permission branch: master diff --git a/docs/advanced-usage/cache.md b/docs/advanced-usage/cache.md index f979e3e08..e48fdf331 100644 --- a/docs/advanced-usage/cache.md +++ b/docs/advanced-usage/cache.md @@ -5,7 +5,7 @@ weight: 4 Role and Permission data are cached to speed up performance. -While we recommend not changing the cache "key" name, if you wish to alter the expiration time you may do so in the `config/permission.php` file, in the `cache` array. Note that as of v2.26.0 the `cache` entry here is now an array, and `expiration_time` is a sub-array entry. +While we recommend not changing the cache "key" name, if you wish to alter the expiration time you may do so in the `config/permission.php` file, in the `cache` array. When you use the built-in functions for manipulating roles and permissions, the cache is automatically reset for you, and relations are automatically reloaded for the current model record: diff --git a/docs/advanced-usage/extending.md b/docs/advanced-usage/extending.md index 52a2b3ae3..5c6db7b13 100644 --- a/docs/advanced-usage/extending.md +++ b/docs/advanced-usage/extending.md @@ -3,20 +3,23 @@ title: Extending weight: 3 --- + +If you are extending or replacing the role/permission models, you will need to specify your new models in the configuration. +First be sure that you've published the configuration file (see the Installation instructions), and edit it to update the `models.role` and `models.permission` values to point to your new models. + +Note the following requirements when extending/replacing the models: + + +### Extending If you need to EXTEND the existing `Role` or `Permission` models note that: - Your `Role` model needs to extend the `Spatie\Permission\Models\Role` model - Your `Permission` model needs to extend the `Spatie\Permission\Models\Permission` model +### Replacing If you need to REPLACE the existing `Role` or `Permission` models you need to keep the following things in mind: - Your `Role` model needs to implement the `Spatie\Permission\Contracts\Role` contract - Your `Permission` model needs to implement the `Spatie\Permission\Contracts\Permission` contract -In BOTH cases, whether extending or replacing, you will need to specify your new models in the configuration. To do this you must update the `models.role` and `models.permission` values in the configuration file after publishing the configuration with this command: - -```bash -php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" --tag="config" -``` - diff --git a/docs/advanced-usage/unit-testing.md b/docs/advanced-usage/unit-testing.md index 881a927b0..ad12b9ea9 100644 --- a/docs/advanced-usage/unit-testing.md +++ b/docs/advanced-usage/unit-testing.md @@ -6,7 +6,7 @@ weight: 1 In your application's tests, if you are not seeding roles and permissions as part of your test `setUp()` then you may run into a chicken/egg situation where roles and permissions aren't registered with the gate (because your tests create them after that gate registration is done). Working around this is simple: In your tests simply add a `setUp()` instruction to re-register the permissions, like this: ```php - public function setUp() + public function setUp(): void { // first include all the normal setUp operations parent::setUp(); diff --git a/docs/basic-usage/blade-directives.md b/docs/basic-usage/blade-directives.md index 543e249e9..99eaf3ac2 100644 --- a/docs/basic-usage/blade-directives.md +++ b/docs/basic-usage/blade-directives.md @@ -3,7 +3,29 @@ title: Using Blade directives weight: 4 --- -This package also adds Blade directives to verify whether the currently logged in user has all or any of a given list of roles. +#### Blade and Permissions +This package doesn't add any **permission**-specific Blade directives. +Instead, use Laravel's native `@can` directive to check if a user has a certain permission. + +```php +@can('edit articles') + // +@endcan +``` +or +```php +@if(auth()->user()->can('edit articles') && $some_other_condition) + // +@endif +``` + +You can use `@can`, `@cannot`, `@canany`, and `@guest` to test for permission-related access. + + +### Roles +As discussed in the Best Practices section of the docs, it is strongly recommended to always use permission directives, instead of role directives. + +However, in case you need to test for Roles, this package also adds Blade directives to verify whether the currently logged in user has all or any of a given list of roles. Optionally you can pass in the `guard` that the check will be performed on as a second argument. @@ -65,17 +87,3 @@ Alternatively, `@unlessrole` gives the reverse for checking a singular role, lik @endunlessrole ``` -#### Blade and Permissions -This package doesn't add any permission-specific Blade directives. Instead, use Laravel's native `@can` directive to check if a user has a certain permission. - -```php -@can('edit articles') - // -@endcan -``` -or -```php -@if(auth()->user()->can('edit articles') && $some_other_condition) - // -@endif -``` diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 15edf0346..70218e51d 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -3,7 +3,7 @@ title: Installation in Laravel weight: 4 --- -This package can be used in Laravel 5.4 or higher. If you are using an older version of Laravel, take a look at [the v1 branch of this package](https://github.com/spatie/laravel-permission/tree/v1). +This package can be used in Laravel 5.8 or higher. You can install the package via composer: @@ -20,7 +20,7 @@ The service provider will automatically get registered. Or you may manually add ]; ``` -You can publish [the migration](https://github.com/spatie/laravel-permission/blob/master/database/migrations/create_permission_tables.php.stub) with: +You must publish [the migration](https://github.com/spatie/laravel-permission/blob/master/database/migrations/create_permission_tables.php.stub) with: ```bash php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" --tag="migrations" @@ -41,7 +41,7 @@ You can publish the config file with: php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" --tag="config" ``` -When published, [the `config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) contains: +When published, [the `config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) initially contains: ```php return [ diff --git a/docs/installation-lumen.md b/docs/installation-lumen.md index f2d1a4b85..cfc6569be 100644 --- a/docs/installation-lumen.md +++ b/docs/installation-lumen.md @@ -3,7 +3,9 @@ title: Installation in Lumen weight: 4 --- -You can install the package via Composer: +NOTE: Lumen is not officially supported by this package. However, the following are some steps which may help get you started. + +First, install the package via Composer: ``` bash composer require spatie/laravel-permission @@ -33,7 +35,7 @@ $app->routeMiddleware([ ]); ``` -As well as the config file, service provider, and cache alias: +Also register the config file, service provider, and cache alias: ```php $app->configure('permission'); diff --git a/docs/introduction.md b/docs/introduction.md index 66868b645..741a9ba83 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -19,7 +19,7 @@ $role->givePermissionTo('edit articles'); If you're using multiple guards we've got you covered as well. Every guard will have its own set of permissions and roles that can be assigned to the guard's users. Read about it in the [using multiple guards](#using-multiple-guards) section of the readme. -Because all permissions will be registered on [Laravel's gate](https://laravel.com/docs/5.5/authorization), you can check if a user has a permission with Laravel's default `can` function: +Because all permissions will be registered on [Laravel's gate](https://laravel.com/docs/authorization), you can check if a user has a permission with Laravel's default `can` function: ```php $user->can('edit articles'); diff --git a/docs/upgrading.md b/docs/upgrading.md index 25aa5208a..c0f407541 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -3,6 +3,9 @@ title: Upgrading weight: 4 --- +### Upgrading from v2 to v3 +There are no special requirements for upgrading from v2 to v3, other than changing `^2.xx` (xx can vary) to `^3.0` in your `composer.json` and running `composer updat`. Of course, your app must meet the minimum requirements as well. + ### Upgrading from v1 to v2 If you're upgrading from v1 to v2, there's no built-in automatic migration/conversion of your data. You will need to carefully adapt your code and your data manually. diff --git a/tests/BladeTest.php b/tests/BladeTest.php index 88a9e7192..75f3d154c 100644 --- a/tests/BladeTest.php +++ b/tests/BladeTest.php @@ -7,7 +7,7 @@ class BladeTest extends TestCase { - public function setUp() + public function setUp(): void { parent::setUp(); diff --git a/tests/CacheTest.php b/tests/CacheTest.php index 001658e4f..084e02ee4 100644 --- a/tests/CacheTest.php +++ b/tests/CacheTest.php @@ -18,7 +18,7 @@ class CacheTest extends TestCase protected $registrar; - public function setUp() + public function setUp(): void { parent::setUp(); diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php index d2015522b..a59ff6bcb 100644 --- a/tests/MiddlewareTest.php +++ b/tests/MiddlewareTest.php @@ -16,7 +16,7 @@ class MiddlewareTest extends TestCase protected $permissionMiddleware; protected $roleOrPermissionMiddleware; - public function setUp() + public function setUp(): void { parent::setUp(); diff --git a/tests/RoleTest.php b/tests/RoleTest.php index 31d06c083..207a4c6ef 100644 --- a/tests/RoleTest.php +++ b/tests/RoleTest.php @@ -11,7 +11,7 @@ class RoleTest extends TestCase { - public function setUp() + public function setUp(): void { parent::setUp(); diff --git a/tests/RouteTest.php b/tests/RouteTest.php index cf76861b8..0bfbad339 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -6,7 +6,7 @@ class RouteTest extends TestCase { - public function setUp() + public function setUp(): void { parent::setUp(); diff --git a/tests/TestCase.php b/tests/TestCase.php index 771a8c45d..3a19bf695 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -31,7 +31,7 @@ abstract class TestCase extends Orchestra /** @var \Spatie\Permission\Models\Permission */ protected $testAdminPermission; - public function setUp() + public function setUp(): void { parent::setUp(); From 73e8c7a6cd63abffd75c6a82f7e70a9823969728 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 2 Sep 2019 11:29:12 -0400 Subject: [PATCH 0097/1013] Update .travis.yml --- .travis.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0041d35b2..6cf586ba8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,16 +5,14 @@ cache: - $HOME/.composer/cache/files php: - - 7.1 - 7.2 - 7.3 - 7.4snapshot matrix: include: - - php: 7.0 - env: COMPOSER_FLAGS="--prefer-lowest" - - php: 7.1 + - php: 7.2 + - php: 7.2 env: COMPOSER_FLAGS="--prefer-lowest" allow_failures: - php: 7.4snapshot From 4d93e8493b5d09125ab881361c1a332862aa19bd Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 2 Sep 2019 13:12:21 -0400 Subject: [PATCH 0098/1013] Update Changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecd360ec5..6c5115c3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ All notable changes to `laravel-permission` will be documented in this file To be clear: v3 requires minimum Laravel 5.8 and PHP 7.2 +## 2.38.0 - 2019-09-02 +- Allow support for multiple role/permission models +- Load roles relationship only when missing +- Wrap helpers in function_exists() check + ## 2.37.0 - 2019-04-09 - Added `permission:show` CLI command to display a table of roles/permissions - `removeRole` now returns the model, consistent with other methods From 8915850ceda9fcd1a50d2789af6d6d40f899113b Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 2 Sep 2019 13:17:25 -0400 Subject: [PATCH 0099/1013] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2bdd73aed..adecac454 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ $user->can('edit articles'); ## Documentation, Installation, and Usage Instructions -See the [documentation site](https://docs.spatie.be/laravel-permission/v2/introduction/) for detailed installation and usage instructions. +See the [documentation site](https://docs.spatie.be/laravel-permission/v3/introduction/) for detailed installation and usage instructions. ## Need a UI? From 539e6f6d47e08aabd636084cafad7326a68baedd Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 2 Sep 2019 13:23:53 -0400 Subject: [PATCH 0100/1013] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index adecac454..6fc715743 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,7 @@ $user->assignRole('writer'); $role->givePermissionTo('edit articles'); ``` -If you're using multiple guards we've got you covered as well. Every guard will have its own set of permissions and roles that can be assigned to the guard's users. Read about it in the [using multiple guards](#using-multiple-guards) section of the readme. - -Because all permissions will be registered on [Laravel's gate](https://laravel.com/docs/5.5/authorization), you can check if a user has a permission with Laravel's default `can` function: +Because all permissions will be registered on [Laravel's gate](https://laravel.com/docs/authorization), you can check if a user has a permission with Laravel's default `can` function: ```php $user->can('edit articles'); @@ -98,6 +96,8 @@ Special thanks to [Alex Vanderbist](https://github.com/AlexVanderbist) who great - [Povilas Korop](https://twitter.com/@povilaskorop) did an excellent job listing the alternatives [in an article on Laravel News](https://laravel-news.com/two-best-roles-permissions-packages). In that same article, he compares laravel-permission to [Joseph Silber](https://github.com/JosephSilber)'s [Bouncer]((https://github.com/JosephSilber/bouncer)), which in our book is also an excellent package. - [ultraware/roles](https://github.com/ultraware/roles) takes a slightly different approach to its features. +- [santigarcor/laratrust](https://github.com/santigarcor/laratrust) implements team support +- [zizaco/entrust](https://github.com/zizaco/entrust) offers some wildcard pattern matching ## Support us From 63ad7a7c11570c7e998c6fd43754afc5568e2144 Mon Sep 17 00:00:00 2001 From: Ryan Hoshor Date: Tue, 3 Sep 2019 10:05:01 -0500 Subject: [PATCH 0101/1013] Fixed a small typo --- docs/upgrading.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index c0f407541..8f5eeec50 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -4,7 +4,7 @@ weight: 4 --- ### Upgrading from v2 to v3 -There are no special requirements for upgrading from v2 to v3, other than changing `^2.xx` (xx can vary) to `^3.0` in your `composer.json` and running `composer updat`. Of course, your app must meet the minimum requirements as well. +There are no special requirements for upgrading from v2 to v3, other than changing `^2.xx` (xx can vary) to `^3.0` in your `composer.json` and running `composer update`. Of course, your app must meet the minimum requirements as well. ### Upgrading from v1 to v2 If you're upgrading from v1 to v2, there's no built-in automatic migration/conversion of your data. From 8877b19f9891a975891076c68fd1c52236639fff Mon Sep 17 00:00:00 2001 From: Willem Van Bockstal Date: Wed, 4 Sep 2019 14:29:00 +0200 Subject: [PATCH 0102/1013] Add header image --- docs/images/header.jpg | Bin 0 -> 391838 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/images/header.jpg diff --git a/docs/images/header.jpg b/docs/images/header.jpg new file mode 100644 index 0000000000000000000000000000000000000000..489010beed775c83617b28d9319bdb7eb28ed107 GIT binary patch literal 391838 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
irDwlGw2f|^B#lhbiU)xlqgwN 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

kjB(E(O1sRDnU#2#WzS(_L5 zMITT$WsXEn`=mS)kKy3-_G+%{=Aq=EC0J+-FcF)mi`BC*D?DYdEf$LS_My;c-4m32 z`)-Z-3Hehi0;k2i(VNGnGT*ZIc@ztJz0LFFbX4=`nkUSX@VQJvrj)hc8TH@d_u=9R zh_CD3UR4_SV!GLCid$d&Q@dW~lE}9`+alG!-Iaqg zq+})%$qWa64jWS$$|4e|WNi5uis{6Plr)r8HB>R2pC8X0Nm*h-mg26P!=7eLaVvfw zEH?d*t9FhZa(i9feJPJb#|fZ)HNV@c@SrqJnjYcgqe>X>06|r$%uqh`{#!1+QJ#{O zW`A>n^Q-BF?b3cn0?Y+$#!*>4OGR-~8W^Vk09#GUQkQ}p@DflzoB;Hi5MNidWVW_- zcnHJ$_mO8viA|1Y^)h#kzyq_gk&3+;gV+7~2nDjs=M&f4`t9KI9;A5ea3a&XWjcyE z_^0!RF=9k_nHk)?E1E3)08ov@h%R*CkC6uqTfN#;NrrqZcj$BDb!jZ?39cLmu9Wl)#ddwAs0Ci$XfMF>k4pFXF%BPFD9 zm9S$L-+m1Jb~ZhqE|OEm%o-|kTuu3LnXIO3H!?Lfu-Q10uBYmpPsLc2Wb!~f!1)u= zOQ5_>A-G|bxZVI%{!(P8hq&K!O$6q7+`(FEn)eMnLA^l@|J*vT(!f`Ink<;Cf;-iB zJDPD%Jk>9GyAzf5sy>MweAsw(tO9Z)#XYv@YP)ZbzD}0WFi1JEY#%^P zBSnp65ICurV6_o480R=Zq&X8|1A00N^G;6kwxt(hWF%^Lb^V`vPJ1Nnn~$42i<-<3 zKdbWUrIS(JUq$bI~qOVt&Y zOj@r=l?^=5((LJ#f%7RT>yLvbV>;vJ#s&a$i)v@p0l!r0tHt}x z{#RcpF?Qc%ta{f_>NDih$cCP}9;a{G2V0?of$0Rl%( z&e_N7#LESKH<%?o-W1xahjjwW%Xu_>X_Erj-KMtO)nU8{lKmrZ0?D1c!uOn8xR*<4 zB(VZA){~q4ez)Mla#^t8nP(C0di;`RlMkbj>m-HgC``Pp>50ti9clIP!c%evamCOk z54|Dl9*683oZo5vb84Bi9AODWjWIFbIQ#a{BaKmzcWc0+Mv6;0x!zk;c3F(HYDn4W zBR+a;>Vsjyza#NDLRZ2)^N4kX=I+cmj#SkgCpZ%3Qw+&B+(kfeSU`JHr@UUG|61iZ zq=&W~-mODj#e9$j7hAP#%-v^@128NacL+Ji%5%H~gZs+$%ko6|`76c?6tAw8sojlUOqvr}*B2&bo{ zo2n$FOkvaAd!e5id+j0|3=5>QW)xd8fKQor$4XF)zL)PCY;;rv2DzLFOc z8Oyu!qD}T*@VX1hwwr$I=Tm$e=}++q9{$@54Q9^lZ{O_;S=)#cY(n2TJRX|)xU5u( zx)VzuWc_*fXGQA+GJ|g(ydU(co4DxY1liI{gM)qm9%(5OOssY+t}XS?xrY8t3c9A} zKJRZqFI&uqPN{tC`1@J-+npO>XJUGvJCrM%DxhTw>$mnpQ`GL+K^HDR%k)10CE}&8 ziy7>`H?35UH6JNVNDZD}+TMQnsuulxPqpevq+!)9Egs8j0a4c4N8O`j;CK6teG!G9 zKGyPk&>is5B?K8i=zY`7{_h1Z!_JniiL8B?GLbIf{p)Ch<4GUW)=vN@(A)^q-0WIz zt-AGo?3tj2P&qrl(av$pS-=@bY05jM+w8Aj&%QX>Q%l`=eiK13QmS?{ESNX{%1vUo zziLqkNZ?duH+6CwXX?n6m$N_!m%!n;Txn-3ny}Ncq`QC|`2KW^D$>I-zzx6ek*?r$UNaz=`<3NN5z2i7FYHR7t%`)x z{nc%EpTLST{gTD^Q8^N=d=6){Q}dktmv$F2?w@Sc{{6lmm&qXk0+ME@Oe*f_W=8UB z0K@7LgHj8daZL=+i`7jeJn91~q4FJAaFgU}F(>AX)}O<}6aUA;>iye_p-km~XLove zHvR4&?j*n7qxyF9O4dRc$zX7hX0@~58uT-7?dIo~2(BK)o27^+rxTIQMRQ{hYf8Y$ zQ}iaA#TL1Ha-#96*>qvCJ4SZ-?`|+`peTcDmRyUr;P=t0dVO{^iLDRs>FAh_JB#EB zo<$N?=!*3JKJz^)2-C}v4mMER)3vuPd0##@KN(gZUEm(7zdfp-P5HH}K}ebrn;ozG zj9|#~Rsc<5pM%1cR*vqjH6f>~V}4W};RFSeTUl;5cYxs`1#?reNI}c+Bzvy7N1+p9 zE{gv;$U!&)b%4|Nq*`Ms9|fWQXT+vJ*`{nO>DNKbswrW*Qf?Nr#>+RK$KGNK0*bk4 zJ|C|&&O-a|$Mie0ON%T~n6p^%ey4P%rDVt!RkNi4W2vnbp1L8$CPRa6bJzs4(Xt+ev8e<9!$ zs?Uk3_qcCQ>rXm=9PfEhi@e8TDc-e+;W%QYtj3T}6@ntDT`XQz2+li+dMp=cAcZf( zXJS0l#1^bE8~O-Sn@QgSd$^I4+Z)Qd<{#B!kbL`4Il)v|8?Y+O0y&B?^P9X>F#>5w z-KX77FxoY=*3soL-~*de_K8My(~mC`K}6ABJUld#g!1sd5xNIoYz9WlA)C~!2-`nd&up3pcJ!O=NU1kzCMG1(HnS)jjq~eg z1@a@Ga_TMr)+tg*;^%RcDw76%;jKIeZ3pP!+mg`~%Qw}XV#=n&0DQXXjki}1T@XAS zgk^qtZJU)sArD(7^mX8C`Q3ElP^{iL?DwYS7uX2Y*26h&mtYyDSpcp27n!5av+|oc_=C~&(00EiP`@sia=Mo+Hm+>Z-K0nd01P?5R z@N->s37tx&SGJ(@I`Yfx$vA`NIa6W<>z@--UD5Zo{8hW(*;7G+;xR~gT@#Eg>ygG< zn4fUAmO7-ycAzT2STF^WOHF7MO58J~x%KJ`i<)jjg5e+pA;5B#2}DChC0XuRgtp6| z61>t8I_jx5xAkXvJ1*+m02Opr3=4xKFdnWV&Jh;SDh4x6>oVGCKrAVx^2DT9@QS|c zLj;X#(t*mK8^u@vK)nHadi0R-USHcH<_}5T&w-HgKJ7$ZSze90P38I^TiZa;FXk?N zyfj-p529_Kg*>tf@_zUn8jGnrs~~wq?tx=_TZpaP8rxB~%;e_D@?qgFs!LWDY$JcA zb+IT5C(5&K?fB=WPq9_*oZ>RqvUx(aqz}c!m2-a^BZz~+lf(wfvXQ$>g-R)EW`?)Y ztwTEj*18XvfV|2{voWloIIXl$91xotsU3_ITPcfG*g_@;OUY>>9t5|?XQ{g6C(7NG z4~E!BiKy0ZkSr^hXCAq1(i7j3mUIh$20!rF5#e*cZYaO{ijhD-yY6%KURYiS&TWq; zRWtk;&Sx%w^aR#t4TQWnkIH#r{u5}X7aFSVABEYK+opcJ7FMXVnVAXHsx z@s+hN6dcx|lm*iTpxRRRALkKT&(>QO;9ldF4kv=9##eTxUYUyK5AV(hPa&#uDFM*} zWGyDEBI0HU{}tD@8msX;CD?<25V{St0`-9jEWf**q<-cPThR|aS%HsU5bnY9i5)xq z)yiJj{p;NT5`*_u_~3f+PS%{<9xb6RqdoD;RX5`G0rlBaL|Kkr!goJ-VlkZ}W1M2? z?%%nX@LamPH#``%gnG=Vf<8+i>8Wt*Vlz-{BsmxePB`|iVv)*jEY0!T>cPqALI%=(WxF*Kpu!)S{Sz%m<7VX+Knpe>>c=l|E!pJCpWN|&WxWpkXX}?yeLhd&4 zfPD0hNI3t4(O}Ek_g>Vo?6|m=pl@-y|I=8YCN7mgkkK1zT?dmg6-a-uD}?eA8}3w_ zgZsf9<7MHM`NF|m%`Ep6ED$b@GCOGD_6P;Cd{gy54V%!3oQ@ zG!@@`AFY&t*KvIX>-n<~g1;Be#}v>z-cN5u(=ar)jG(2r2*_Yb9AkxY z)>i{&X@WfLeK;9pE-oN&P=Z<4bN2-Y>Fx`MU;otDKi0EDhcN{&7?eu}TRSLNH2u^B z2~g!(*UAL4-y7=LA9G2<{t14XEqo9xg+Yqfr_~J>N%biFRqyh?rcFar6pa_Dj2Z-J z)LdDX4hqPhf1UqvpScNUC_fCXnZiBktWK%YP2piqCNpoP)H`gTC}X=@%dhl8Y^gcK z8d_d6E+v}JLbO1NtLpmLS=SB(SSga|md1>enPkY`&da;ga&qw zB4Zpf%4~^>2&m()ksYd}p3IG%< ziHN_P(y|RE0J%L7FJv1U8VmfMyJ!blYb8_ zFj=)s;|bwsKq=64-)I9z5zN}{BwFUvrN&JH;0!w3ZsAe|icnSvaJKjLmynqa=!jpa z5pCYrA49ceQY2V6O5R=czJ00W_FI@9;(_RSQxOhBQVCjuZF}9a0l-NLfO1tp(N(Th z1lcSX`}}8-yIEA3a@>W4HgG$UEHjT&gQr3IBQj!=Nr7rdOO*rSs;-dX#|eQ}C!R-WqwV#Pl+^NNVb$tVD1$Z%~0h0>MB!pu;TRz?J#= zKfsrx4auSLldQAN>FpcOEA5}$8fPt$4&N=H1AUSri-9V${N#taN7c1IZL?K}Qay(>mS0lK0CU z81#^GCjPfuq?Jf33Y;Y9J=^aG?K;$}2ZljiajLQl`)B@Q$iw(MtnWazLi9Rp` z$ubVNGpmf z#g26=d_Bdn=BB?zCYo35rZ}fZcy`>%8rDjV&Rihenx+Tnaq%t*kbo&c06j(TPtx9P znnJN#bb}dc@q-9G{#)O;@7L_?T$c_3d3jS!vM(0eoB9LJUKDF+F;s(MOeXUh_dfq+ zZI6*327O$9cH?tJAK#T&n99>wZ4(VJ+req2OXq_WIqC=p%ujL79>3L z*`cbroAbQ2Z|<|?W=bJt*Zn4sc`wCJ$>+aMMQvQ=OP!Hke3r7-J#%mW)?K}E5Y*G> zan7R5plLyWto!#eertc|N~t}1Skl?K$&2Ray^UMP`ux^4QpOxdnG>%)QTKVIQvU2|R>*(M%r3gxZ-@(Y5}Cuw2O@S6`%hl#&w z&IO;HnzQ+4AdJ@_LP-x15ApjEVR+C9m{owW`xIi1i5JzR`E4Xa3NV3bzg4n;fUQ{- zLrkWy2w8_1(rZr$+%+V``C+6wsrCe^_TY^Wt(@_vkcFq9d@!3HP;t5Wd!7LQ__=}& z$gg4%i_9`!gaasCBKLKGJo0z|V};0@6=^5N*Pu;nRlMgwe=s>sZHE)GSVERq7BF

vHN9;wNAbhsYN-o;3u6nl}q*%b_-5m4f;k;hR$#iEVMs0G#Zt_SdRe9#>*&5AB!z zBMr<2R7`{Q5QbQQ7#-Awe+ST&{HrMxG~YU$`SqIsu(0d7CglJ$RfO zmNf)n>gYY4=2rCgG0K~)7SA%?R4<|Cm>}Wc%&hvYm&gvpTd4-knqZJc{$pTFxR zM86CWm|EZOovjFO=W81OI%bPnRPP()%wKpevq4DZ>PYu&KvsbE`V;t=v=MyZz?Cb_mHd8?mG87&#GOt0WLo+B6V4QyNkf+&u%;is@rX#oRH~~m2?TI_elqS z^wXU1YSQ-iDa;W6Vlh-~LFt4LK6G(u0YN>HER{p^jDUCh;km5t??bzS_gbBvaz8 zMU4WTtvrAAZSRgDO{PNedHQ83En>TS7{zLVkOcF*n})@TrXfdP1SaKTOjs)8wW?E~ILk)<$(y8(c-lYla2q ziPr7rjx-s|Nq=r-H4vP4RkAI8@^q=)g)^*2LiTd`%9Gz$g$0b z!EUP9UV@VE_Pj9~qhk@1l?u|&6}n>YnXs^q$T4GM(3fg`NHjGL$zN(SW%p0aPFn&9z?$GO|I^#LZE9UaN@O1SnC71o?-e)WCc zw;%o9+DBSG%9*{S^U|J~Lv@wRhfdarz&7bX+#?`MPHpe_@xtGJHd)@P=mr2ANCLT? z`$i`PA{X4ZxRSTv6_vptLB=z05Utnnw&AG_zyFi9cE`bb_9b2(IfvYVHqa*cFUp3b zE`yJY$H7u@DevBEe_7uh*i3*$MHr!vCh~5;Sr$Fpt?-|I)`HEOpsV(%H0+G2!QwN<;2wlWH<gc=RzQ31uM)FQD*dX$#12WVsNYpSYo{FDWPeQp8L_k+<(2RJ_ywL6yWU zX`2w1z}Y?Av%zd$d^E))e!LgL$KKVA9Dd&!|CVQdl=18OD29xp>+^&*VanOBFF^!W zF{V(VuksoyOy<#{j#DLna-0vcbtJxXZF^FxC9kNXFHMC<*x@ZxgtAr2M8Qn8Z--lp zss>%Z>GM>5Y_pnnc^SEp1zqX(KH9gEu1ft6@a`{1l$%O45_q0Wqd!a##x zJ*#l510B+@A7jDS$NymYDMpzCzXQMv`W_qBCVb0i3xg%42P6w<>~iDv~jB1 zt;hQs1J9mt;shrFt6pA(E7HcS^i!ONUNGKpSUt~xUl!>Wy5tOVEc z1C_oM>nY9RP#F6h=u_AZQ!$|yE;RZ_F!qbBT5O6_z#mUt8EE!JQ0@M!??qve50$&! zjaE_@hdZy70*^ngGZ*0^=d|~{x##UavBU6!6Xg{%@>bG1-S*Gs=4KzbuRwrcvzclJ zj`0Sz2DTDCe3Xx|MuIadbGf%EWg2~KKiiV0?V!TdYo1tVWgEk=wHK|Usx{V5!Dhmh z+Feqt3ZNk;FEXB@+tz|5JRcM&x zJoVDiY?vIVDqI-Vcr(p$xT_J1>mOyIR&Czz1u;!ZZ>2SOdDLINuVex`H+WiOxf0x= zgtdR9TYw!b4}wY2>0b5nb7o1DR{M5Djn_%SZftYn#8(KKk+?j2xje?>u1son;lA|r znXZfPJWoEow{O)EqZQd4FCQ)l;SjM%(GqWAe>!JPGW)(FHCDIQ$8S#v4^^HeO@B20 zm;Us}^%YmY$XyC8Ri`tV-sc_v-iC*_8C`iwTy}(Bf2D)tYNnmLMCed#lsHUUK&dFg z#>-0LZss217Afx3o|Cg-a7SkuAD$0nGEfset^1zqaB7De)wEa0Z=Y{Xxlda%nI`t) z<4p9nuBekDDXH=wz&+Pyf`Lm?OP?S%vuqi0;d1<;Q9zt0FVo4~YH3lmdxz!;^tMxN zC15OP@Sge1y1Xpc6Tz6|Y=2)RIu@%gO@@M^xYi>jB@Y{e3xsC>9ZP2;ZQ4tZJ7tv4 zHJrS=oaMKCdC`({FdY%VAve91rmUQN>o*Doa;jh@;!g75#!yBv{-*sNPJegzs^YVZ zyH2#hHmj4|TAlsXV%P8V2mhG(4&0cmpVcYP@HHk5#estd>2J#CbfsKW^*Bto8qVk{ zy~q%>tfRJnqBD*ko$wR`4)wB9n+w6_6x!ELK9%KagWM`{;EVamybqTTIY$-2_0+qT z)z4k~L|3F57h)C{IUfCd;?NLeo^&pD{&(so=N-O>+Zy*+l|^bQ@W6h!eyZi96x;;k z@-}#mJeqIHCl@ra2@eqBCvmpf?j~miT6Jm#l?o!W$8}atY2gND9d;M3e-V!%f)yNm zAZ$RiH&Z#Lc&Ch;<5d(~Lt+uZjfryqyFWK&3Z~{~ycY}Sl4AF+s z%iUO2k#&I+q^p%H{?C`Ix9)DIIG?JDL=JzIcGp*;Y25ZOvj&2-E8 zW{#7Ri^mdKgbCy|FWWeXdd}0UfE>dj^zFH5ErA^ZQ8&4y-vv4|gL{{%*F8XbCe$1X zwWZck9iHqO`XeJu@B_UEm#P2+*P$k^JDD4dpaqfA`nxW~(g!-Lm*NyOK!T4}&QT@n z3xJo7DWF=S(2J%q%6mpmpLg>2^x`Jm5%On`D@qWfcDrK0h*^U(v76${o+pxwLT&% zqIO`34)H?_j6(RREsU-2aocCkR1HMyxx49#BVDmkv?Mu!;bo-*tA&RSyB$dujX&7KvE|Ofu#wO`a`vu+SUF=pAu0sVc9D-K z1nL-Z0In_4L&w03soiPxuPtS<+E=$81(lNJBuWo`c3R8K9Q8>n;H`b(Nono|SLZ=E z8_lw%S3egaWU%J+n0|W~DObfkHM$`gXj*HkJ)3_QpvhtyGp}vgpmMCup+iM9vu7<& z=^aWMIS;a8!)={~y3yeoy6sg>xvtd^At5|OYN864>F&STexk|^KJMvgTTU6ql2lIF zYpxioZC$Z7#h?n{YYQ{cQqGi)N{`G1jFV(T{3KSqAi{(E?$-XchGmG5U}Ui@$+?Kr zfISPwS8%>u)2l3nr;olw+ywEV;A|E=J&I6f zuwdc%+lc*ZF&x}_J7Dx65t2dU!7%)Co2?=`Uz4R6pw22dg!$WrM`Op0S ziJw2WX8FSk!6v?W6BJ7{eDH_eEI){Q&S_RyvP&lKlCQjEyc{ejNgJqhxzm?HaVX2) zyHJw_N?s8M+_iz7!z5iXLbY(cZ}P;hgu1SvHzl?++I0fr`uG80?O%(2!ss10K6wt2 zVkSi<;l69phPg)3Quk1trA%i|sD z-|}q%;#Nr4Zn8}a6XL;}`8@H&47kIeg;$3&7vaTJZQx<%1kkS42zbBhCu1zXA=Q2$ zk<_JjN*{!AEf7R)pGCqV^>oYbO$`i@X$u*aKIOZ^^rg)lEZ8^2LNW{j#-(vf7k#}GO2)Cix0mlD{D_l8-3;#M%x<@(oX7&aCO;>@l9kou>MhNib>pI=GV5Wf8^a^G-|Rnb(`y zF!*=ULJKo`pOPyGSsY4Pgeb$(9=!T4Lr ztXd)G)q5sJ|EV5i{SUeH6%9#EDJjmk{{ix!Z}mR&H8KNyJk)B+gru?Bt?N{cZT}mo ztnGqpEL5EeP5#U49qFnfy!W*ZnKc!XxZY~H<>@4|+jie>XarKD2P?Jiy(IX1nPp3` zfPqlnw=TMr!I5zI(*lY0$O6<7jiIJtgpld?W75Wpi|;zEh%ZH;2|!jecM#~+5GLh$ z?4JRLR*(xl#ajx$?Mni{&C8Snmy@cZ{())`tcB#20({!oaBCZlAUu8Idm*P+)n-H( z=Zi|TTi?>H$x@)W$Y2-bvPP8wY0yae=X&-*g}W?EtCMxV{igxO1Xs@>nuLb3?!E!YZVYq3|YBS*ubX*7(1<{yI$Ba-xZtu}eAgC3YW9CRi){ zA>r6f7G$4FF)q>Dc2cV}xphw%LG^YrhWVoqxiCNhP#CXUQ)P|0nSmk{HuPs$nongI zzfE92u5J1nFo;*1(W)R{C?E2gVj-sm;6vv3)ymiFZ$QHJ;R$+umCf#<8@kmkREM4* ziKcKrRNt;?V~{c4c-quA>RsCLc~!-^jFMQF`&)&`Ce0V6H2Ec|lAhxBoip37%{`x$ zOsnY$H3c8IY}!8Iva6SVuEx13BrYWQ*VnXa<=OsK$yG%+aws8If$Zy9UW7!k+~z~Y zakrJmHMuXuC(An&!ir*OK<#B#hHYp%VNcR0wGBYmWL@ng^H?vHifeUJ6KZ(rL95O8 zHS%jhsQ@Vd#!Y5WNDygQ7F8dbJlZfY6j=IRFI|wzi#d#gWQwJh6o}C|TiGcrux!wr z?Qe!NfsEG9=-n)DTC5z;{MK66Eeu&!t^VhJLu^$}mwSGE;7#*kMB%6GcIKo{seMbK zb8F2XpF0l3_NQ^ikV*(&XTCjpI4=uev03|4^hij;Qo#N{z%BeMc4nQ4xeG^;2T~tzO`l1=nRy zxTWQOFQ9c@?4sPk$rcbXzIPFQj`D0=ZkD+3tCV4uJ)CBF>uBcHfq(3w#-Ksz;2((B!x`dSf*J8B`f==`+@yniqPB>}na&NYtB&+oMC{N zT#F2(zjfO{m2q^}MCU{&X^Blu56|hsiq_M0n!&|Iuhg@TrY}_03TSAH$?_Yf&~7FO zGYiTdlS&w_g9NH2`5t7`V4`Zxy922`zZp8G`fEUbY)<t=2hN5R)#%qc>PnMa71QK74fvq}Q_}&(AvgX9j*5c#n zwu}f&j~#Q#LFsPZZ;K>p+(wy5IyS+y;HSueSea3MHdk}d>}^rz26vxn5i0ud$XIyntNxPBLy3Vu3k?40qy;hn^H^bMEkl6C>&{W%oge+(O=M)i#} z*1Pu#3%3FTmqg~-$zJ>XhSq0(Jg|sQbyP#RElO=5Q($wO4=Lsiylxld%iOaFaiYg3+|N!kM?GHmp8a)|B4q_te)F}gWB_jo3v`m??^Nq^pjY9h>k9x$ zV5E9cu^u;u_inE6RU3xdzIMC6j9q5uGe8SIl8e7X)`g-XU)e>Wq@PRqnGPq2~VzNNaDfU%6M+rbzA+mQsTP=VRTR0 z=~0`3HU}Ts;1w-;v`8w4QlL_HCWYGxqRS@$246HzR@c;6NNQyUY^};SM3%3s#KF@x z>RjexI`-{XWuw3otr8ODa%)vJ)`{TVJsO&@2nl6+f36sE7@(5Xu{G6WSbJDKV4gf0 zLQ^M^yVH8aIl{Fqa&L2$2$S1Ut^P!p9;}~eLKb+Ls*_7f;{HB+dCM1|HxsjmP9Q3N z=W_fHVD~KYrjO8W^R%3Xt?MK2CQL^4l#!v8@}Yznkd2eYbb(aRcm4H>Z^r(2_HG-B&QUkyvvnb1e{La* z4L~O7s;`LPY3* zGD~6tGNlzL!Dc949b;kS6pYv9p0)ukiZ_GKAbJPNXNE;~qPRh%c>}eMH^HKaM-(?A zpc$UK9K(RKHGX4yV^wN^hmmNdO8NMi)xpdOztRG-Ij`hq&u0@D-h`@MKQhQ)Laz%> z*s7fr15oqcJh0QY9ARSW6ZM3vMAli6-3PQVuU@(p{5V7K4=3*g*x%AHjoo(DfGnHO zRm9a)Sqacz+8qiM?TLH&{mXoJhl}|$ep=}f(o#P3&3^!pD_xJtJF&70U)@*D70Ih# z;k@rGXAn8H+a!hN2SH4B2(3k>R^HEij1Ajo$2ueJu(tvK0i0zH;}nB&o*v<-p#SyQ zRbIc!D(OJhd&|c)2b5ds29lTmv~Vqr&IJf0YCq@8J*ri;v@c(jG^9MAlCARDcTE6O zE*{l#bNm*&Lt%NGyF49r2ZZcS6w7KpsTN699%grFu(^TvRx}}|N#E&a3;Z&S!zzSk zB2qpF?%K@G-f%mVG;%W3)ysoCO#(b7yfHYR>wEWYixl1UtMqOH;pEj&GMQldQGYLt z^(Z#x4`7R2@3Izy7q{!;thCH$K%P-FtSe$8qcTMrLyKy{vLaG&`U7=cgS1WS{hj8w@=y zw+)mbcMS#E!N0lu z{Nr$4tdoTszRH&m1Uhh!@SrS`Qy^2)qoP22*ID1@{}Z@yXcd3yDcs42WtZ z{I*xtCYy5}<$#^}Nn?e|tw+}@NlCGzmSk4@7O_vCnmzc9%SdspdW?cd)EX8IJ2*Ub zEwBm zU}pNtKkJXDFO4Mkhd+#u;}C|o{9N^l9)_a!UWdH?cT+UYJRiYfB)U+s-N>sJap<%k zr+9MH1#Zb3cvQF0vhNz8+Myq7cktKn%o@UZkR=mUd|I)TCI*fN-H6bRh*No%j&(84C>#(scc;`0?>aW(jWNWHQXEwh||) zh$9!|QH<+GfThF?$9XpskA68aChR7XvZjQlklG(=rPZl%QVY7DlSrmnV>Vw4bh(`* z2_Wa~sE%#;DM@|nn2@Q2wE;E<`2MvGU?a^bNb3q42sXB0+1pg3bjukIYXB26S{pR= zYL+h9i(Ly9914*a!b{4T>j&$|HmxoL zjA$WXH8y#^MkX{s)0Bh?KXIl1_<5mvYj!2g_Z(;ujWp?RC|B>5M3R44tWh|spr>K( zuPS8LRYW1Edx3o?=#+IFki)gdNf0U+?)FWP@}v`8Ux( zl?Foq{(kS>m~9pUfo%${h0^O>#bU^k4`E5ElujG5qq}I9A1|13theW;tgEkz9#elt zx5*4Vi$rGf{G9WGg~vQ$lvrRNU-&D$uOi>blSBXG)I~nD>r94*EcKbEi)9tvSI@7v zbfV3iClLZCs-Dj3@nQRQTnp^ME}L* z%RcFfGracwfM3l)pML-U4Z>IOw~dmeYT%4s(f5-^*ZLxcvyY1=VJY@AFTZKb{5-{T z1K?*8ln0bwEwh9{;t-Be4x*w+eA~%pvZz~>$up&-AIJOZTtVDj0$eJRK0arQ2{XS& zj)F|iss%jlGa*?_-YxT^vNF7d0qi5gb<|5=5vB8$9s}AZ+Kh(u)E&ds6)}okApH=* z?oWCNPZ#+c_f7BLre@W2eQZAd>6ye$4@vD8IoZ3Gk+nv^6ze(6hd`=r(*QbvW+DiW z9Azia)R!Yh8wLTgRe8ZGnyl>Xj0?q3eRle(3UOXpug^K=06Nog-=~i42{xc$3~Oud zj(CPeP$pOoHfSMePCrY^cnp#uGi1G8E{1Ez0+;=Q+CyYOwUop7*I zkW4%a)Hkh1<|!^oE;>$rL&259lveEV2#Ykk0`RaO7k@)V2Hbuvq}5Ghec!)R@t0Yv zb^rfP`8(Ym(zg8|jd$MUJ?HNab_%Xex<- z(~Uu5Nd{qN0C^-yZ&$$C6kH)jO5HhY>6J<+;&*6FU11EZps;f>5S!)(_CY{c_!3e? zPoe;x2~A=XVk|r)hrM!ow_ z!bu&4_P_b}S!l$;Y@e-c$$XPzbGjqSq>Kw*J7bH86iT(wmLJCv1qB2n$lK( z*1ZIJ9eeem4SQ{7fnXj>%a=ZWot42g;>^41$qT^^x^Hg)n&v*q)gRE?-#%bq*C$i3 zJg)EFdA~s;2M}_2NmvNXRt(0*gwS8hCNvnt!nd3T!d;3+$Ah7ivd&g zl4hFGpzj@1ESnA~=&9%_gxFnhM^M^!24afzMsMpYBoXil#y*5qvN)d6otK!rKQdE7eW#R1p zakV-nVcg(90E8DgU&Jp+%8@#cu?o5w9q4GD35k7D|5P<5Qwm7>&_=dZ zc;Ahh|Mi~n`W^YWo@0USJU!E0ZDuKoOryV3q=*^xU#4{W_Cm&u>s?J{>j=s*UGD5} zT;XVBAAaORtutK!HTfP5HhlU-RwMr&#c)BddS=DOXbm$-LwrwS)+#On9`C^Hb!|>; zxEY~VDNIqnX;{d*yh*mK9AVDmG)JUttwnK`{6q@0G9}6EX@`^w|;;AuutR z=HEp#F!2rhy-|>!PcjHpJ=}fyDKO;XDO`!{Y2l(TomhCZv%Y2zf99@j*}wsdt|WTP z^b`<4&8uN|t_r&G9$IgXo(QV+dH;t7JxeIE&Eh)8i#yG^SE#RhdBlDXz{8tsk@p2I z>l5z7VZS&M=wUW-a-nZ@l>QkP&*&n#}%?lP_xH+47Z|KZKOru*0cTw)jx z5tv0^dCS#^?7xaSpVAA>lu_0%nQY8A(@0M19Q;Ud<9H|({+in=FTO_yVL)~qo>m%m z=*teTp%|C1v$jc2_CJa>4^v9My|2;QAjyx{Dg@%FUATntl*_l-$sbw^#%R`FTeTEL~c*Fc<1F`RC) zdPl?oQ-zy7iwg|csk6{zjS~Z|PE>%=9q@oTgkJrx;&cMji!%=}?%^hA#X^8Y; zf_wN=fv|7=Ru6A&;(^pX<%%wF{|8#$cHg{`sx?=eG$Q0}b<1GX*IEbw{lW{|M3%8F zqZ)NXYT9KK%VW&XmR>3FX$-A5uam9py@^Ai^1VmZvA`M0m}WKaZMaou@z>hUxg_1M znuOoUQDqV#m6g;$F`TBf8c{bkJ|NYa*7LiOR!?DT5sI>@Bct?q7!EKBqL38lHVC}c z7+g_4#k|6cC_mNE=v1_;!jh^#yhDf_|Mljo(5yvrsj*o@&k;p{0cNVhHr%kx0VvNC zdp=!;7U!Eu>@bwg)euD7SrY9}RWWuWF&c$N|ISQf`*Icz<&N+b6p6TL^SB*(d}(Ng z3e}<>n?NXmLbVwP?N-BLRE%TlpGH@|N07%Pyz<@fWIArukb1aSH%qoD)J1&&5g}F= z$#B^@#t2Shgdny~kL$OlG2$?0)*Xtd>NxK5K*SyZ9{xPll+**2O-LP}n6#Xr9VqHiZb}9R`E+flw zVVbs+Has*3JK^4&H(5v7y(4?1S53l+k4Xoj_QPwz?XH+C)KW>2icD9JZVXvfjgBPK z8%`HlJ#x0O=nW+< z9`==~*{+*uEr=s~&bym=bK8g>!sg!Mfs^OKHklz%CL~xvS(nkH4s->*L2w*>d1|8> zo$|%YhN^E^OXotKh_VS94Q3k_4R{S%sCl$y4jdTmQ6I@BPtO2P&SCiKX>O*a@n}A8 zJQHU1zAJaGRzmDQ0E@;{>a%Aslhw*m85o5?VSk2S;vFQ`bXmcP+r?9sIIq__N^G}T-o${Ah;X~rrTMvFMw6f2l14iT z<^dwL_=GN>O~bDxp7&3?K2nbtq;Mg0X{xn0Y27328$Uqxqu z|NRHxK`q^>h|aLmL0#^OSHx$~r?t=TvcE`&Wc;RkZd=GE7On1-o6|J)#3) zC&pBWh5Q3)6oTvJ%P}@YT1l@7%N@m)ocL=)0GSib_*ru1_psOH#zjgr-+orL+)=dY z*;?KyyYv>pr%;YTwJ$%QXhiWoxuP{h7sPkYTFWE~NY7u(jJk?dmg+-(+XZ3#@OKt+ z;~bICRu4-WUhQY=Wpk;J7U{(8gj*$<p&hM8!%+`~38_;e&NFzRWBGc&I4|o-Yhf zHU;LXAWF{wdgfmx)Gq8}y12-Z4<^yL|U!osC;`-LI%}kHhotDGT_u{SL zK-&pjlA&^m{FimF@4NNzoEMRwSp!t|g*z#rD}rH^Q@P%^%MVBM z>KSH|>m19I9ZRBrEFELCjhby0w@V!4|30IIY5_pj3roqf_tX24OycL~XjdsO|GyXk zZKJc&HWJPs5gtx+n|CBhuz#U6E`}xAAwG&4ZXselDA!&d7b27ckp7I?y{o9w&l(nw zua@A?`^at7kE`H(_QfBLSRzdZzEz!uo?IXNL=3XXoZUO`U2*Q*cCKf&$bNb~g>C-)T-Buz0hXH`0!A6$Ul*)Dfoda(D`8zvUGQb(* zgugAD0Lb{U^`$?bOjklmD?sZbblkNxBccXm# zn|?${1hm&5sCxMyl?M5HGp{eS?7jHmZy+xAb2H!!SJ|b(fo?y8GT0bOefFlE0)HPT zZ3K9R90*M$b47wBT8hoI5l-sUK&gbWfAp$T&81|;|y7(wJ(j=NE@nyO#dA2Jl1rUpy~llo)g-V?G4vOYYyClN1W6@Ad?WKAkHd z9}ku8LuOiLt%dp}jT9AV+>B70NNEafUFwYtKcNWic2|-GNctl8h z8Sasq&ZW&P3v-uFKDLiD<>$pRqU%ro%F#RwNYefB101yHWfD6{5s0j(UL>IiQ{{Rt z_2u{BfF?MT(6vG-P!RMYqHa&A7szaSwKcBu+CwCd6@)395hZ$#S$5Dei;yq<+@%SU zdrM*WOe|r~X35vxPB`fN?#`O4GNOwX!Wg$E_j#Zo4@xOK?ZDlpl|X|AwQ96DlCA*& z1J$}3AgBUC?G6Be!Q2jIMn!2k(F&O?Aa=^n5GAC^gKv@RD@n8>g}t#l_v|r70+-ScQNo6b+$kP_3ZBqz?<$H+%8du@V>)#^WJzz2q$c3mWb1IgOaXb zf|z&+mt^{~*~kOJxs=z_5FyTd&;t5`rt3zd)nLgpricX~=FrIE_OH!D^HP$(f{bWF z_xkd-!D%_`gZg^=a{ti#-9rYB33V3nL=}5=4JUl;J=clkWASCn1C8A$Lx))vFlJwM zB}NnH_3S_Q_~V2ONzIjiAsv4>qj>Wp3+F6@d*eHvx`iGe3eEq{c{7~z-AnmUHw7XH z(4L~L9&Ufxb+8!xLe%R=Zgs5oNR0jNwyB2RP!!meg0Lmj=`X;iOxh`PTi^dI+=3pp z)l;F%0*GFA)6@vwIUZ#E55T9k>tI^KO_X`H5zwVb_cQ~vGk1;o#i@CR1ddM1!Y<|&KwTP6Bo-Jv|4Uh+*r43CWmgX4FK%FW^BSy-dL-pqiX ziU&i^3I(E_hh>dXL(Cv>zPtG^5-d!S^J%h23WyeKT*eQ7=F1@u$*(jp0RADhW(5o( z?*ClvzUvk%cSA2;CW|afNhyF@7-$YZuItYTL#pjqA2~+-FGQ~4rr@Q!t;q_F5@Y^7 z6K#-#Atwi|+h%U!G?2^VYecrlRz5`^eR%t9}?%L@d?6kxJL4g_UiFe&Qfv~{Csff{f1!q%N@>F_q37gL6vtx^_j9H z)z*EBD5797FzyZ7+9iuMBbg{kQ-ZjXdM->|^|Y6d;q!LQ9nNQqs*(c4hwIzzJ{4-WjS$T_8tC- z8wF)rbj0XUb+m!oc!>*%oF%o;;1|w>G?<@PV{;O%y+bODuh_Rqs={L)7gDM#-S*B% z4&2qf*>KRR6V)Dbjgyx^3T*{V?@7uA;|}arK0T|ozBwrg7NGiRp0p_7FnQV}2)X5v znm;HfDJNXZbJa0FF*Bq?9Y~kj-EhhcrFKjbyb!guwq>o@qjIfm0MpRKn;Cfd2h}t4L3yxy?kVQJ;%NA zB6Aqd<@aM&g7zY4HJRNeqMLv~wh_fYo8ktzF9Xt$g?eT8yXjzS^EcH}P!IojiXv6Y z5{ov;ltsZX&t{#S-{oXV+Ps)3mbc$~O0k+6$-CfXe(PB&G&+PZ%@GRUax}Z4g zlPKRjQj%J|h&_>dTUhr$KpdXXKLg<}dzB~WmnFg+=zh7I%a*exQ`0tRx{S)@*?6LC zsNbZ}E4#Gz^=YCQ@JQhC~F@ zlRh5{89AJJzUU7)Q__=|PUL3M8L(~LB3%6f>^#mBBic^BSfaGK!J@_1q_=ife<071 zIo_h#=HwoGvNguVu{sNAC)seue&iyaW{7&9)m-5h&G$q3mH`RQQLdwvBu3CkL*cW> zH^`Pv_j?H>MwB{rze5mV(A$iPNzB|E*v83y?_MeCRizgv3q2O3CEkW40Pvr7$q7`l z0GR-$JBHLHW819KiM=Lt4Q6sQFbQl3zWmPlvy;Qb?c@4u94AQ@6aY8#haK1Kw>^=6 z-Prx_yAMfSl?$}mwSQAg4Eq)4XeAx?z&M$zhnK3q`Lo^SrrHyVxU`Fs|6zW&#NZT& z5;kI3Y^nZ0`&LCR=?L2IM3)1#PQW}ol8Jhv(a^U-dbN6h&m#^j7;uT&nL8uyG;j5x%71VO?tPd}%NTX%bNS6@gNpg} zdSx%tZGWrnS$}<^;gh`8Ep@wsg&Rhn?_HgPWO9|GE;7;7`@J10kArll<%l;N^J1fn zO+GCN8wT|Z4lBcT%`J3arhvrgsxoVFZ?WWp+=d*FTeiWujMdjmaIKR2Pn|q9&VEqoOUE2U!_x^OIYHjWV zJN*Kry{o3-&;Yt0ah#AIup;^4434X>Ar#+@?BkS|{{6e) zfY#SBP&fQgPF=|=8yyv{F`mPX)`7ITJ$lnhvGJI?cdsuqDwF%?9D;m=CyRB=Ndta2 zK_`P@?$<9iq)5SL^y;$yY!^ zzLF?x&oCZ)8qav5h44AFWzwmnF;pf)`bU4D9QzZo92GEOSjgwO2m8@MRS_f$5Q*Uc zSt;r2A<$s69vN6=qk&B4<;{8>`LN8bo*rhD>FHf`~P7l!1(4}hC7qZm+K0#2AJ6;SM4 zvlT`B>4m!6CFD3o`M%Z{2SdLo?C7k(VsIU{Ndlf6=JUazXlob<#N4)!IdGe5<3{VF zSID}pNv_|q|DLAK6^syeHfSF1oC&{M!4y!|hc~Yr|~UJWnLJTEZXcT$0SRtt1RAkXsy(XxuwFVmA2*R`AKj_ zsbF$(kq0;bm3sk(rt`C_2lw;XgL7fAC_c<%63XsT_JOo}XHXI8i6g_#4-g{{Z%EL4xH34Jvl#{7k;K=9VP5 zryH{l`H#2Ux5r3a4Qg?O6guYDNEF$);;-5nCiWd<)$q&N6?>#;Hz#tfT;5BDZy_fp zhV1QSpe?f#7e(X__C|;M?z&?fQP6n|ltts&a#;hVOb>3+aQ{D!&cmP0{%ylatcVrF zUWvV{YF9O4MXboM)vi5DDQ(59P0ZG6j3AVnEn1~^b=mr;YOU5Nk18#yo;SbuKS=J+ zojce4y^ixd3}B4%dlo#q*FO(T)qPnXi9M5-egNlYl5*$BboCde-*Z=5@mv&55IsP4oRNbi0jI4z^qM2^{;aGRJz|`joOvToX4G@X^X^r2; zJR{$YXj_JB&j#W06Vlmt{1r_SI*CIZXMXyq#N@x!Q*QoPQ!@NXSOrFGQ3(Ux8 z!Ejt2X1gcxa0}_^B@Hrf5}`7Pk_yf;d8`}kC$jY;&?)>{k*+-O3Qkz5$$ z+){5EQKLeX>30I#s>4P;rR}U)w)(vx*(P;&F}b%rFupU#`!3BQUC12Zoc|=Cx|)?@ z3fj1;*C4PbZrl(k#@|o@e^33jW2pcS0=)vu=GmEt2{~-1pH2fOtoM@XHg3?=IDrN{ z46DvUlgD$CzZbS%SWf?OqvZ)TIrlsGX()Q~UsI-3L*CPWZ6H(eZIe9^&f?`pW2e>$ z{@<#6_+~=-?1N-mPtn4F$N6({^A_IhR(4QF^(4tOKG0G9fs>h*52wx0H+x-S9!2z7 zG)usvD;U6{-|19pE6`BFPjM}fzKSr`Lqu`};#!wdV7jwq^##IZe6-z}wGDc_0sjmO z666StE#q=Yl;>=jUR8APDs*DrmUZhbnKk-1$Vp%fVbU(e0Hy@K?S-Pp8H7K)Vz}yE?B0s3243A6Sr;2}xlT znZhajS*u~7ilo$_Y)Ee}<~)Dmc5xP>Hf@}gEtdU$2QAkHg@ewoA9|<&B~vDH(mK~= z?(@K^13o7=4$iTYZp2qLCDR_byWahZG^HwC^9+dV&lB^SG1YvOZgWG2KCvcuA2vKs zE$v%uCr(pBW)C(D(7r2$3OcCVYFeH+neWd@UW%Y*xl9w6iUsq0LiMi`6 zAZ#)s@Fq36&a%boZSGotfuT#j>e@&^tv*gDBa?0k{c){x6s!w>-Pyjx(TvagAr!&8 zx~Jd((v{{YayX*ZZGHtb9rEM9S(wT?T5?;YSQRJbT~s%D-NV)=W!yG_F&%k{i8o2B zdH8~677xtG@A42IU# z5&x{lk>&sG?oaV`Z?4qVv~GARl40)mWupo7LiiSzozqB`hPPI}#XP0@bidZ)z3%i# zajn3sQs&)9crS=y_@FD@qbY6Lr?oS-6Xr zKj_p5K;GvhCqnq`n7a6|^5J5!$MG567zQRofQ3O zMg;WetTPMv(!nRE7;l&1f(W}$`a9-E_xOz|^i`4347h~%*)q*R2iF`%%oVFCC;PF=-~z#uOR?}IF%@~7$p0#&)>de-#RcJL+eLegGB0J;Ai?gpBK{z;N){Qt{ zSJoDGupTp<(ssci@`e3>fP*)rd@1)DB~c2<43v=+{w^)hj8JafLQd&%^)X)2{zRMZB0Jn0a} z2>`He7_OuZoB)yB2A5+dxfKB7dGy*-P_^dfbMPqkU*UJ+&P#VbyqVBh{&UU|5w+9Yv-0id*TULu%g&<3 zol`qp*Pvs&WZp-y)Vasfc;&G$BtcRFGL2Hn_bA>N3y+;PPLUjDZ;_=V`lNcgO zn!YrdeXRO)Q5u^8RIMR2S6yLY;AKh|&zlF|l z@6=#rY4${QI|CJLLcyB3yZ`ZD?K}jQ;uK$|)zXB{va%&#n{3L=*a$g**mOF{H)g5c}77ndlfC^kggnPNb-hfFD*Uir~i+OfHQb&R1) zVD0hhTsGb}Rght=M}o!abxlw>C}9SvKi^*C1kLjB(GyT{nB0N^YDi_baFQpoNVo{% z^90Gr010HQBk76x(~2f0G9#y)em5p0ykAdN%||7*EE8`JtDWe|=(+?Y(_2C3)K=-% z6M*P)3rp><7YHbh56js4S8?4c_5~+!iwrb*>8)T=yS(E5$9`}7YreCfJBW!0;KoyK z)bL%+j>W0>yq%=b38qs@s#kZM#1;K*KB1+gmu0b&@a|FcS9S3=y)!xT9N%t72 zF35-h<2Z5=uYCPb6%?vy3|~5PRW>7903vd z4`9jsn=L?1{iNp#Pbj|c<3t_xTi;NjPxVpjv7J5D)WCBu{-UiN_`*y;L7FVN(Vr12 z4?cq!N|I~2vh_iOiUA2(VgCX4+asMKxYLYjK*!L4M+~MiB-d(iy`=vmtk{}SZR#LR z<%K8&08%QoD-YgZz4y%dm7t2x^qMoPCwm1*V!o%>#Tp;k`(pml+p-C~5M23+fvW*Q z)ct2!H^SDhOeRmpO3M_5@098Z1caVoL3vSTToN39fL9hgW%;Up$`C&WDElk!;3`PHIRbwS1Kn~mi8CELkx{fq35&ErGBc0ck7apkZHeurIKfN=j-drOGl;u0L&4~t=qlFnQzho zAcZi_@$;yA*6(%rr=+`5nnK{vQ4%Sr#20;bnvZy2y;A%2f##j3d0ue+@h#(w zqVIoe)alF{Xixb_G1o|lm1OJX&iPCj88h_kIGG*Xakd@AT{AK`&?+%3{%qEvSwfau zh&w!t`9DB~vI^G|T)xOd1ls?dHrpR|?3I`M9C-gbSLB|=1%;!QC5jpiqQoNSa?~YR zkL@~&#c)fs4XBi;Iga!X9@Cv4aZeSl3@tGsG%+8S2a(6+3Zr=&7Vig#8}f)fyM4QH zT{byf%AfY539~BIgA8s)LUlVNih%AAMC;-qBgtg9ZJKm{IN5a*g646p_G5~mo5Xy* zoqxpIrTTVnoOeAiX|+h|gEk2Z>YhC>K7Sr~;FR;sf9&e%(%PTfI>$5r7Pr$><`b zpu3YmZ9RP6gR^CuBV(0!DaDmg!SIdsSm`+U-AJY45{aa-QN|Z*bSbE_?Y$;nvD&=X z%?i8}^lxJdgxmhWiLkZ`PbBG1@*UepOnb+Z;nf}Z)V(kHa;DR>j2GP!80x>F$xGv+ zk0(f5ikAX@QH`?t3{AdYm?ot>pJUD#IN!CDl?#N1E;xbEJXq$M(RZL3ImNLkcFko{ z&f20Smi}50JsCxj>W(QB)1)Z2AvBfm-PvM^W@5}X3$toAgclViI`AO-_T(WG#!th5 zgQXzj0m;9OJ2!eLiVgU^j}eDM>7`ZR6yqS~oo`4(=|oD^sThq>NG}->@bs@Aq@l;N z@wR>g&NPGXgH&CmMm7;3DKGCF7G2A~hmhf84PJ1*ODN)3S46#d6L~W2lEW<3kWCYQ zDP{q)>*i>da=_SuF%m^ne)>qn;E_cBWT2!AeAD?R2IOi>?6bNN%i-R8=}OTMg5p`f zi~ray{QX5FJ0U;O7+@3=lA0};NJX_0{;{Zc(r9y3&MZ<43N}rN%gl3LWquz3T6L|u zj6)cpvJsM=Ms#St5sxrzQ-tZL@{T@W%Q0)Pk_Tnw%CSLT)o*t~+sc!X3t_^QvRN{c z$(@8otdVhvowxHfnug&WZa$6RUaAg1H_IX6pb;Sr<3Rl*q>v)x(@Ixkbiu2jHx@_<# z!X&I7Xol)NQn9A}xXe<25Zf)CvteBy8xz~g_^aUSZ0ix6xkFdX-(!2cueS+!(q*mW z%Keo=&TX-71k)ztm}VlkKS;$uY0~jY{&HhVKO#@9j;BTwJUHU3~;XKK(J*k5Q9rgA{DfZ_wL))Rf?Kcs?`!Dla}k ztmSU;u`MD1snA|FI#ypnNX|v4ON<^E!v?$`zu{}vO)+xFRMTR3$=^vU)G9=ny!oQh~@1X z<^mQLs(AvergwLZkc=zI1B4GT67z{{a*RsQ7a_JOgw58@R!Kw@B+Fu3;jio%Dk7oe z?AJdur;9bi-_U5|hh5`~Z~poRgsLpdb?4vRV6hEo_VEdH|4L)n!(pAsV)x|5lWZ48 zi*&B*r-XaqV%tjPKk1d028?%4|Nf_qNW6C}?II5TK zjCmvjZy<0zXqGh2g+?=)zUUWILe2T?o)<}st)D_5C9iisNrc~6=^|7OWu#RRboS|T z=3$F8(Y)AA6HFIFhF0HhfLg#<)@7v4o0d-TI(;c#?;iUuVc+9&I2e6Y6`>gxhT;II zYA+|&(Ft02ecX8G7hzF&$T}=Aqnf6#cK%S$*!coPhF?AiIwkLWppy)8RQ_b7NuWBG zx9I3O{aA>t_Bc2mP$+plGDyS_a|;-skxaT<8lv&ocWU)NT5Xp!cLp;O&jtAKgr-5vH_0p9P7``S-lZ zFzy?k)+6Vd?{5jzz30txpyJQR*si-nHL z4;=E090gRxgwQgS#HUNT3}!PuEIJ6AD(Beh+KgFwMX7-nC)PkyRm-IS=|CV-?{#}dT14-^ zT`uWY23H@fbLTFxls3Cuq2v4mnuc0T|Jny=ZXWB0iGm1$RZF~!K&P$Q&=q?K$vuZA<7#`e`Boa6 zG^!C{e3^a_^!a|Kg*OQj#BjlB5h1t&I~;-4CW<5xP-MYZbl>`18XvMmG6Jxrq)L(y zX1(|R9gxgCk06@(`i(u@@R9z6^y-M3ZzCkXP66T$6>v=db}4b-aRD{c;8UzIqoo-dX1EO#Jp_{_2@ehGDur6Eim3W?=SUZu5`;4}%AQ}nUQSp~ zXG*i_ZMpGim!4wxytoI>{rYG6d81vbxH=n@EB|iSJ=?HZDfw*G3acBD(@Mq0zg?DM zLLJL47dli)bC_<;IB_Jr+vO=VGW|Qai?=GtJGD5C0z}mU%aK-+(Kl8s8G~F6vA-$I zDbZK@zdlg(+QI2R_a~oUWaSx9y!+Pmn%~yr5yhM`zpdEsD@8#+-iP(QU~4{Vc_Db{ zPvHCHZP6U?R=Ss8m6_B|1w4Pn$jxsNdNRrSjeflfrh_&vpfBB0`e4D88xTdcIlI)} zAlEw}ZQOgbWsBigkGMBRo1zK-fWP*{kb=Fdut7*TymB&>>N^)ae36x8=v-{2&G&xp z=E=d<8)!z&@#fz^;q9+p`HK06B-_}4!W=!1g0Q66u+2+2RzM!NvOxV{LsFrbdAC@- zDP>>zJ{g8kOgg(p_AD3%1iMCWS!o)jHNX!IO{D~QEWV6)a*lYF%ni2|Z z8Ur0@4$b4KTkJX4udQ=`IM%clFt)Th)VMSf=PFPCyOPc$V;Zlxbw;oj0)b2=rhnx} z%vx4`_7atHSG6UqizGYMpdZ;Hw+uC7JRsyLmW^+=M2?Ly1+T1>m&y%yHDLu~tI^|t z(<)l$6|N_LZl&S*B3TY-=QWKBS&W9q+~-Qyyx8zM<5le+#|E2tKpazp4lz!KFWAKwg5h%l=6E zux`5veBEbsORNFnW7xbjN#A1U$@oqUn6ZiDKNSsxF{x<06I|iBn4HEjUCd^;CntF2 z)q)!YTyjZ6syctL#Yl=S+`eW0O0Av9EuY?JK5($6Hl9JcClvr_-H=`bSWT8blF}~_ zq65d*D6i6?aU!F&YpV%p6D%uOd%tC)8-xPfF!f^&}6UISk(Gdfm_okvLw zn+&z(jP7C;2U|F!VrQ)QWx$*oQxdd#(0Pe4L);w7G4jRhYCHsE%cysEmUJE2ihZI% ziin^ZGEA33{ea_{&YjKYkt>SV&GGIoVkYWlzXV6Xih_|*^l96;owSirb5CQxfIWqf zrz+qbOXD@oF)`+fk~T{ZIAuk#e7OtT@6>-FBUu`{D*m_< z@8=P1`r|3Rd~loIQkLfYu95@YGh@~#sHxD)lfb)T%5hCvYkRl?zp;kT2;*m@nSfOl!*omy;7rCf9MU2; zTc~=34-I>>(oDh5-|!yWeF@Bs+pz=>%5XKQ+RcAkvxdXb3z@X{A!Y~f&OV5ko&XU( zprQDFqwqdKo-{)X^(CqxOf7;aow=?_w_#oEU}nNLWh;0DSh^DJzZSYr)1A6LX0YO+RtX3V z#XqyFUBk|I>GVVzLZj>XQ7!Bhh(K{WVFcc6sE2*eZ-m8Xo9|>uDgu^?e6~bWq!uuB zPA`0*O>61g#`IpaF}huQQ4gQ|+`J>Z@H`Umz7Y{MyLia$LUBoI6QdU{p^eiq6I}T_ z$g3lj=)9n)JFHz=bmaPZ(xUQCNVAm+yHH|dy+yM~|6o*-oK))sQ2-XR&hH)#PXw@_kr{eq)QM^z-i+vlWS*B|2%0OPcSD(~9wS81! zh6zzSz^Udega^0~AW3J$Q<+B80z@xUTtm`L^Y>R9+~2&2^w)F2>4!F}IcxK;T=2p$ zM_lThza{sw?9jA&4q?@WJ`V+Zo~63q9`>{WtQ*I0&KNfL*>FORl4PSoFt zmrx4bk)RV!`|tc)0kx6)PKVwF$Af%%c^v`$+5SWTSlzSHp{JpPsAQB&GKXDGjkCXc zTVlpOGZCI@c%$838~af|2tWdJv@Fw`E^wX4AEw?Ge=L2osP7LL+CciD;G{y@m?URn z9)qg8e!QLv``KBs*9zWW{X}#avQF4qlPY-&HZOjN$(;Hr=<+!t^Yn3zv+<+2t=e4e zJ@M^~d^|-yuwZ8Hv1s7?h@J=Puj0DoI-gbaE{HvOLar#06#p{*#o4$TX(PRNHO=bf zzLL{8>&C=1x#WA-Q#Jm#g@qCmr++z%9;?-RLdu`dt+k#0&D6x*#<}PtW=N=zto>XUO?Iij^JX zQc~hGYWoi$*|xb6=rJJ_!i|39H^Xo_ZM+7qRm*crWgDD3DWeGSc`E>K*WLd}HyytI z8lg)2Slh+?n1Su#D<&fA)#3gGK756Rs*kyh52CIi(4Xh3gmAK*F!0%Y`M{@N@8qR< zhEtc~OCMRqhHyav5Cp)_oNC@bG{DCm>X)}gk)>#qsOjrDI7z?q}A(;}Df zsjABEu4LySs{b1VyJwwotFJ3PNip7%z(Hc|b5p1qU#fDw-LJP$$WVN@?1N)aC0{?7 z8iB~qb!cm@mM61{oH$_w;7^03>*JycYG0?v;znJ@w`xfsH2um1q~@?; zzO*4qs`0!KP$#umyS#N&52Bza;8K!N8sq=MQh-?ApZtAMx1{R&q0G_+OL6w$^s<|+ zj#1o!C+DvpEiZ&_2BV6Dq5jizP1>+o|EzNpETBTA z%f%TzMfRjG(pN8j^C6yhj5jT4SDi``!A9S0aC%$jwzuyz03xWpxW=z+Stp7tFFq^2 zt~V9a))jV>3B`z{l{vp*1zYuJRO#`M~7r8|oA3cOmiivlnIjmkX zyAx@^XKWy@>@I9j^JQ(c5*z}>ay}I&Mli6zsP)-)(znC%SXT&44EVVE4QpgR(+gJx z7VZFsWUHj3T5kDvksTx#LXuO<37`uxfsms5&?J?n2o1%S_x}TYAM1RS{$fr`^mf=l(z?xi%y&OBAOmzyfNo}L%X|Ih zA%r1|P$q8!kO%L+NbZ($W11!<4m6icB!;D#+J5w5*TeE4XD{b*Y-$VE@Sij&SmsG? z9J#xkV9~qX;(1yWvi$4L=F!r;s1I1Er@_BZ&TA+LA$c8II>NO@{{j*vB@ehG50;b` zV%TD*xy#F792^B1s6YtuyadLmz`UO9Ly?-zGDMBX>w((sIhiI{wmE?2=p9MwX-hh$ zoM?_9h)p~;LeKY-;u2>4;$t}^ExFB|&(krb6JOYjDGWxZI2Q-_IA|QYMN?>&fn$`R z+&-}w%erNIxw=GhZDTb}BI1Y|P~A7;&BymdQ;$cMRsQZOa+7;L$w@t(6N~1`ITRi> z>ny^75^vhfldAGXcoDN#g!y<2_@brN3%lriyhD{3v=5+bm6d*20qjo!;>pAJYcoG^ z--P)MqtHOb(ZzbFSUWBuG@ya;0eu!JN4Ac$@+*XK<4yJ(VIFGS4Bhrjy=?f|EOi$> z=CCBHnt?71@^*X9zb-S4(stCF|3oQ(M_*=3(e3NkM(7kV&cu)|h-R>a$i3)|&xqwy z7o(aht}_SgH%zmS`_?>850A4qU_oK{eDD`Z6 zMKM(`KSO52N|UjzpV&(rFEKUdrf?cI<~5KXu-@o$No)FDaJmUtZZVBg1ft9qw(KRb zsc%22{QI>C$AfxOVQ)c1wQyo{FTN;zHNZ13XB9gm-EiUN@}c`&$ttv}V|paO^Sd?> zJfC&NN%gkX+wCAi(?`%N26P#l!hbcSU)TYrhJ3XY zB6g~x46kP+F}Mv(zob}R=Cb=XK=|cf`_i5{pASjrLCPC$ozTls2Bku+PdSSs zpo!ottvgT$;&@RNe=q~@)O-Ug040*2VKbUy2nXH^Q?~mf3@d=)T(KvMMY;l6%j1By z_UT4gtHQ!FDt!IOkrXd>4r8n?-ZTMR8`L+C{5UWJGKOL_LhK*AZq>D9>D~FHH!b6^ z{Yl%zDep^ier(hMWzJk-Ht}^;t(bmx3|aUDN7T#ye&BsiF96@y$N=423x9 z#>aDaiOf}u=npZUn}J*^{EK~!RIsG^l9t!g9m|T3vcgRPXI(*!q7AFnk_>E>Lk51k;< zYbX#ob5c;F3k9=86Fq|}%{Uys?4VYFZE@qdCX7XJ2hqJxYh@0P#!MJmuTiM?S_Cn< z7Db$g?o$`tKOnT4HnhsdMh_dg=Jb`&@3M3nS>avd$ITAL-Mt|b*A55bk6LVR3dc-> z-j3{HQCyd>G>i9Xu*{~qCMsL{*f6SA0XUj@t{`HRJp=|u4By(6*D4lQ{FKw5fXZ20 zrR&s@+MEj7kaT49Ur!C=EmP7)g1~gy%%q#PD5zF;mfj?o!|R^n+zf5vEgK`ZI)*5o z)Yfx-^aB2!0fv5bJ_+x-N3}1slNpyh`uu*4`KBL}b#9Fqf9JG9KI4<8PG#1BiV{!a z;R!ZamD2=HN_!m&?W&Od4I3WjM+jB+TtST1i-98B7p__%-uRw=5X{7>|w)5DSRpJ)F8 z=zh4~ZS1XutJj)+FX+DiElDtLh*qWH4Jl@ZSmrWHc=}mSZzvu)8^B8U+Qw}U<+L`OBZ>8^P zqpn?!s)xOMZ!q2RQMT~ygG!$(=zP1IJ6{x--4wlz5hzH~lUHk~2l|OEP|z^j@oXda zI3F0**zjv|GGr*Pk;jtsX}XB?uY|fXNc<}*sJd;N*FLj6jZOi{JP#DF>be*~LjS5c z^)$J+DxK3=`?Ry;|xAM2Ftl*R`_pqso;|tlHV|i$mUSk+ygFk_$Mf>PjQ9O zM;hz0#5{jge^En$7g{q$RG`qk3e4gPxoRwA|`USw`KTS1yo1bz}t zhv8K1&}n(}8o1aU>x{wQ?1L8-3H{j9SzeYRbzf;E_-8KRj0Pvn+h03tc!KEEIZLw? z#87WG#k|(8){W zN^DEa?6u0Vgs~juJ9&v}az9(%>6~OW2du1Rs(i0-R6?72jmo6OL9V;63X@!lmph(U zCglTSy8zr#2Yh26tu3xk2(-nE#4NDX+{Jt?h!-sIwSUC_=Bp*}H=J#|e|)x#<-YGW zc$L19S22LmOoL{+j6_rmvlJR`@g3HmXbjw?q(GTjtLVdJGDCHj4oq$w25~h3{1(~6 zyASh;2fN-4It>h)g%$%ZM~n0QGbCQ-5}ElATLr}4txPbX~z!nR%yew`)b3SRR&e&&A@}@PYhd z>F-^dw=?-X*Y%<#Ft4B5+^=}I{q&j4jmUpjJS0haWy25aQ;ZCyv{_ITwyuj!#+;ML zJ4aT*mNOrvchj#kBmb%JFUwL@E|wVzwv<&P9DyGC$6wBXWQwYjAtQnU5wq*&FSL1k zEg_Pby;055W>)OCK~i(5DpiX>%GDO7cT(x zXhk6X^D8;&>!*g9IDFnObl|*6i65NNl#?ciM@}Ru51^T1LE&Fz&F`ESX>dy8=4<)L zS64}#j&R7m!6I-TGizhu&syN_;Xl7=Lj;CJROF5gLxQQ)iY3zn=p&a_E4zf0{w=Gx zg}Fn@)PDe{2&E;SFO$F$VfFC`=D`8MIxMQ#kxXe%B-rT}mB}N1aeZ}6KR>6|tfkQ6 z^d`ry!??sR^(T%>uDygK;#ux}cIk2MNcVH5zz#0E^DbxTVv(V54Kro!umR-vP81vP z8biP8?$*zeu$@EIWpm zTVagFb_Tu^8GIK7Oqc{&e(qSdO=po1wb_d$o48fdO7?g+*9Mh=_#N9oVT)WQRV5(I zJvWN*rj#z59a&`F?zUS>1B85Lv<<3h9s%0I6e-6ncN)NOK#5Y=h(G`!qWu?%wfCIB zgkcENlvdqE+F8K}7w@jDVM-eb@^WtCx#-CkMxns{G3u*Tu3~zW~{i8)1514$7BYozpY{dp#T0OVZ z8w1f(fAhoZf3Q&#=!kFsG@4^BlevpUa$0>4M;KSwG3>=j2PsP}G-6?Pgb1K|C{rvC zDUKtrti6cc2pfR@axgdd=$4hAjozn*T)Ym19 ze(lq5;FM05d!;P=GSraK+t&Ma$7FzV`qgoc=BGu# zYsJ8YSRWi?=Hc~roRM4B)b?nOt z6g+u$!JgZmZ3$KwscqGS&S-ZiHW3pTY>{!N1f*oT3I$Y0jnDrFm}N+=vsT8&B-PUL zU4;@qdihYKK||mE%y1$dxcJW+*f^)fHa{g5D>-#^GXf<%`hldk+gki;Msaz4;1amKdv1|se zn2DX0?dgUEn=Qg8%4+KHXAN@-JayvhqH19(LqRZ~z5%Q?Ax~D(Xj7va#c3|$vBp1VHsFoqO6dE=O zV7q%aK{4nG&SCIFV}D7C?=3=u4!@ag_rjXPGO^_z2$z{_N!+sb>r;(Eaf_TSH-_U6 zIbD#@N|T2HkI7a40nB%q0GFw~wkdUCVOQI)OoLx0IXm6RHrX5)5+r>-EpzU3S@#

Nqn;$b+EA8mo8yXlwyJ_LAW52WENjx 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 M{>mQv)Y6at**L_8-T(jq literal 0 HcmV?d00001 From f443c51a67f347f5a66da3381d671d32fc33886d Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 5 Sep 2019 08:48:13 -0400 Subject: [PATCH 0103/1013] [3.0] Support only Laravel 5.8 and 6.0+ --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 781ab762c..3afb155f2 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "illuminate/database": "^5.8|^6.0" }, "require-dev": { - "orchestra/testbench": "^3.8", + "orchestra/testbench": "^3.8|^4.0", "phpunit/phpunit": "^8.0", "predis/predis": "^1.1" }, From 18acfcc514852f35f3f33ea3ec513bded6da38ba Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 5 Sep 2019 08:49:56 -0400 Subject: [PATCH 0104/1013] Update .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6cf586ba8..1dcba7c50 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,11 +11,11 @@ php: matrix: include: - - php: 7.2 - php: 7.2 env: COMPOSER_FLAGS="--prefer-lowest" allow_failures: - php: 7.4snapshot + - php: nightly before_script: - travis_retry composer self-update From 0f3edd14013d9ebe441d1e9dac42bfd6038fded6 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 5 Sep 2019 08:50:58 -0400 Subject: [PATCH 0105/1013] Update .travis.yml --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 1dcba7c50..4cba751ca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ php: - 7.2 - 7.3 - 7.4snapshot + - nightly matrix: include: From 648eed391372abe781dd3aa2f7e9391379b73e33 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 5 Sep 2019 11:04:05 -0400 Subject: [PATCH 0106/1013] Require Laravel 5.8 or newer --- src/PermissionServiceProvider.php | 4 +--- tests/RouteTest.php | 16 ---------------- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 5dbf0dec0..3bb901634 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -23,9 +23,7 @@ public function boot(PermissionRegistrar $permissionLoader, Filesystem $filesyst __DIR__.'/../database/migrations/create_permission_tables.php.stub' => $this->getMigrationFileName($filesystem), ], 'migrations'); - if (app()->version() >= '5.5') { - $this->registerMacroHelpers(); - } + $this->registerMacroHelpers(); } if ($this->app->runningInConsole()) { diff --git a/tests/RouteTest.php b/tests/RouteTest.php index 0bfbad339..833c46347 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -6,17 +6,6 @@ class RouteTest extends TestCase { - public function setUp(): void - { - parent::setUp(); - - if (! $this->isVersionAvailable()) { - $this->markTestSkipped( - 'This feature available for Laravel 5.5 and higher' - ); - } - } - /** @test */ public function test_role_function() { @@ -60,11 +49,6 @@ public function test_role_and_permission_function_together() ); } - protected function isVersionAvailable() - { - return app()->version() >= '5.5'; - } - protected function getLastRouteMiddlewareFromRouter($router) { return last($router->getRoutes()->get())->middleware(); From f8ac59b1254b28901f3ba578a074454457693516 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 5 Sep 2019 11:04:45 -0400 Subject: [PATCH 0107/1013] Tests --- tests/MiddlewareTest.php | 6 +++--- tests/User.php | 5 ----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php index a59ff6bcb..8b42df044 100644 --- a/tests/MiddlewareTest.php +++ b/tests/MiddlewareTest.php @@ -20,11 +20,11 @@ public function setUp(): void { parent::setUp(); - $this->roleMiddleware = new RoleMiddleware($this->app); + $this->roleMiddleware = new RoleMiddleware(); - $this->permissionMiddleware = new PermissionMiddleware($this->app); + $this->permissionMiddleware = new PermissionMiddleware(); - $this->roleOrPermissionMiddleware = new RoleOrPermissionMiddleware($this->app); + $this->roleOrPermissionMiddleware = new RoleOrPermissionMiddleware(); } /** @test */ diff --git a/tests/User.php b/tests/User.php index a2917aa19..e98ccb1fc 100644 --- a/tests/User.php +++ b/tests/User.php @@ -13,11 +13,6 @@ class User extends Model implements AuthorizableContract, AuthenticatableContrac { use HasRoles, Authorizable, Authenticatable; - /** - * The attributes that are mass assignable. - * - * @var array - */ protected $fillable = ['email']; public $timestamps = false; From c483e1787821a1d90df80b0c5d21de3daaa4e76d Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 5 Sep 2019 11:05:00 -0400 Subject: [PATCH 0108/1013] Add scope test --- tests/HasPermissionsTest.php | 8 ++++++++ tests/HasRolesTest.php | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index ef5e07758..cc0e5e024 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -175,6 +175,14 @@ public function it_throws_an_exception_when_calling_hasDirectPermission_with_nul $this->assertFalse($user->hasDirectPermission(null)); } + /** @test */ + public function it_throws_an_exception_when_trying_to_scope_a_non_existing_permission() + { + $this->expectException(PermissionDoesNotExist::class); + + User::permission('not defined permission')->get(); + } + /** @test */ public function it_throws_an_exception_when_trying_to_scope_a_permission_from_another_guard() { diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 71bd3b6a4..530a33406 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -393,6 +393,14 @@ public function it_throws_an_exception_when_trying_to_scope_a_role_from_another_ User::role($this->testAdminRole)->get(); } + /** @test */ + public function it_throws_an_exception_when_trying_to_scope_a_non_existing_role() + { + $this->expectException(RoleDoesNotExist::class); + + User::role('role not defined')->get(); + } + /** @test */ public function it_can_determine_that_a_user_has_one_of_the_given_roles() { From 7ec8f33a3e6820865a551ae953b31ac98203e83a Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 5 Sep 2019 11:12:14 -0400 Subject: [PATCH 0109/1013] Tests --- tests/Admin.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/Admin.php b/tests/Admin.php index b97ca2c45..302c37df4 100644 --- a/tests/Admin.php +++ b/tests/Admin.php @@ -13,11 +13,6 @@ class Admin extends Model implements AuthorizableContract, AuthenticatableContra { use HasRoles, Authorizable, Authenticatable; - /** - * The attributes that are mass assignable. - * - * @var array - */ protected $fillable = ['email']; public $timestamps = false; From 05db742fa407cdab330d9a051bab8cfaaf38a394 Mon Sep 17 00:00:00 2001 From: Mindaugas Date: Mon, 9 Sep 2019 10:23:29 +0300 Subject: [PATCH 0110/1013] update role list --- docs/basic-usage/role-permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/role-permissions.md b/docs/basic-usage/role-permissions.md index ee6dfbb75..e1d6a4694 100644 --- a/docs/basic-usage/role-permissions.md +++ b/docs/basic-usage/role-permissions.md @@ -36,7 +36,7 @@ $user->hasRole('writer'); You can also determine if a user has any of a given list of roles: ```php -$user->hasAnyRole(Role::all()); +$user->hasAnyRole('writer', 'reader'); ``` You can also determine if a user has all of a given list of roles: From c44858cc99a7a6fb2e31f3a28dae90581a715a01 Mon Sep 17 00:00:00 2001 From: Julius Kiekbusch Date: Tue, 10 Sep 2019 16:08:25 +0200 Subject: [PATCH 0111/1013] Fix hasAnyRole hasAnyRole requires an array instead of multiple parameters --- docs/basic-usage/role-permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/role-permissions.md b/docs/basic-usage/role-permissions.md index e1d6a4694..be31b0533 100644 --- a/docs/basic-usage/role-permissions.md +++ b/docs/basic-usage/role-permissions.md @@ -36,7 +36,7 @@ $user->hasRole('writer'); You can also determine if a user has any of a given list of roles: ```php -$user->hasAnyRole('writer', 'reader'); +$user->hasAnyRole(['writer', 'reader']); ``` You can also determine if a user has all of a given list of roles: From 059a6da5315fd76cec3bbc5a87b4959b3e2dce42 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 10 Sep 2019 10:15:41 -0400 Subject: [PATCH 0112/1013] Update .travis.yml --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4cba751ca..4dc196110 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,6 @@ php: - 7.2 - 7.3 - 7.4snapshot - - nightly matrix: include: @@ -16,7 +15,6 @@ matrix: env: COMPOSER_FLAGS="--prefer-lowest" allow_failures: - php: 7.4snapshot - - php: nightly before_script: - travis_retry composer self-update From 0b38370d8092e8f460678c6d8a8d58599c52f67f Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 18 Sep 2019 11:29:23 -0400 Subject: [PATCH 0113/1013] Update basic-usage.md Fixes #1218 - Thanks @hi-rafa --- docs/basic-usage/basic-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/basic-usage.md b/docs/basic-usage/basic-usage.md index 02b5471e0..fe06ca5a9 100644 --- a/docs/basic-usage/basic-usage.md +++ b/docs/basic-usage/basic-usage.md @@ -66,7 +66,7 @@ $role->revokePermissionTo($permission); $permission->removeRole($role); ``` -If you're using multiple guards the `guard_name` attribute needs to be set as well. Read about it in the [using multiple guards](#using-multiple-guards) section of the readme. +If you're using multiple guards the `guard_name` attribute needs to be set as well. Read about it in the [using multiple guards](../using-multiple-guards) section of the readme. The `HasRoles` trait adds Eloquent relationships to your models, which can be accessed directly or used as a base query: From d618b389b18ccde3d09cf713750965a42f280335 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 18 Sep 2019 11:30:28 -0400 Subject: [PATCH 0114/1013] Update role-permissions.md --- docs/basic-usage/role-permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/role-permissions.md b/docs/basic-usage/role-permissions.md index be31b0533..08511bd49 100644 --- a/docs/basic-usage/role-permissions.md +++ b/docs/basic-usage/role-permissions.md @@ -110,4 +110,4 @@ the second will be a collection with the `edit article` permission and the third ### NOTE about using permission names in policies -When calling `authorize()` for a policy method, if you have a permission named the same as one of those policy methods, your permission "name" will take precedence and not fire the policy. For this reason it may be wise to avoid naming your permissions the same as the methods in your policy. While you can define your own method names, you can read more about the defaults Laravel offers in Laravel's documentation at https://laravel.com/docs/5.8/authorization#writing-policies +When calling `authorize()` for a policy method, if you have a permission named the same as one of those policy methods, your permission "name" will take precedence and not fire the policy. For this reason it may be wise to avoid naming your permissions the same as the methods in your policy. While you can define your own method names, you can read more about the defaults Laravel offers in Laravel's documentation at https://laravel.com/docs/authorization#writing-policies From 2f26ff51039be3f810f2900462bfc9cd90c096f5 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 18 Sep 2019 12:14:43 -0400 Subject: [PATCH 0115/1013] Update basic-usage.md --- docs/basic-usage/basic-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/basic-usage.md b/docs/basic-usage/basic-usage.md index fe06ca5a9..7a4fbe13a 100644 --- a/docs/basic-usage/basic-usage.md +++ b/docs/basic-usage/basic-usage.md @@ -66,7 +66,7 @@ $role->revokePermissionTo($permission); $permission->removeRole($role); ``` -If you're using multiple guards the `guard_name` attribute needs to be set as well. Read about it in the [using multiple guards](../using-multiple-guards) section of the readme. +If you're using multiple guards the `guard_name` attribute needs to be set as well. Read about it in the [using multiple guards](../multiple-guards) section of the readme. The `HasRoles` trait adds Eloquent relationships to your models, which can be accessed directly or used as a base query: From d339d2f358e711eb8091560b3ce65035a73104ca Mon Sep 17 00:00:00 2001 From: Abdelouahab Djoudi Date: Fri, 20 Sep 2019 08:35:27 +0100 Subject: [PATCH 0116/1013] migratation to L 5.8 and 6.0 --- .../migrations/create_permission_tables.php.stub | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/database/migrations/create_permission_tables.php.stub b/database/migrations/create_permission_tables.php.stub index be416fcba..f6c189eb6 100644 --- a/database/migrations/create_permission_tables.php.stub +++ b/database/migrations/create_permission_tables.php.stub @@ -17,21 +17,21 @@ class CreatePermissionTables extends Migration $columnNames = config('permission.column_names'); Schema::create($tableNames['permissions'], function (Blueprint $table) { - $table->increments('id'); + $table->bigIncrements('id'); $table->string('name'); $table->string('guard_name'); $table->timestamps(); }); Schema::create($tableNames['roles'], function (Blueprint $table) { - $table->increments('id'); + $table->bigIncrements('id'); $table->string('name'); $table->string('guard_name'); $table->timestamps(); }); Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) { - $table->unsignedInteger('permission_id'); + $table->unsignedBigInteger('permission_id'); $table->string('model_type'); $table->unsignedBigInteger($columnNames['model_morph_key']); @@ -47,7 +47,7 @@ class CreatePermissionTables extends Migration }); Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames) { - $table->unsignedInteger('role_id'); + $table->unsignedBigInteger('role_id'); $table->string('model_type'); $table->unsignedBigInteger($columnNames['model_morph_key']); @@ -63,8 +63,8 @@ class CreatePermissionTables extends Migration }); Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames) { - $table->unsignedInteger('permission_id'); - $table->unsignedInteger('role_id'); + $table->unsignedBigInteger('permission_id'); + $table->unsignedBigInteger('role_id'); $table->foreign('permission_id') ->references('id') From 2a6b502709e1553a92a6bce8655daeb6292825e8 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 4 Oct 2019 13:24:18 -0400 Subject: [PATCH 0117/1013] Create exceptions.md --- docs/advanced-usage/exceptions.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 docs/advanced-usage/exceptions.md diff --git a/docs/advanced-usage/exceptions.md b/docs/advanced-usage/exceptions.md new file mode 100644 index 000000000..d7c8f9b79 --- /dev/null +++ b/docs/advanced-usage/exceptions.md @@ -0,0 +1,26 @@ +--- +title: Exceptions +weight: 3 +--- + +If you need to override exceptions thrown by this package, you can simply use normal [Laravel practices for handling exceptions](https://laravel.com/docs/errors#render-method). + +An example is shown below for your convenience, but nothing here is specific to this package other than the name of the exception. + +You can find all the exceptions added by this package in the code here: https://github.com/spatie/laravel-permission/tree/master/src/Exceptions + + +**app/Exceptions/Handler.php** +```php +public function render($request, Exception $exception) +{ + if ($exception instanceof \Spatie\Permission\Exceptions\UnauthorizedException) { + return response()->json([ + 'responseMessage' => 'You do not have the required authorization.', + 'responseStatus' => 403, + ]); + } + + return parent::render($request, $exception); +} +``` From 9cebffb3fbcbd4b39a1fca2a35f50df74b753554 Mon Sep 17 00:00:00 2001 From: "Dennis (DZ)" <52223403+dennis-git@users.noreply.github.com> Date: Mon, 7 Oct 2019 12:29:53 -0700 Subject: [PATCH 0118/1013] Implementation of optional guard check for hasRoles and hasAllRoles (#1236) implementation of optional guard check for hasRoles and hasAllRoles methods inside HasRoles trait --- src/Traits/HasRoles.php | 33 +++++++++++++++++++++------------ tests/HasRolesTest.php | 27 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index ec78e296c..f50f3810d 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -158,7 +158,7 @@ public function removeRole($role) /** * Remove all current roles and set the given ones. * - * @param array|\Spatie\Permission\Contracts\Role|string ...$roles + * @param array|\Spatie\Permission\Contracts\Role|string ...$roles * * @return $this */ @@ -173,21 +173,25 @@ public function syncRoles(...$roles) * Determine if the model has (one of) the given role(s). * * @param string|int|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles - * + * @param string|null $guard * @return bool */ - public function hasRole($roles): bool + public function hasRole($roles, string $guard = null): bool { if (is_string($roles) && false !== strpos($roles, '|')) { $roles = $this->convertPipeToArray($roles); } if (is_string($roles)) { - return $this->roles->contains('name', $roles); + return $guard + ? $this->roles->where('guard_name', $guard)->contains('name', $roles) + : $this->roles->contains('name', $roles); } if (is_int($roles)) { - return $this->roles->contains('id', $roles); + return $guard + ? $this->roles->where('guard_name', $guard)->contains('id', $roles) + : $this->roles->contains('id', $roles); } if ($roles instanceof Role) { @@ -196,7 +200,7 @@ public function hasRole($roles): bool if (is_array($roles)) { foreach ($roles as $role) { - if ($this->hasRole($role)) { + if ($this->hasRole($role, $guard)) { return true; } } @@ -204,7 +208,7 @@ public function hasRole($roles): bool return false; } - return $roles->intersect($this->roles)->isNotEmpty(); + return $roles->intersect($guard ? $this->roles->where('guard_name', $guard) : $this->roles)->isNotEmpty(); } /** @@ -222,18 +226,20 @@ public function hasAnyRole($roles): bool /** * Determine if the model has all of the given role(s). * - * @param string|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles - * + * @param string|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles + * @param string|null $guard * @return bool */ - public function hasAllRoles($roles): bool + public function hasAllRoles($roles, string $guard = null): bool { if (is_string($roles) && false !== strpos($roles, '|')) { $roles = $this->convertPipeToArray($roles); } if (is_string($roles)) { - return $this->roles->contains('name', $roles); + return $guard + ? $this->roles->where('guard_name', $guard)->contains('name', $roles) + : $this->roles->contains('name', $roles); } if ($roles instanceof Role) { @@ -244,7 +250,10 @@ public function hasAllRoles($roles): bool return $role instanceof Role ? $role->name : $role; }); - return $roles->intersect($this->getRoleNames()) == $roles; + return $roles->intersect( + $guard + ? $this->roles->where('guard_name', $guard)->pluck('name') + : $this->getRoleNames()) == $roles; } /** diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 530a33406..7f90f8e19 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -12,6 +12,26 @@ class HasRolesTest extends TestCase public function it_can_determine_that_the_user_does_not_have_a_role() { $this->assertFalse($this->testUser->hasRole('testRole')); + + $role = app(Role::class)->findOrCreate('testRoleInWebGuard', 'web'); + + $this->assertFalse($this->testUser->hasRole($role)); + + $this->testUser->assignRole($role); + $this->assertTrue($this->testUser->hasRole($role)); + $this->assertTrue($this->testUser->hasRole($role->name)); + $this->assertTrue($this->testUser->hasRole($role->name, $role->guard_name)); + $this->assertTrue($this->testUser->hasRole([$role->name, 'fakeRole'], $role->guard_name)); + $this->assertTrue($this->testUser->hasRole($role->id, $role->guard_name)); + $this->assertTrue($this->testUser->hasRole([$role->id, 'fakeRole'], $role->guard_name)); + + $this->assertFalse($this->testUser->hasRole($role->name, 'fakeGuard')); + $this->assertFalse($this->testUser->hasRole([$role->name, 'fakeRole'], 'fakeGuard')); + $this->assertFalse($this->testUser->hasRole($role->id, 'fakeGuard')); + $this->assertFalse($this->testUser->hasRole([$role->id, 'fakeRole'], 'fakeGuard')); + + $role = app(Role::class)->findOrCreate('testRoleInWebGuard2', 'web'); + $this->assertFalse($this->testUser->hasRole($role)); } /** @test */ @@ -444,11 +464,18 @@ public function it_can_determine_that_a_user_has_all_of_the_given_roles() $this->testUser->assignRole($this->testUserRole); + $this->assertTrue($this->testUser->hasAllRoles('testRole')); + $this->assertTrue($this->testUser->hasAllRoles('testRole', 'web')); + $this->assertFalse($this->testUser->hasAllRoles('testRole', 'fakeGuard')); + $this->assertFalse($this->testUser->hasAllRoles(['testRole', 'second role'])); + $this->assertFalse($this->testUser->hasAllRoles(['testRole', 'second role'], 'web')); $this->testUser->assignRole('second role'); $this->assertTrue($this->testUser->hasAllRoles(['testRole', 'second role'])); + $this->assertTrue($this->testUser->hasAllRoles(['testRole', 'second role'], 'web')); + $this->assertFalse($this->testUser->hasAllRoles(['testRole', 'second role'], 'fakeGuard')); } /** @test */ From 98a9d1ee428d102845a3e50ba6218387c01e662c Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 7 Oct 2019 17:56:39 -0400 Subject: [PATCH 0119/1013] Create other.md --- docs/advanced-usage/other.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 docs/advanced-usage/other.md diff --git a/docs/advanced-usage/other.md b/docs/advanced-usage/other.md new file mode 100644 index 000000000..a6bc996d1 --- /dev/null +++ b/docs/advanced-usage/other.md @@ -0,0 +1,4 @@ +--- +title: Other +weight: 8 +--- From 42ab6d3cc6facf3e57883cef1e54736a6bcf8e1d Mon Sep 17 00:00:00 2001 From: Juvenal Pengele Date: Sun, 13 Oct 2019 09:09:39 +0000 Subject: [PATCH 0120/1013] Add an API to check many permissions There is a function that allow to get direct permissions. There is also a method that get all permissions but this method check if the user has direct permission or has permission via the role These methods are going to check only based on direct permissions without dealing with permissions of the role --- src/Traits/HasPermissions.php | 32 ++++++++++++++++++++++++++++++++ tests/HasPermissionsTest.php | 15 +++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 8b81acf6a..0908cd18c 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -438,4 +438,36 @@ public function forgetCachedPermissions() { app(PermissionRegistrar::class)->forgetCachedPermissions(); } + + /** + * Check if there are all the direct permissions + * @param array $permissions + * @return bool + */ + public function hasAllDirectPermissions(array $permissions) : bool + { + foreach ($permissions as $permission) { + if(! $this->hasDirectPermission($permission)) { + return false; + } + } + + return true; + } + + /** + * Check if there are any the direct permissions + * @param array $permissions + * @return bool + */ + public function hasAnyDirectPermission(array $permissions) : bool + { + foreach ($permissions as $permission) { + if($this->hasDirectPermission($permission)) { + return true; + } + } + + return false; + } } diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index cc0e5e024..757557586 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -497,4 +497,19 @@ public function it_can_retrieve_permission_names() $this->testUser->getPermissionNames() ); } + + /** @test */ + public function it_can_check_many_direct_permissions() + { + $this->testUser->givePermissionTo(['edit-articles', 'edit-news']); + $this->assertTrue($this->testUser->hasAllDirectPermissions(['edit-news', 'edit-articles'])); + $this->assertFalse($this->testUser->hasAllDirectPermissions(['edit-articles', 'edit-news', 'edit-blog'])); + } + + /** @test */ + public function it_can_check_if_there_is_any_of_the_direct_permissions_given() + { + $this->testUser->givePermissionTo(['edit-articles', 'edit-news']); + $this->assertTrue($this->testUser->hasAnyDirectPermission(['edit-news', 'edit-blog'])); + } } From 690f85daea192079100e35d7f81181d6382d78b6 Mon Sep 17 00:00:00 2001 From: Juvenal Pengele Date: Sun, 13 Oct 2019 09:09:39 +0000 Subject: [PATCH 0121/1013] Add an API to check many direct permissions There is a function that allow to get direct permissions. There is also a method that get all permissions but this method check if the user has direct permission or has permission via the role These methods are going to check only based on direct permissions without dealing with permissions of the role --- src/Traits/HasPermissions.php | 32 ++++++++++++++++++++++++++++++++ tests/HasPermissionsTest.php | 15 +++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 8b81acf6a..0908cd18c 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -438,4 +438,36 @@ public function forgetCachedPermissions() { app(PermissionRegistrar::class)->forgetCachedPermissions(); } + + /** + * Check if there are all the direct permissions + * @param array $permissions + * @return bool + */ + public function hasAllDirectPermissions(array $permissions) : bool + { + foreach ($permissions as $permission) { + if(! $this->hasDirectPermission($permission)) { + return false; + } + } + + return true; + } + + /** + * Check if there are any the direct permissions + * @param array $permissions + * @return bool + */ + public function hasAnyDirectPermission(array $permissions) : bool + { + foreach ($permissions as $permission) { + if($this->hasDirectPermission($permission)) { + return true; + } + } + + return false; + } } diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index cc0e5e024..757557586 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -497,4 +497,19 @@ public function it_can_retrieve_permission_names() $this->testUser->getPermissionNames() ); } + + /** @test */ + public function it_can_check_many_direct_permissions() + { + $this->testUser->givePermissionTo(['edit-articles', 'edit-news']); + $this->assertTrue($this->testUser->hasAllDirectPermissions(['edit-news', 'edit-articles'])); + $this->assertFalse($this->testUser->hasAllDirectPermissions(['edit-articles', 'edit-news', 'edit-blog'])); + } + + /** @test */ + public function it_can_check_if_there_is_any_of_the_direct_permissions_given() + { + $this->testUser->givePermissionTo(['edit-articles', 'edit-news']); + $this->assertTrue($this->testUser->hasAnyDirectPermission(['edit-news', 'edit-blog'])); + } } From 29f0c77c57ac73386939d558b88fb05c4d992f1f Mon Sep 17 00:00:00 2001 From: Juvenal Pengele Date: Sun, 13 Oct 2019 10:04:04 +0000 Subject: [PATCH 0122/1013] Update the documentation in order to check if the user has all direct permissions --- docs/basic-usage/role-permissions.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/basic-usage/role-permissions.md b/docs/basic-usage/role-permissions.md index 08511bd49..96ee19986 100644 --- a/docs/basic-usage/role-permissions.md +++ b/docs/basic-usage/role-permissions.md @@ -90,6 +90,20 @@ but `false` for `$user->hasDirectPermission('edit articles')`. This method is useful if one builds a form for setting permissions for roles and users in an application and wants to restrict or change inherited permissions of roles of the user, i.e. allowing to change only direct permissions of the user. +You can check if the user has direct permissions without passing by the role's permissions: + +```php +// Check if the user has all direct permissions +$user->hasAllDirectPermissions(['edit articles', 'delete articles']); + +// Check if the user has any permission directly +$user->hasAnyDirectPermission(['create articles', 'delete articles']); +``` +By following the previous example, when we call `$user->hasAllDirectPermissions(['edit articles', 'delete articles'])` +it returns `true`, because the user has all these direct permissions. When we call +`$user->hasAnyDirectPermission('edit articles')`, it returns `true` because the user has one of the provided permissions. + + You can list all of these permissions: ```php @@ -105,9 +119,16 @@ $user->getAllPermissions(); All these responses are collections of `Spatie\Permission\Models\Permission` objects. + + +If we follow the previous example, the first response will be a collection with the `delete article` permission and +the second will be a collection with the `edit article` permission and the third will contain both. + If we follow the previous example, the first response will be a collection with the `delete article` permission and the second will be a collection with the `edit article` permission and the third will contain both. + + ### NOTE about using permission names in policies When calling `authorize()` for a policy method, if you have a permission named the same as one of those policy methods, your permission "name" will take precedence and not fire the policy. For this reason it may be wise to avoid naming your permissions the same as the methods in your policy. While you can define your own method names, you can read more about the defaults Laravel offers in Laravel's documentation at https://laravel.com/docs/authorization#writing-policies From dd30d18cf0eee6f5f940960de4ecbab6d7adb1eb Mon Sep 17 00:00:00 2001 From: Juvenal Pengele Date: Sun, 13 Oct 2019 10:32:29 +0000 Subject: [PATCH 0123/1013] Update documentation and code to fix Style CI issues. --- src/Traits/HasPermissions.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 0908cd18c..821f06d9c 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -440,14 +440,14 @@ public function forgetCachedPermissions() } /** - * Check if there are all the direct permissions + * Check if there are all the direct permissions. * @param array $permissions * @return bool */ public function hasAllDirectPermissions(array $permissions) : bool { foreach ($permissions as $permission) { - if(! $this->hasDirectPermission($permission)) { + if (! $this->hasDirectPermission($permission)) { return false; } } @@ -456,14 +456,14 @@ public function hasAllDirectPermissions(array $permissions) : bool } /** - * Check if there are any the direct permissions + * Check if there are any the direct permissions. * @param array $permissions * @return bool */ public function hasAnyDirectPermission(array $permissions) : bool { foreach ($permissions as $permission) { - if($this->hasDirectPermission($permission)) { + if ($this->hasDirectPermission($permission)) { return true; } } From 11b4b3e973a7934b7a634e66e5e2b263adba4ff3 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 16 Oct 2019 15:08:16 -0400 Subject: [PATCH 0124/1013] Update extending.md --- docs/advanced-usage/extending.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/advanced-usage/extending.md b/docs/advanced-usage/extending.md index 5c6db7b13..58e9f4447 100644 --- a/docs/advanced-usage/extending.md +++ b/docs/advanced-usage/extending.md @@ -3,7 +3,13 @@ title: Extending weight: 3 --- +## Extending User Models +Laravel's authorization features are available in Models which implement the `Illuminate\Foundation\Auth\Access\Authorizable` trait. In a fresh Laravel app this is done in `\App\User` by extending `Illuminate\Foundation\Auth\User` out-of-the-box. +If you are creating your own, or additional, User models and wish Authorization features to be available on it, including Roles and Permissions with this package, you need to implement `Illuminate\Foundation\Auth\Access\Authorizable` in one of those ways as well. + + +## Extending Role and Permission Models If you are extending or replacing the role/permission models, you will need to specify your new models in the configuration. First be sure that you've published the configuration file (see the Installation instructions), and edit it to update the `models.role` and `models.permission` values to point to your new models. From e8ce51b0272f65b8a403a9878fab9e515872b45d Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 16 Oct 2019 15:12:43 -0400 Subject: [PATCH 0125/1013] Update basic-usage.md Removed confusing comment. Fixes #1248 See https://docs.spatie.be/laravel-permission/v3/advanced-usage/extending/#extending-user-models for better information. --- docs/basic-usage/basic-usage.md | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/docs/basic-usage/basic-usage.md b/docs/basic-usage/basic-usage.md index 7a4fbe13a..f191fd68f 100644 --- a/docs/basic-usage/basic-usage.md +++ b/docs/basic-usage/basic-usage.md @@ -17,22 +17,6 @@ class User extends Authenticatable } ``` -> - note that if you need to use `HasRoles` trait with another model ex.`Page` you will also need to add `protected $guard_name = 'web';` as well to that model or you would get an error -> -```php -use Illuminate\Database\Eloquent\Model; -use Spatie\Permission\Traits\HasRoles; - -class Page extends Model -{ - use HasRoles; - - protected $guard_name = 'web'; // or whatever guard you want to use - - // ... -} -``` - This package allows for users to be associated with permissions and roles. Every role is associated with multiple permissions. A `Role` and a `Permission` are regular Eloquent models. They require a `name` and can be created like this: From e3821559c69b2b1ea8fdd967fa89ba13a01ead78 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 16 Oct 2019 15:25:56 -0400 Subject: [PATCH 0126/1013] Update CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c5115c3d..0b73f3755 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to `laravel-permission` will be documented in this file +## 3.2.0 - 2019-10-16 +- Implementation of optional guard check for hasRoles and hasAllRoles - See #1236 + +## 3.1.0 - 2019-10-16 +- Use bigIncrements/bigInteger in migration - See #1224 + ## 3.0.0 - 2019-09-02 - Update dependencies to allow for Laravel 6.0 - Drop support for Laravel 5.7 and older, and PHP 7.1 and older. (They can use v2 of this package until they upgrade.) From 1e7feccb0300c5cad419f27b07bd9ea7aaf40c0c Mon Sep 17 00:00:00 2001 From: Sergej Date: Mon, 21 Oct 2019 16:36:04 +0200 Subject: [PATCH 0127/1013] Remove version checks --- src/Models/Permission.php | 4 ---- src/Models/Role.php | 4 ---- src/PermissionRegistrar.php | 7 ------- src/Traits/HasPermissions.php | 15 ++++----------- tests/CacheTest.php | 2 +- 5 files changed, 5 insertions(+), 27 deletions(-) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index a4adb4416..825924685 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -40,10 +40,6 @@ public static function create(array $attributes = []) throw PermissionAlreadyExists::create($attributes['name'], $attributes['guard_name']); } - if (isNotLumen() && app()::VERSION < '5.4') { - return parent::create($attributes); - } - return static::query()->create($attributes); } diff --git a/src/Models/Role.php b/src/Models/Role.php index 3a4c5bd98..7fb02468c 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -37,10 +37,6 @@ public static function create(array $attributes = []) throw RoleAlreadyExists::create($attributes['name'], $attributes['guard_name']); } - if (isNotLumen() && app()::VERSION < '5.4') { - return parent::create($attributes); - } - return static::query()->create($attributes); } diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index d4bdbd62d..b99b89bda 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -59,13 +59,6 @@ protected function initializeCache() { self::$cacheExpirationTime = config('permission.cache.expiration_time', config('permission.cache_expiration_time')); - if (app()->version() <= '5.5') { - if (self::$cacheExpirationTime instanceof \DateInterval) { - $interval = self::$cacheExpirationTime; - self::$cacheExpirationTime = $interval->m * 30 * 60 * 24 + $interval->d * 60 * 24 + $interval->h * 60 + $interval->i; - } - } - self::$cacheKey = config('permission.cache.key'); self::$cacheModelKey = config('permission.cache.model_key'); diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 8b81acf6a..a445db987 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -263,17 +263,10 @@ public function hasDirectPermission($permission): bool */ public function getPermissionsViaRoles(): Collection { - $relationships = ['roles', 'roles.permissions']; - - if (method_exists($this, 'loadMissing')) { - $this->loadMissing($relationships); - } else { - $this->load($relationships); - } - - return $this->roles->flatMap(function ($role) { - return $role->permissions; - })->sort()->values(); + return $this->loadMissing('roles', 'roles.permissions') + ->roles->flatMap(function ($role) { + return $role->permissions; + })->sort()->values(); } /** diff --git a/tests/CacheTest.php b/tests/CacheTest.php index 084e02ee4..58c2c0819 100644 --- a/tests/CacheTest.php +++ b/tests/CacheTest.php @@ -199,7 +199,7 @@ public function get_all_permissions_should_use_the_cache() $actual = $this->testUser->getAllPermissions()->pluck('name'); $this->assertEquals($actual, collect($expected)); - $this->assertQueryCount(method_exists($this->testUser, 'loadMissing') ? 2 : 3); + $this->assertQueryCount(2); } /** @test */ From 1188d22227b916cca3444387920f49c5677115f8 Mon Sep 17 00:00:00 2001 From: Sergej Date: Thu, 24 Oct 2019 11:01:20 +0200 Subject: [PATCH 0128/1013] Use checkPermissionTo method at gate check --- src/PermissionRegistrar.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index d4bdbd62d..8d0984031 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -8,7 +8,6 @@ use Illuminate\Contracts\Auth\Access\Gate; use Spatie\Permission\Contracts\Permission; use Illuminate\Contracts\Auth\Access\Authorizable; -use Spatie\Permission\Exceptions\PermissionDoesNotExist; class PermissionRegistrar { @@ -98,11 +97,8 @@ protected function getCacheStoreFromConfig(): \Illuminate\Contracts\Cache\Reposi public function registerPermissions(): bool { $this->gate->before(function (Authorizable $user, string $ability) { - try { - if (method_exists($user, 'hasPermissionTo')) { - return $user->hasPermissionTo($ability) ?: null; - } - } catch (PermissionDoesNotExist $e) { + if (method_exists($user, 'checkPermissionTo')) { + return $user->checkPermissionTo($ability) ?: null; } }); From 53d9272b03c0de548fd79ecb47bc5d551cbc6f06 Mon Sep 17 00:00:00 2001 From: Sergej Date: Thu, 24 Oct 2019 19:37:52 +0200 Subject: [PATCH 0129/1013] Remove unreachable code --- src/Traits/HasPermissions.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 8b81acf6a..4dcdc0d38 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -239,16 +239,10 @@ public function hasDirectPermission($permission): bool if (is_string($permission)) { $permission = $permissionClass->findByName($permission, $this->getDefaultGuardName()); - if (! $permission) { - return false; - } } if (is_int($permission)) { $permission = $permissionClass->findById($permission, $this->getDefaultGuardName()); - if (! $permission) { - return false; - } } if (! $permission instanceof Permission) { From 541991daf1ff37fbe126fa1e051f3cb9f8b2b3ff Mon Sep 17 00:00:00 2001 From: Sergej Date: Thu, 24 Oct 2019 19:38:12 +0200 Subject: [PATCH 0130/1013] Throw an exception, if permission does not exist --- src/Traits/HasPermissions.php | 2 +- tests/HasPermissionsTest.php | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 4dcdc0d38..d9137498d 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -246,7 +246,7 @@ public function hasDirectPermission($permission): bool } if (! $permission instanceof Permission) { - return false; + throw new PermissionDoesNotExist; } return $this->permissions->contains('id', $permission->id); diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index cc0e5e024..f75fa626b 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -164,7 +164,9 @@ public function it_throws_an_exception_when_calling_hasDirectPermission_with_an_ { $user = User::create(['email' => 'user1@test.com']); - $this->assertFalse($user->hasDirectPermission(new \stdClass())); + $this->expectException(PermissionDoesNotExist::class); + + $user->hasDirectPermission(new \stdClass()); } /** @test */ @@ -172,7 +174,9 @@ public function it_throws_an_exception_when_calling_hasDirectPermission_with_nul { $user = User::create(['email' => 'user1@test.com']); - $this->assertFalse($user->hasDirectPermission(null)); + $this->expectException(PermissionDoesNotExist::class); + + $user->hasDirectPermission(null); } /** @test */ From f4e5611a3985ed967c17cbe04e0f4f648981d5fc Mon Sep 17 00:00:00 2001 From: Sergej Date: Fri, 25 Oct 2019 18:48:59 +0200 Subject: [PATCH 0131/1013] Enforce unique constraints on database level --- database/migrations/create_permission_tables.php.stub | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/database/migrations/create_permission_tables.php.stub b/database/migrations/create_permission_tables.php.stub index f6c189eb6..1e43e59e5 100644 --- a/database/migrations/create_permission_tables.php.stub +++ b/database/migrations/create_permission_tables.php.stub @@ -21,6 +21,8 @@ class CreatePermissionTables extends Migration $table->string('name'); $table->string('guard_name'); $table->timestamps(); + + $table->unique(['name', 'guard_name']); }); Schema::create($tableNames['roles'], function (Blueprint $table) { @@ -28,6 +30,8 @@ class CreatePermissionTables extends Migration $table->string('name'); $table->string('guard_name'); $table->timestamps(); + + $table->unique(['name', 'guard_name']); }); Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) { From 4c99e130e8d9412c1ae87f95a65c1305763722a4 Mon Sep 17 00:00:00 2001 From: Frederick Vanbrabant Date: Wed, 6 Nov 2019 13:20:49 +0100 Subject: [PATCH 0132/1013] Made the readme a tiny bit friendlier --- docs/installation-laravel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 70218e51d..0b72142a5 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -20,7 +20,7 @@ The service provider will automatically get registered. Or you may manually add ]; ``` -You must publish [the migration](https://github.com/spatie/laravel-permission/blob/master/database/migrations/create_permission_tables.php.stub) with: +You should publish [the migration](https://github.com/spatie/laravel-permission/blob/master/database/migrations/create_permission_tables.php.stub) with: ```bash php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" --tag="migrations" From 753ce00d91bb2b5a2f03ecb7d1740de29c0a24fa Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 8 Nov 2019 12:28:39 -0500 Subject: [PATCH 0133/1013] Update .styleci.yml --- .styleci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.styleci.yml b/.styleci.yml index 977d0ec6e..08589c868 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -1,7 +1,10 @@ preset: laravel enabled: +- length_ordered_imports - long_list_syntax disabled: +- self_accessor - short_list_syntax +- single_class_element_per_statement From cb3ecf8b6e311f433801687f07e6b0df558b2585 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 21 Nov 2019 17:45:26 -0500 Subject: [PATCH 0134/1013] Update installation-lumen.md --- docs/installation-lumen.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/installation-lumen.md b/docs/installation-lumen.md index cfc6569be..4f148fb45 100644 --- a/docs/installation-lumen.md +++ b/docs/installation-lumen.md @@ -48,3 +48,6 @@ Now, run your migrations: ```bash php artisan migrate ``` + +Remember that Laravel's authorization layer requires that your `User` model implement the `Illuminate\Contracts\Auth\Access\Authorizable` contract. In Lumen you will then also need to use the `Laravel\Lumen\Auth\Authorizable` trait. + From 09ff8534d12b85f4b9546cbdd075e4080e508a47 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 21 Nov 2019 17:48:07 -0500 Subject: [PATCH 0135/1013] Update extending.md --- docs/advanced-usage/extending.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/advanced-usage/extending.md b/docs/advanced-usage/extending.md index 58e9f4447..dbda3f9b6 100644 --- a/docs/advanced-usage/extending.md +++ b/docs/advanced-usage/extending.md @@ -4,9 +4,9 @@ weight: 3 --- ## Extending User Models -Laravel's authorization features are available in Models which implement the `Illuminate\Foundation\Auth\Access\Authorizable` trait. In a fresh Laravel app this is done in `\App\User` by extending `Illuminate\Foundation\Auth\User` out-of-the-box. +Laravel's authorization features are available in models which implement the `Illuminate\Foundation\Auth\Access\Authorizable` trait. By default Laravel does this in `\App\User` by extending `Illuminate\Foundation\Auth\User`, in which the trait and contracts are declared. -If you are creating your own, or additional, User models and wish Authorization features to be available on it, including Roles and Permissions with this package, you need to implement `Illuminate\Foundation\Auth\Access\Authorizable` in one of those ways as well. +If you are creating your own, or additional, User models and wish Authorization features to be available on it, including the Roles and Permissions features of this package, you need to implement `Illuminate\Foundation\Auth\Access\Authorizable` in one of those ways as well. ## Extending Role and Permission Models From aec87969678f0121e8b6aca3e9b0f00fd269535c Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 21 Nov 2019 17:50:54 -0500 Subject: [PATCH 0136/1013] Update introduction.md --- docs/introduction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/introduction.md b/docs/introduction.md index 741a9ba83..047c039bd 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -17,7 +17,7 @@ $user->assignRole('writer'); $role->givePermissionTo('edit articles'); ``` -If you're using multiple guards we've got you covered as well. Every guard will have its own set of permissions and roles that can be assigned to the guard's users. Read about it in the [using multiple guards](#using-multiple-guards) section of the readme. +If you're using multiple guards we've got you covered as well. Every guard will have its own set of permissions and roles that can be assigned to the guard's users. Read about it in the [using multiple guards](basic-usage/multiple-guards) section of the readme. Because all permissions will be registered on [Laravel's gate](https://laravel.com/docs/authorization), you can check if a user has a permission with Laravel's default `can` function: From d3c6ade80eed0203fd1040de6099beeeecb59223 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 21 Nov 2019 17:53:48 -0500 Subject: [PATCH 0137/1013] Update introduction.md --- docs/introduction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/introduction.md b/docs/introduction.md index 047c039bd..ab3ee5918 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -17,7 +17,7 @@ $user->assignRole('writer'); $role->givePermissionTo('edit articles'); ``` -If you're using multiple guards we've got you covered as well. Every guard will have its own set of permissions and roles that can be assigned to the guard's users. Read about it in the [using multiple guards](basic-usage/multiple-guards) section of the readme. +If you're using multiple guards we've got you covered as well. Every guard will have its own set of permissions and roles that can be assigned to the guard's users. Read about it in the [using multiple guards](/basic-usage/multiple-guards) section of the readme. Because all permissions will be registered on [Laravel's gate](https://laravel.com/docs/authorization), you can check if a user has a permission with Laravel's default `can` function: From d599909f3501c7297a97b253b8ad0f55db2d4e51 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 22 Nov 2019 00:14:10 -0500 Subject: [PATCH 0138/1013] Update introduction.md @riasvdv Do you have any suggestions for how to not have to hard-code the version-number into an internal link within the docs? --- docs/introduction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/introduction.md b/docs/introduction.md index ab3ee5918..12c9011eb 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -17,7 +17,7 @@ $user->assignRole('writer'); $role->givePermissionTo('edit articles'); ``` -If you're using multiple guards we've got you covered as well. Every guard will have its own set of permissions and roles that can be assigned to the guard's users. Read about it in the [using multiple guards](/basic-usage/multiple-guards) section of the readme. +If you're using multiple guards we've got you covered as well. Every guard will have its own set of permissions and roles that can be assigned to the guard's users. Read about it in the [using multiple guards](/v3/basic-usage/multiple-guards) section of the readme. Because all permissions will be registered on [Laravel's gate](https://laravel.com/docs/authorization), you can check if a user has a permission with Laravel's default `can` function: From 4fa0b08e8bb27b0035bd1e1bc78549fc57d73466 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 22 Nov 2019 00:15:42 -0500 Subject: [PATCH 0139/1013] Update introduction.md --- docs/introduction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/introduction.md b/docs/introduction.md index 12c9011eb..eaca3636d 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -17,7 +17,7 @@ $user->assignRole('writer'); $role->givePermissionTo('edit articles'); ``` -If you're using multiple guards we've got you covered as well. Every guard will have its own set of permissions and roles that can be assigned to the guard's users. Read about it in the [using multiple guards](/v3/basic-usage/multiple-guards) section of the readme. +If you're using multiple guards we've got you covered as well. Every guard will have its own set of permissions and roles that can be assigned to the guard's users. Read about it in the [using multiple guards](https://docs.spatie.be/laravel-permission/v3/basic-usage/multiple-guards/) section of the readme. Because all permissions will be registered on [Laravel's gate](https://laravel.com/docs/authorization), you can check if a user has a permission with Laravel's default `can` function: From aaf481a7412c93df5461e940cb91a8face30238b Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 22 Nov 2019 00:20:10 -0500 Subject: [PATCH 0140/1013] Update README.md --- README.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/README.md b/README.md index 6fc715743..c4355c8cc 100644 --- a/README.md +++ b/README.md @@ -40,20 +40,6 @@ $user->can('edit articles'); See the [documentation site](https://docs.spatie.be/laravel-permission/v3/introduction/) for detailed installation and usage instructions. -## Need a UI? - -The package doesn't come with any screens out of the box, you should build that yourself. Here are some options to get you started: - -- [Laravel Nova package by @vyuldashev for managing Roles and Permissions](https://github.com/vyuldashev/nova-permission) - -- [Laravel Nova package by @paras-malhotra for managing Roles and Permissions and permissions based authorization for Nova Resources](https://github.com/insenseanalytics/laravel-nova-permission) - -- [Extensive tutorial for building permissions UI](https://scotch.io/tutorials/user-authorization-in-laravel-54-with-spatie-laravel-permission) by [Caleb Oki](http://www.caleboki.com/). - -- [How to create a UI for managing the permissions and roles](http://www.qcode.in/easy-roles-and-permissions-in-laravel-5-4/) - -- [Laravel User Management for managing users, roles, permissions, departments and authorization](https://github.com/Mekaeil/LaravelUserManagement) by [Mekaeil](https://github.com/Mekaeil) - ### Testing From 07a414230cfe5f89a00c675ecba822d0176599c2 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 22 Nov 2019 00:20:36 -0500 Subject: [PATCH 0141/1013] Create ui-options.md --- docs/advanced-usage/ui-options.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 docs/advanced-usage/ui-options.md diff --git a/docs/advanced-usage/ui-options.md b/docs/advanced-usage/ui-options.md new file mode 100644 index 000000000..c68cba827 --- /dev/null +++ b/docs/advanced-usage/ui-options.md @@ -0,0 +1,18 @@ +--- +title: UI Options +weight: 10 +--- + +## Need a UI? + +The package doesn't come with any screens out of the box, you should build that yourself. Here are some options to get you started: + +- [Laravel Nova package by @vyuldashev for managing Roles and Permissions](https://github.com/vyuldashev/nova-permission) + +- [Laravel Nova package by @paras-malhotra for managing Roles and Permissions and permissions based authorization for Nova Resources](https://github.com/insenseanalytics/laravel-nova-permission) + +- [Extensive tutorial for building permissions UI](https://scotch.io/tutorials/user-authorization-in-laravel-54-with-spatie-laravel-permission) by [Caleb Oki](http://www.caleboki.com/). + +- [How to create a UI for managing the permissions and roles](http://www.qcode.in/easy-roles-and-permissions-in-laravel-5-4/) + +- [Laravel User Management for managing users, roles, permissions, departments and authorization](https://github.com/Mekaeil/LaravelUserManagement) by [Mekaeil](https://github.com/Mekaeil) From d8442dea509a7b5f67b9a6a208450c9753daf2ca Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 22 Nov 2019 00:23:40 -0500 Subject: [PATCH 0142/1013] Update .styleci.yml --- .styleci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.styleci.yml b/.styleci.yml index 08589c868..5e3aeba5a 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -7,4 +7,5 @@ enabled: disabled: - self_accessor - short_list_syntax +- alpha_ordered_imports - single_class_element_per_statement From 3dbab1c2b2bf721d5ff25b6435ad3f2ff7d45e42 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 22 Nov 2019 00:26:59 -0500 Subject: [PATCH 0143/1013] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b73f3755..dc8f5be5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to `laravel-permission` will be documented in this file +## 3.3.0 - 2019-11-22 +- Remove duplicate and unreachable code +- Remove checks for older Laravel versions + ## 3.2.0 - 2019-10-16 - Implementation of optional guard check for hasRoles and hasAllRoles - See #1236 From 5507d28c1e58f15017c45800af593567284325cd Mon Sep 17 00:00:00 2001 From: Bagus Erlang Date: Mon, 2 Dec 2019 08:31:10 +0800 Subject: [PATCH 0144/1013] Update installation-laravel.md (#1291) --- docs/installation-laravel.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 0b72142a5..0d6ceaeea 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -29,13 +29,7 @@ php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvid If you're using UUIDs or GUIDs for your `User` models you can update the `create_permission_tables.php` migration and replace `$table->unsignedBigInteger($columnNames['model_morph_key'])` with `$table->uuid($columnNames['model_morph_key'])`. For consistency, you can also update the package configuration file to use the `model_uuid` column name instead of the default `model_id` column. -After the migration has been published you can create the role- and permission-tables by running the migrations: - -```bash -php artisan migrate -``` - -You can publish the config file with: +Then, publish the config file with: ```bash php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" --tag="config" @@ -171,3 +165,9 @@ return [ ], ]; ``` + +After the config and migration have been published and configured, you can create the role- and permission-tables by running the migrations: + +```bash +php artisan migrate +``` From 2f44eebd1080179168a1e9d904ea546cf5de21cb Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 1 Dec 2019 19:31:58 -0500 Subject: [PATCH 0145/1013] Update .travis.yml --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4dc196110..b5f5ee8d4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,8 +13,6 @@ matrix: include: - php: 7.2 env: COMPOSER_FLAGS="--prefer-lowest" - allow_failures: - - php: 7.4snapshot before_script: - travis_retry composer self-update From 1929e7bc37b448d2cc43a1ff1e940e05492be5d1 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 1 Dec 2019 19:51:12 -0500 Subject: [PATCH 0146/1013] Update installation-laravel.md --- docs/installation-laravel.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 0d6ceaeea..62d120e11 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -20,22 +20,24 @@ The service provider will automatically get registered. Or you may manually add ]; ``` -You should publish [the migration](https://github.com/spatie/laravel-permission/blob/master/database/migrations/create_permission_tables.php.stub) with: +You should publish [the migration](https://github.com/spatie/laravel-permission/blob/master/database/migrations/create_permission_tables.php.stub) and [the `config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) with: ```bash -php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" --tag="migrations" +php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" ``` If you're using UUIDs or GUIDs for your `User` models you can update the `create_permission_tables.php` migration and replace `$table->unsignedBigInteger($columnNames['model_morph_key'])` with `$table->uuid($columnNames['model_morph_key'])`. For consistency, you can also update the package configuration file to use the `model_uuid` column name instead of the default `model_id` column. -Then, publish the config file with: +After the config and migration have been published and configured, you can create the role- and permission-tables by running the migrations: ```bash -php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" --tag="config" +php artisan migrate ``` -When published, [the `config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) initially contains: +### Default config file contents + +When published, config file initially contains: ```php return [ @@ -165,9 +167,3 @@ return [ ], ]; ``` - -After the config and migration have been published and configured, you can create the role- and permission-tables by running the migrations: - -```bash -php artisan migrate -``` From c56c544f0aa9a6d971c23504a6e5901170d34ed4 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 5 Dec 2019 16:19:00 -0500 Subject: [PATCH 0147/1013] Update .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b5f5ee8d4..430c2454b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ cache: php: - 7.2 - 7.3 - - 7.4snapshot + - 7.4 matrix: include: From cec967a8dd806f4d1679074b20889737c40a3b74 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 15 Dec 2019 12:19:33 -0500 Subject: [PATCH 0148/1013] Update BladeTest.php Tidy names of tests --- tests/BladeTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/BladeTest.php b/tests/BladeTest.php index 75f3d154c..8558f5092 100644 --- a/tests/BladeTest.php +++ b/tests/BladeTest.php @@ -21,7 +21,7 @@ public function setUp(): void } /** @test */ - public function all_blade_directives_will_evaluate_falsly_when_there_is_nobody_logged_in() + public function all_blade_directives_will_evaluate_false_when_there_is_nobody_logged_in() { $permission = 'edit-articles'; $role = 'writer'; @@ -38,7 +38,7 @@ public function all_blade_directives_will_evaluate_falsly_when_there_is_nobody_l } /** @test */ - public function all_blade_directives_will_evaluate_falsy_when_somebody_without_roles_or_permissions_is_logged_in() + public function all_blade_directives_will_evaluate_false_when_somebody_without_roles_or_permissions_is_logged_in() { $permission = 'edit-articles'; $role = 'writer'; @@ -55,7 +55,7 @@ public function all_blade_directives_will_evaluate_falsy_when_somebody_without_r } /** @test */ - public function all_blade_directives_will_evaluate_falsy_when_somebody_with_another_guard_is_logged_in() + public function all_blade_directives_will_evaluate_false_when_somebody_with_another_guard_is_logged_in() { $permission = 'edit-articles'; $role = 'writer'; From 5e337fc6bfdc14a71e188ef3959a52a1f64274ed Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 15 Dec 2019 15:25:00 -0500 Subject: [PATCH 0149/1013] Update roles-vs-permissions.md --- docs/best-practices/roles-vs-permissions.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/best-practices/roles-vs-permissions.md b/docs/best-practices/roles-vs-permissions.md index f94a16c0e..50be7b117 100644 --- a/docs/best-practices/roles-vs-permissions.md +++ b/docs/best-practices/roles-vs-permissions.md @@ -6,3 +6,6 @@ weight: 1 It is generally best to code your app around `permissions` only. That way you can always use the native Laravel `@can` and `can()` directives everywhere in your app. Roles can still be used to group permissions for easy assignment, and you can still use the role-based helper methods if truly necessary. But most app-related logic can usually be best controlled using the `can` methods, which allows Laravel's Gate layer to do all the heavy lifting. + +eg: `users` have `roles`, and `roles` have `permissions`, and your app always checks for `permissions`, not `roles`. + From d0b15a60068818f97f40418d404b6b34da5060b0 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 15 Dec 2019 15:40:37 -0500 Subject: [PATCH 0150/1013] Add Prerequisites page --- docs/prerequisites.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 docs/prerequisites.md diff --git a/docs/prerequisites.md b/docs/prerequisites.md new file mode 100644 index 000000000..9956aff17 --- /dev/null +++ b/docs/prerequisites.md @@ -0,0 +1,26 @@ +--- +title: Prerequisites +weight: 3 +--- + +This package can be used in Laravel 5.8 or higher. + +This package uses Laravel's Gate layer to provide Authorization capabilities. +The Gate/authorization layer requires that your `User` model implement the `Illuminate\Contracts\Auth\Access\Authorizable` contract. +Otherwise the `can()` and `authorize()` methods will not work in your controllers, policies, templates, etc. + +In the `Installation` instructions you'll see that the `HasRoles` trait must be added to the User model to enable this package's features. + +Thus, a typical basic User model would have these basic minimum requirements: + +```php +use Illuminate\Foundation\Auth\User as Authenticatable; +use Spatie\Permission\Traits\HasRoles; + +class User extends Authenticatable +{ + use HasRoles; + + // ... +} +``` From 4a151e4a7cce2a2f2876c39ed705af607ec9645f Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 15 Dec 2019 15:44:34 -0500 Subject: [PATCH 0151/1013] Update prerequisites.md --- docs/prerequisites.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/prerequisites.md b/docs/prerequisites.md index 9956aff17..06cd3ab2a 100644 --- a/docs/prerequisites.md +++ b/docs/prerequisites.md @@ -24,3 +24,5 @@ class User extends Authenticatable // ... } ``` + +Additionally, your `User` object MUST NOT have a `role` or `roles` property (or field in the database), nor a `roles()` method on it. Those will interfere with the properties and methods added by the `HasRoles` trait provided by this package, thus causing unexpected outcomes when this package's methods are used to inspect roles and permissions. From c634afb0226468dda6eda5ac9d275b5fb53fcc5e Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 15 Dec 2019 15:44:53 -0500 Subject: [PATCH 0152/1013] Update prerequisites.md --- docs/prerequisites.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/prerequisites.md b/docs/prerequisites.md index 06cd3ab2a..58422edd4 100644 --- a/docs/prerequisites.md +++ b/docs/prerequisites.md @@ -25,4 +25,4 @@ class User extends Authenticatable } ``` -Additionally, your `User` object MUST NOT have a `role` or `roles` property (or field in the database), nor a `roles()` method on it. Those will interfere with the properties and methods added by the `HasRoles` trait provided by this package, thus causing unexpected outcomes when this package's methods are used to inspect roles and permissions. +Additionally, your `User` model/object MUST NOT have a `role` or `roles` property (or field in the database), nor a `roles()` method on it. Those will interfere with the properties and methods added by the `HasRoles` trait provided by this package, thus causing unexpected outcomes when this package's methods are used to inspect roles and permissions. From 5e8fbcaa34a6c58c73380e85522f6ecfbf852df0 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 18 Dec 2019 22:13:51 -0500 Subject: [PATCH 0153/1013] Update prerequisites.md --- docs/prerequisites.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/prerequisites.md b/docs/prerequisites.md index 58422edd4..943929bf1 100644 --- a/docs/prerequisites.md +++ b/docs/prerequisites.md @@ -26,3 +26,5 @@ class User extends Authenticatable ``` Additionally, your `User` model/object MUST NOT have a `role` or `roles` property (or field in the database), nor a `roles()` method on it. Those will interfere with the properties and methods added by the `HasRoles` trait provided by this package, thus causing unexpected outcomes when this package's methods are used to inspect roles and permissions. + +Similarly, your `User` model/object MUST NOT have a `permission` or `permissions` property (or field in the database), nor a `permissions()` method on it. Those will interfere with the properties and methods added by the `HasPermissions` trait provided by this package (which is invoked via the `HasRoles` trait). From e44dfce7ab1019a8046dcb79abb6ca9bc355455c Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 18 Dec 2019 22:18:58 -0500 Subject: [PATCH 0154/1013] Update extending.md --- docs/advanced-usage/extending.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/advanced-usage/extending.md b/docs/advanced-usage/extending.md index dbda3f9b6..986a86b6e 100644 --- a/docs/advanced-usage/extending.md +++ b/docs/advanced-usage/extending.md @@ -4,13 +4,14 @@ weight: 3 --- ## Extending User Models -Laravel's authorization features are available in models which implement the `Illuminate\Foundation\Auth\Access\Authorizable` trait. By default Laravel does this in `\App\User` by extending `Illuminate\Foundation\Auth\User`, in which the trait and contracts are declared. +Laravel's authorization features are available in models which implement the `Illuminate\Foundation\Auth\Access\Authorizable` trait. By default Laravel does this in `\App\User` by extending `Illuminate\Foundation\Auth\User`, in which the trait and `Illuminate\Contracts\Auth\Access\Authorizable` contract are declared. -If you are creating your own, or additional, User models and wish Authorization features to be available on it, including the Roles and Permissions features of this package, you need to implement `Illuminate\Foundation\Auth\Access\Authorizable` in one of those ways as well. +If you are creating your own User models and wish Authorization features to be available, you need to implement `Illuminate\Contracts\Auth\Access\Authorizable` in one of those ways as well. ## Extending Role and Permission Models -If you are extending or replacing the role/permission models, you will need to specify your new models in the configuration. +If you are extending or replacing the role/permission models, you will need to specify your new models in this package's `config/permissions.php` file. + First be sure that you've published the configuration file (see the Installation instructions), and edit it to update the `models.role` and `models.permission` values to point to your new models. Note the following requirements when extending/replacing the models: @@ -23,8 +24,7 @@ If you need to EXTEND the existing `Role` or `Permission` models note that: - Your `Permission` model needs to extend the `Spatie\Permission\Models\Permission` model ### Replacing -If you need to REPLACE the existing `Role` or `Permission` models you need to keep the -following things in mind: +If you need to REPLACE the existing `Role` or `Permission` models you need to keep the following things in mind: - Your `Role` model needs to implement the `Spatie\Permission\Contracts\Role` contract - Your `Permission` model needs to implement the `Spatie\Permission\Contracts\Permission` contract From 6b1aedd0f25dfbca19c174b9f03bf2e69d9eeb28 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 24 Dec 2019 12:19:52 -0500 Subject: [PATCH 0155/1013] Expose Artisan commands to app layer, not just to console Fixes #1029 --- src/PermissionServiceProvider.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 3bb901634..2a13ab5bf 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -26,14 +26,12 @@ public function boot(PermissionRegistrar $permissionLoader, Filesystem $filesyst $this->registerMacroHelpers(); } - if ($this->app->runningInConsole()) { - $this->commands([ - Commands\CacheReset::class, - Commands\CreateRole::class, - Commands\CreatePermission::class, - Commands\Show::class, - ]); - } + $this->commands([ + Commands\CacheReset::class, + Commands\CreateRole::class, + Commands\CreatePermission::class, + Commands\Show::class, + ]); $this->registerModelBindings(); From f43f9fad45f2a1830b40645a651027a0324e2de4 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 24 Dec 2019 12:30:12 -0500 Subject: [PATCH 0156/1013] Add clarity to the Artisan docs --- docs/basic-usage/artisan.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/basic-usage/artisan.md b/docs/basic-usage/artisan.md index 53dec0814..b8c98dd53 100644 --- a/docs/basic-usage/artisan.md +++ b/docs/basic-usage/artisan.md @@ -3,7 +3,9 @@ title: Using artisan commands weight: 7 --- -You can create a role or permission from a console with artisan commands. +## Creating roles and permissions with Artisan Commands + +You can create a role or permission from the console with artisan commands. ```bash php artisan permission:create-role writer @@ -28,3 +30,25 @@ When creating roles you can also create and link permissions at the same time: ```bash php artisan permission:create-role writer web "create articles|edit articles" ``` + +## Displaying roles and permissions in the console + +There is also a `show` command to show a table of roles and permissions per guard: + +```bash +php artisan permission:show +``` + +## Resetting the Cache + +When you use the built-in functions for manipulating roles and permissions, the cache is automatically reset for you, and relations are automatically reloaded for the current model record. + +See the Advanced-Usage/Cache section of these docs for detailed specifics. + +If you need to manually reset the cache for this package, you may use the following artisan command: + +```bash +php artisan permission:cache-reset +``` + +Again, it is more efficient to use the API provided by this package, instead of manually clearing the cache. From 647c514ac5cbcd319ded1e8585d9802576876beb Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 24 Dec 2019 13:49:35 -0500 Subject: [PATCH 0157/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc8f5be5d..912b5db3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 3.3.1 - 2019-12-24 +- Expose Artisan commands to app layer, not just to console + ## 3.3.0 - 2019-11-22 - Remove duplicate and unreachable code - Remove checks for older Laravel versions From 2b66388c6d1a8668049f015e0e19a8fcbd63c81b Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 24 Dec 2019 14:31:33 -0500 Subject: [PATCH 0158/1013] Update introduction.md --- docs/introduction.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/introduction.md b/docs/introduction.md index eaca3636d..c1bed6dc2 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -24,3 +24,11 @@ Because all permissions will be registered on [Laravel's gate](https://laravel.c ```php $user->can('edit articles'); ``` + +and Blade directives: + +```blade +@can('edit articles') +... +@endcan +``` \ No newline at end of file From 2ba3ad60c5e6fc391ff195cbffde35bb7c2cd4dd Mon Sep 17 00:00:00 2001 From: Jochen Sengier Date: Thu, 26 Dec 2019 13:37:51 +0100 Subject: [PATCH 0159/1013] Fix comma dangle --- database/migrations/create_permission_tables.php.stub | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/database/migrations/create_permission_tables.php.stub b/database/migrations/create_permission_tables.php.stub index f6c189eb6..1f8507468 100644 --- a/database/migrations/create_permission_tables.php.stub +++ b/database/migrations/create_permission_tables.php.stub @@ -35,7 +35,7 @@ class CreatePermissionTables extends Migration $table->string('model_type'); $table->unsignedBigInteger($columnNames['model_morph_key']); - $table->index([$columnNames['model_morph_key'], 'model_type', ], 'model_has_permissions_model_id_model_type_index'); + $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index'); $table->foreign('permission_id') ->references('id') @@ -51,7 +51,7 @@ class CreatePermissionTables extends Migration $table->string('model_type'); $table->unsignedBigInteger($columnNames['model_morph_key']); - $table->index([$columnNames['model_morph_key'], 'model_type', ], 'model_has_roles_model_id_model_type_index'); + $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index'); $table->foreign('role_id') ->references('id') From 5f0429b64e22b2e1fea3fbfb45ccfc2a92504a5d Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 27 Dec 2019 11:52:58 -0500 Subject: [PATCH 0160/1013] Swoole compatibility Fixes #981 --- src/PermissionRegistrar.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 4cff1a492..27b544a48 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -11,9 +11,6 @@ class PermissionRegistrar { - /** @var \Illuminate\Contracts\Auth\Access\Gate */ - protected $gate; - /** @var \Illuminate\Contracts\Cache\Repository */ protected $cache; @@ -41,12 +38,10 @@ class PermissionRegistrar /** * PermissionRegistrar constructor. * - * @param \Illuminate\Contracts\Auth\Access\Gate $gate * @param \Illuminate\Cache\CacheManager $cacheManager */ - public function __construct(Gate $gate, CacheManager $cacheManager) + public function __construct(CacheManager $cacheManager) { - $this->gate = $gate; $this->permissionClass = config('permission.models.permission'); $this->roleClass = config('permission.models.role'); @@ -84,12 +79,13 @@ protected function getCacheStoreFromConfig(): \Illuminate\Contracts\Cache\Reposi /** * Register the permission check method on the gate. + * We resolve the Gate fresh here, for benefit of long-running instances. * * @return bool */ public function registerPermissions(): bool { - $this->gate->before(function (Authorizable $user, string $ability) { + resolve(Gate::class)->before(function (Authorizable $user, string $ability) { if (method_exists($user, 'checkPermissionTo')) { return $user->checkPermissionTo($ability) ?: null; } From fa18db53513db2b4ea72e3eeda964853afc8dac9 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 27 Dec 2019 14:05:39 -0500 Subject: [PATCH 0161/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 912b5db3c..d14453315 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 3.4.0 - 2019-12-27 +- Make compatible with Swoole - ie: for long-running Laravel instances + ## 3.3.1 - 2019-12-24 - Expose Artisan commands to app layer, not just to console From cbdca3c1be8461fa6f411dca68638c452b1062ba Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 28 Dec 2019 08:21:06 -0500 Subject: [PATCH 0162/1013] Fix for Lumen --- src/PermissionRegistrar.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 27b544a48..66aa4e978 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -85,7 +85,7 @@ protected function getCacheStoreFromConfig(): \Illuminate\Contracts\Cache\Reposi */ public function registerPermissions(): bool { - resolve(Gate::class)->before(function (Authorizable $user, string $ability) { + app(Gate::class)->before(function (Authorizable $user, string $ability) { if (method_exists($user, 'checkPermissionTo')) { return $user->checkPermissionTo($ability) ?: null; } From fe0573ddb6fd95378eec67e401882564db830312 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 28 Dec 2019 08:22:00 -0500 Subject: [PATCH 0163/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d14453315..4e46db844 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 3.4.1 - 2019-12-28 +- Fix 3.4.0 for Lumen + ## 3.4.0 - 2019-12-27 - Make compatible with Swoole - ie: for long-running Laravel instances From 7d9a8fb8030f62148360e7f758418325871a9803 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 28 Dec 2019 17:33:47 -0500 Subject: [PATCH 0164/1013] Update installation-lumen.md --- docs/installation-lumen.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/installation-lumen.md b/docs/installation-lumen.md index 4f148fb45..928e939d7 100644 --- a/docs/installation-lumen.md +++ b/docs/installation-lumen.md @@ -19,13 +19,13 @@ cp vendor/spatie/laravel-permission/config/permission.php config/permission.php cp vendor/spatie/laravel-permission/database/migrations/create_permission_tables.php.stub database/migrations/2018_01_01_000000_create_permission_tables.php ``` -You will also need to create another configuration file at `config/auth.php`. Get it on the Laravel repository or just run the following command: +You will also need the `config/auth.php` file. If you don't already have it, copy it from the vendor folder: ```bash -curl -Ls https://raw.githubusercontent.com/laravel/lumen-framework/5.7/config/auth.php -o config/auth.php +cp vendor/laravel/lumen-framework/config/auth.php config/auth.php ``` -Then, in `bootstrap/app.php`, register the middlewares: +Then, in `bootstrap/app.php`, uncomment the `auth` middleware, and register this package's middleware: ```php $app->routeMiddleware([ @@ -35,7 +35,7 @@ $app->routeMiddleware([ ]); ``` -Also register the config file, service provider, and cache alias: +Also in `bootstrap/app.php` register the config file, service provider, and cache alias: ```php $app->configure('permission'); @@ -43,11 +43,19 @@ $app->alias('cache', \Illuminate\Cache\CacheManager::class); // if you don't ha $app->register(Spatie\Permission\PermissionServiceProvider::class); ``` -Now, run your migrations: +If you are using guards you will need to uncomment the AuthServiceProvider line: +```php +$app->register(App\Providers\AuthServiceProvider::class); +``` + +Now, ensure your database configuration is set. + +Then run the migrations: ```bash php artisan migrate ``` -Remember that Laravel's authorization layer requires that your `User` model implement the `Illuminate\Contracts\Auth\Access\Authorizable` contract. In Lumen you will then also need to use the `Laravel\Lumen\Auth\Authorizable` trait. + +NOTE: Remember that Laravel's authorization layer requires that your `User` model implement the `Illuminate\Contracts\Auth\Access\Authorizable` contract. In Lumen you will then also need to use the `Laravel\Lumen\Auth\Authorizable` trait. From 22eb0eacd9c989ae94620e1dbfb067065e0069cf Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 28 Dec 2019 19:01:14 -0500 Subject: [PATCH 0165/1013] Update TravisCI --- .travis.yml | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 430c2454b..33c9e0407 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,19 +4,29 @@ cache: directories: - $HOME/.composer/cache/files -php: - - 7.2 - - 7.3 - - 7.4 - matrix: include: - - php: 7.2 - env: COMPOSER_FLAGS="--prefer-lowest" + - php: 7.2 + env: FRAMEWORK_VERSION=laravel/framework:5.8.* COMPOSER_FLAGS="--prefer-lowest" + - php: 7.2 + env: FRAMEWORK_VERSION=laravel/framework:6.* + - php: 7.3 + env: FRAMEWORK_VERSION=laravel/framework:6.* + - php: 7.4 + env: FRAMEWORK_VERSION=laravel/framework:6.* + - php: 7.2 + env: FRAMEWORK_VERSION=laravel/lumen-framework:5.8.* COMPOSER_FLAGS="--prefer-lowest" + - php: 7.3 + env: FRAMEWORK_VERSION=laravel/lumen-framework:6.* + - php: 7.4 + env: FRAMEWORK_VERSION=laravel/lumen-framework:6.* + +before_install: + - phpenv config-rm xdebug.ini || true before_script: - - travis_retry composer self-update - - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction + - composer require "${FRAMEWORK_VERSION}" --no-update + - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --no-suggest --optimize-autoloader script: - - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover + - vendor/bin/phpunit From e424470b8906fa68cc4abdde3b4eda6ed0fa56bc Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 28 Dec 2019 19:50:24 -0500 Subject: [PATCH 0166/1013] Github Workflow --- .github/workflows/run-tests.yml | 46 +++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/run-tests.yml diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 000000000..5eea95ee9 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,46 @@ +name: "Run Tests" + +on: [push] + +jobs: + test: + + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + php: [7.2, 7.3, 7.4] + laravel: [5.8.*, 6.*] + dependency-version: [prefer-lowest, prefer-stable] + include: + - laravel: 6.* + testbench: 4.* + - laravel: 5.8.* + testbench: 3.8.* + + name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} + + steps: + - name: Checkout code + uses: actions/checkout@v1 + + - name: Cache dependencies + uses: actions/cache@v1 + with: + path: ~/.composer/cache/files + key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} + + - name: Setup PHP + uses: shivammathur/setup-php@v1 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick + coverage: none + + - name: Install dependencies + run: | + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:^4.3.4" --no-interaction --no-update + composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest + + - name: Execute tests + run: vendor/bin/phpunit From e967296a4c8d3f7e2f0c00d8f98ed607ba6d0808 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Mon, 6 Jan 2020 19:30:53 +0100 Subject: [PATCH 0167/1013] Delete .travis.yml --- .travis.yml | 32 -------------------------------- 1 file changed, 32 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 33c9e0407..000000000 --- a/.travis.yml +++ /dev/null @@ -1,32 +0,0 @@ -language: php - -cache: - directories: - - $HOME/.composer/cache/files - -matrix: - include: - - php: 7.2 - env: FRAMEWORK_VERSION=laravel/framework:5.8.* COMPOSER_FLAGS="--prefer-lowest" - - php: 7.2 - env: FRAMEWORK_VERSION=laravel/framework:6.* - - php: 7.3 - env: FRAMEWORK_VERSION=laravel/framework:6.* - - php: 7.4 - env: FRAMEWORK_VERSION=laravel/framework:6.* - - php: 7.2 - env: FRAMEWORK_VERSION=laravel/lumen-framework:5.8.* COMPOSER_FLAGS="--prefer-lowest" - - php: 7.3 - env: FRAMEWORK_VERSION=laravel/lumen-framework:6.* - - php: 7.4 - env: FRAMEWORK_VERSION=laravel/lumen-framework:6.* - -before_install: - - phpenv config-rm xdebug.ini || true - -before_script: - - composer require "${FRAMEWORK_VERSION}" --no-update - - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --no-suggest --optimize-autoloader - -script: - - vendor/bin/phpunit From cd96b4cd7801650f532d38a1c0ae4a1b8c9e7938 Mon Sep 17 00:00:00 2001 From: musapinar Date: Mon, 6 Jan 2020 23:29:52 +0100 Subject: [PATCH 0168/1013] Added missing `guardName` to Exception `PermissionDoesNotExist::withId()` https://github.com/spatie/laravel-permission/blob/master/src/Models/Permission.php#L110 --- src/Exceptions/PermissionDoesNotExist.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Exceptions/PermissionDoesNotExist.php b/src/Exceptions/PermissionDoesNotExist.php index 305b61906..e6d7c101e 100644 --- a/src/Exceptions/PermissionDoesNotExist.php +++ b/src/Exceptions/PermissionDoesNotExist.php @@ -11,8 +11,8 @@ public static function create(string $permissionName, string $guardName = '') return new static("There is no permission named `{$permissionName}` for guard `{$guardName}`."); } - public static function withId(int $permissionId) + public static function withId(int $permissionId, string $guardName = '') { - return new static("There is no [permission] with id `{$permissionId}`."); + return new static("There is no [permission] with id `{$permissionId}` for guard `{$guardName}`."); } } From e9c08249ec3c24258531d0f967bd3d5559e0f79e Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 7 Jan 2020 11:34:21 -0500 Subject: [PATCH 0169/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e46db844..0b26f2c3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 3.5.0 - 2020-01-07 +- Added missing `guardName` to Exception `PermissionDoesNotExist` #1316 + ## 3.4.1 - 2019-12-28 - Fix 3.4.0 for Lumen From 4da7c7945f51143de4883c590d349ebbb4b2e9b9 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Tue, 7 Jan 2020 16:34:29 +0000 Subject: [PATCH 0170/1013] Apply fixes from StyleCI --- src/Guard.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Guard.php b/src/Guard.php index a99265337..48ac56b3c 100644 --- a/src/Guard.php +++ b/src/Guard.php @@ -12,7 +12,7 @@ class Guard * @param $model * @return Collection */ - public static function getNames($model) : Collection + public static function getNames($model): Collection { if (is_object($model)) { $guardName = $model->guard_name ?? null; From a5f9d2b50a5bde5d275bbe5fb0d3d86664880a24 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 7 Jan 2020 20:34:29 -0500 Subject: [PATCH 0171/1013] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c4355c8cc..3eeab0cb0 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-permission.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-permission) -[![Build Status](https://img.shields.io/travis/spatie/laravel-permission/master.svg?style=flat-square)](https://travis-ci.org/spatie/laravel-permission) +![](https://github.com/spatie/laravel-permission/workflows/Run%20Tests/badge.svg?branch=master) [![StyleCI](https://styleci.io/repos/42480275/shield)](https://styleci.io/repos/42480275) [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-permission.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-permission) From dd258ce099cf082034b4e834fec26c1b48a472de Mon Sep 17 00:00:00 2001 From: musapinar Date: Mon, 13 Jan 2020 02:12:35 +0100 Subject: [PATCH 0172/1013] fixed PHPDoc fixed PHPDoc for method `hasAllRoles` in trait `HasRoles` : added `array` as accepted type for first parameter `$roles` --- src/Traits/HasRoles.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index f50f3810d..b779fa61f 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -226,7 +226,7 @@ public function hasAnyRole($roles): bool /** * Determine if the model has all of the given role(s). * - * @param string|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles + * @param string|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles * @param string|null $guard * @return bool */ From bb3b846d86f00cc0c688865528abc2466a90d0cb Mon Sep 17 00:00:00 2001 From: musapinar Date: Tue, 14 Jan 2020 03:41:40 +0100 Subject: [PATCH 0173/1013] Update CacheTest.php Fixed random test failure adding ```->sort()->values()``` to ```$actual``` Collection in ```get_all_permissions_should_use_the_cache()``` https://i.imgur.com/MoS3erH.png --- tests/CacheTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CacheTest.php b/tests/CacheTest.php index 58c2c0819..9256e6c45 100644 --- a/tests/CacheTest.php +++ b/tests/CacheTest.php @@ -196,7 +196,7 @@ public function get_all_permissions_should_use_the_cache() $this->assertQueryCount($this->cache_init_count + $this->cache_load_count + $this->cache_run_count); $this->resetQueryCount(); - $actual = $this->testUser->getAllPermissions()->pluck('name'); + $actual = $this->testUser->getAllPermissions()->pluck('name')->sort()->values(); $this->assertEquals($actual, collect($expected)); $this->assertQueryCount(2); From e412bb2665f240bc4ce502b4e4d0018f1059b69c Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 14 Jan 2020 10:48:18 -0500 Subject: [PATCH 0174/1013] Add support for Laravel 7 --- composer.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 3afb155f2..796562f25 100644 --- a/composer.json +++ b/composer.json @@ -23,13 +23,13 @@ ], "require": { "php" : "^7.2", - "illuminate/auth": "^5.8|^6.0", - "illuminate/container": "^5.8|^6.0", - "illuminate/contracts": "^5.8|^6.0", - "illuminate/database": "^5.8|^6.0" + "illuminate/auth": "^5.8|^6.0|^7.0", + "illuminate/container": "^5.8|^6.0|^7.0", + "illuminate/contracts": "^5.8|^6.0|^7.0", + "illuminate/database": "^5.8|^6.0|^7.0" }, "require-dev": { - "orchestra/testbench": "^3.8|^4.0", + "orchestra/testbench": "^3.8|^4.0|^5.0", "phpunit/phpunit": "^8.0", "predis/predis": "^1.1" }, From 6b1bc04a50768cc629e87cf4a0482e9bc4139bb3 Mon Sep 17 00:00:00 2001 From: musapinar Date: Wed, 15 Jan 2020 01:54:33 +0100 Subject: [PATCH 0175/1013] Update HasPermissionsTest.php Fixed random test failure adding ```->sort()->values()``` to ```pluck()``` Exact same issue as https://github.com/spatie/laravel-permission/pull/1324 --- tests/HasPermissionsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index f75fa626b..416d112c4 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -376,7 +376,7 @@ public function it_can_list_all_the_coupled_permissions_both_directly_and_via_ro $this->assertEquals( collect(['edit-articles', 'edit-news']), - $this->testUser->getAllPermissions()->pluck('name') + $this->testUser->getAllPermissions()->pluck('name')->sort()->values() ); } From 274ee668d3462980c520a40c8e46d5f3155c8649 Mon Sep 17 00:00:00 2001 From: musapinar Date: Wed, 15 Jan 2020 22:47:56 +0100 Subject: [PATCH 0176/1013] Updates HasRoles.php Added splat operator to ```hasAnyRole()``` Fixes the following : https://github.com/spatie/laravel-permission/blob/master/tests/HasRolesTest.php#L492 --- src/Traits/HasRoles.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index f50f3810d..e261069f5 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -218,7 +218,7 @@ public function hasRole($roles, string $guard = null): bool * * @return bool */ - public function hasAnyRole($roles): bool + public function hasAnyRole(...$roles): bool { return $this->hasRole($roles); } From 256095f3d07cd61033ef5cb44516c38694de2150 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 17 Jan 2020 10:58:48 -0500 Subject: [PATCH 0177/1013] Update HasRoles.php --- src/Traits/HasRoles.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 8a81347ab..313340c93 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -213,8 +213,10 @@ public function hasRole($roles, string $guard = null): bool /** * Determine if the model has any of the given role(s). + * + * Alias to hasRole() but without Guard controls * - * @param string|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles + * @param string|int|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles * * @return bool */ From 74f626d9d8e4888fffb62f24ad10f250f7fa3aa5 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 17 Jan 2020 11:00:43 -0500 Subject: [PATCH 0178/1013] Add splat-operator tests for hasAnyRole Contributed by @musapinar --- tests/HasRolesTest.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 7f90f8e19..761f46574 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -492,6 +492,20 @@ public function it_can_determine_that_a_user_does_not_have_a_role_from_another_g $this->assertFalse($this->testUser->hasAnyRole('testAdminRole', $this->testAdminRole)); } + /** @test */ + public function it_can_check_against_any_multiple_roles_using_multiple_arguments() + { + $this->testUser->assignRole('testRole'); + + $this->assertTrue($this->testUser->hasAnyRole($this->testAdminRole, ['testRole'], 'This Role Does Not Even Exist')); + } + + /** @test */ + public function it_returns_false_instead_of_an_exception_when_checking_against_any_undefined_roles_using_multiple_arguments() + { + $this->assertFalse($this->testUser->hasAnyRole('This Role Does Not Even Exist', $this->testAdminRole)); + } + /** @test */ public function it_can_retrieve_role_names() { From e7f21fd43ec96cd1ef6c85085d54536ad866a93c Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Fri, 17 Jan 2020 16:00:50 +0000 Subject: [PATCH 0179/1013] Apply fixes from StyleCI --- src/Traits/HasRoles.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 313340c93..0ffe1c7c5 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -213,7 +213,7 @@ public function hasRole($roles, string $guard = null): bool /** * Determine if the model has any of the given role(s). - * + * * Alias to hasRole() but without Guard controls * * @param string|int|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles From a89ebcc77dff6b9e81de8fd82d435a4ea52d8d85 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 17 Jan 2020 11:13:26 -0500 Subject: [PATCH 0180/1013] Update role-permissions.md --- docs/basic-usage/role-permissions.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/basic-usage/role-permissions.md b/docs/basic-usage/role-permissions.md index 08511bd49..e59525d1b 100644 --- a/docs/basic-usage/role-permissions.md +++ b/docs/basic-usage/role-permissions.md @@ -31,12 +31,17 @@ You can determine if a user has a certain role: ```php $user->hasRole('writer'); + +// or at least one role from an array of roles: +$user->hasRole(['editor', 'moderator']); ``` You can also determine if a user has any of a given list of roles: ```php $user->hasAnyRole(['writer', 'reader']); +// or +$user->hasAnyRole('writer', 'reader'); ``` You can also determine if a user has all of a given list of roles: From 0a063ce206c7545737d19c1beda12003fc66e4e9 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 17 Jan 2020 11:18:51 -0500 Subject: [PATCH 0181/1013] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b26f2c3f..e8cd759dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to `laravel-permission` will be documented in this file +## 3.6.0 - 2020-01-17 +- Added Laravel 7.0 support +- Allow splat operator for passing roles to `hasAnyRole()` + ## 3.5.0 - 2020-01-07 - Added missing `guardName` to Exception `PermissionDoesNotExist` #1316 From e627001d281a11a362c5639b1e1e7ed70bbf80f4 Mon Sep 17 00:00:00 2001 From: musapinar Date: Fri, 17 Jan 2020 20:36:37 +0100 Subject: [PATCH 0182/1013] Update ui-options.md Removed a link to a tutorial full of outdated (if not garbage) code. https://i.imgur.com/Od231gI.png --- docs/advanced-usage/ui-options.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/advanced-usage/ui-options.md b/docs/advanced-usage/ui-options.md index c68cba827..9a157f506 100644 --- a/docs/advanced-usage/ui-options.md +++ b/docs/advanced-usage/ui-options.md @@ -11,8 +11,6 @@ The package doesn't come with any screens out of the box, you should build that - [Laravel Nova package by @paras-malhotra for managing Roles and Permissions and permissions based authorization for Nova Resources](https://github.com/insenseanalytics/laravel-nova-permission) -- [Extensive tutorial for building permissions UI](https://scotch.io/tutorials/user-authorization-in-laravel-54-with-spatie-laravel-permission) by [Caleb Oki](http://www.caleboki.com/). - - [How to create a UI for managing the permissions and roles](http://www.qcode.in/easy-roles-and-permissions-in-laravel-5-4/) - [Laravel User Management for managing users, roles, permissions, departments and authorization](https://github.com/Mekaeil/LaravelUserManagement) by [Mekaeil](https://github.com/Mekaeil) From 56d94b38f79ceefd0ec4cd2c243790e2027df001 Mon Sep 17 00:00:00 2001 From: musapinar Date: Fri, 17 Jan 2020 23:55:50 +0100 Subject: [PATCH 0183/1013] Updated HasPermissions.php - Removed ```@throws \Exception``` in PHPDoc because IDE was complaining about unhandled Exception when using ```getAllPermissions()```, while in the worst case scenario, as far as I can tell, it will just return an empty Collection. - Added IDE helper for ```$permissions``` --- src/Traits/HasPermissions.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 143fb1847..2e6538fa7 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -265,11 +265,10 @@ public function getPermissionsViaRoles(): Collection /** * Return all the permissions the model has, both directly and via roles. - * - * @throws \Exception */ public function getAllPermissions(): Collection { + /** @var Collection $permissions */ $permissions = $this->permissions; if ($this->roles) { From 757706df8604814be5d9a764fe64c0f0fedd43b1 Mon Sep 17 00:00:00 2001 From: musapinar Date: Sat, 18 Jan 2020 09:31:11 +0100 Subject: [PATCH 0184/1013] Updated `scopeRole()` in trait `HasRoles.php` - faster/prettier than multiple ```orWhere``` --- src/Traits/HasRoles.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 0ffe1c7c5..4fbc64e25 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -78,12 +78,8 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder return $this->getRoleClass()->{$method}($role, $guard); }, $roles); - return $query->whereHas('roles', function ($query) use ($roles) { - $query->where(function ($query) use ($roles) { - foreach ($roles as $role) { - $query->orWhere(config('permission.table_names.roles').'.id', $role->id); - } - }); + return $query->whereHas('roles', function (Builder $subQuery) use ($roles) { + $subQuery->whereIn(config('permission.table_names.roles') . '.id', \array_column($roles, 'id')); }); } From 3a0f165bc4ce9dfbecf4c57757a3e2cba7666b61 Mon Sep 17 00:00:00 2001 From: musapinar Date: Sat, 18 Jan 2020 09:41:22 +0100 Subject: [PATCH 0185/1013] Updated `HasPermissions.php` Updated `scopePermission()` in trait `HasPermissions.php` - faster/prettier than multiple orWhere --- src/Traits/HasPermissions.php | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 2e6538fa7..826330564 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -65,21 +65,13 @@ public function scopePermission(Builder $query, $permissions): Builder return array_merge($result, $permission->roles->all()); }, [])); - return $query->where(function ($query) use ($permissions, $rolesWithPermissions) { - $query->whereHas('permissions', function ($query) use ($permissions) { - $query->where(function ($query) use ($permissions) { - foreach ($permissions as $permission) { - $query->orWhere(config('permission.table_names.permissions').'.id', $permission->id); - } - }); + return $query->where(function (Builder $query) use ($permissions, $rolesWithPermissions) { + $query->whereHas('permissions', function (Builder $subQuery) use ($permissions) { + $subQuery->whereIn(config('permission.table_names.permissions') . '.id', \array_column($permissions, 'id')); }); if (count($rolesWithPermissions) > 0) { - $query->orWhereHas('roles', function ($query) use ($rolesWithPermissions) { - $query->where(function ($query) use ($rolesWithPermissions) { - foreach ($rolesWithPermissions as $role) { - $query->orWhere(config('permission.table_names.roles').'.id', $role->id); - } - }); + $query->orWhereHas('roles', function (Builder $subQuery) use ($rolesWithPermissions) { + $subQuery->whereIn(config('permission.table_names.roles') . '.id', \array_column($rolesWithPermissions, 'id')); }); } }); From 1c8bc85088f33c76bd1ae8aa97891dcd02c79af9 Mon Sep 17 00:00:00 2001 From: musapinar Date: Sat, 18 Jan 2020 10:23:24 +0100 Subject: [PATCH 0186/1013] Update HasRoles.php --- src/Traits/HasRoles.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 4fbc64e25..cced46cf1 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -79,7 +79,7 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder }, $roles); return $query->whereHas('roles', function (Builder $subQuery) use ($roles) { - $subQuery->whereIn(config('permission.table_names.roles') . '.id', \array_column($roles, 'id')); + $subQuery->whereIn(config('permission.table_names.roles').'.id', \array_column($roles, 'id')); }); } From d8a216ad3465bc4f0840ec56d82e3c0d13744bce Mon Sep 17 00:00:00 2001 From: musapinar Date: Sat, 18 Jan 2020 10:24:18 +0100 Subject: [PATCH 0187/1013] Update HasPermissions.php --- src/Traits/HasPermissions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 826330564..7243c800c 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -67,11 +67,11 @@ public function scopePermission(Builder $query, $permissions): Builder return $query->where(function (Builder $query) use ($permissions, $rolesWithPermissions) { $query->whereHas('permissions', function (Builder $subQuery) use ($permissions) { - $subQuery->whereIn(config('permission.table_names.permissions') . '.id', \array_column($permissions, 'id')); + $subQuery->whereIn(config('permission.table_names.permissions').'.id', \array_column($permissions, 'id')); }); if (count($rolesWithPermissions) > 0) { $query->orWhereHas('roles', function (Builder $subQuery) use ($rolesWithPermissions) { - $subQuery->whereIn(config('permission.table_names.roles') . '.id', \array_column($rolesWithPermissions, 'id')); + $subQuery->whereIn(config('permission.table_names.roles').'.id', \array_column($rolesWithPermissions, 'id')); }); } }); From 22f1ecdd8d4f3c8976363068308e4463be5eb181 Mon Sep 17 00:00:00 2001 From: musapinar Date: Tue, 21 Jan 2020 03:21:26 +0100 Subject: [PATCH 0188/1013] Update HasPermissions.php If accepted, proposition should be enforced with a test such as ```$this->assertFalse($this->testUser->hasAllPermissions(['edit-articles', 'edit-news'], 'edit-blog'));``` with User having permission for `edit-article` and `edit-news` but no permission for `edit-blog` --- src/Traits/HasPermissions.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 2e6538fa7..485a04b75 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -177,9 +177,7 @@ public function checkPermissionTo($permission, $guardName = null): bool */ public function hasAnyPermission(...$permissions): bool { - if (is_array($permissions[0])) { - $permissions = $permissions[0]; - } + $permissions = collect($permissions)->flatten(); foreach ($permissions as $permission) { if ($this->checkPermissionTo($permission)) { @@ -200,9 +198,7 @@ public function hasAnyPermission(...$permissions): bool */ public function hasAllPermissions(...$permissions): bool { - if (is_array($permissions[0])) { - $permissions = $permissions[0]; - } + $permissions = collect($permissions)->flatten(); foreach ($permissions as $permission) { if (! $this->hasPermissionTo($permission)) { From a49b3591c6c805e10a7baa7f0eeb8ad06befc9f5 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 23 Jan 2020 14:41:53 -0500 Subject: [PATCH 0189/1013] Update extending.md --- docs/advanced-usage/extending.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/advanced-usage/extending.md b/docs/advanced-usage/extending.md index 986a86b6e..94d27960b 100644 --- a/docs/advanced-usage/extending.md +++ b/docs/advanced-usage/extending.md @@ -29,3 +29,6 @@ If you need to REPLACE the existing `Role` or `Permission` models you need to ke - Your `Role` model needs to implement the `Spatie\Permission\Contracts\Role` contract - Your `Permission` model needs to implement the `Spatie\Permission\Contracts\Permission` contract +## Migrations - Adding fields to your models +You can add your own migrations to make changes to the role/permission tables, as you would for adding/changing fields in any other tables in your Laravel project. +Following that, you can add any necessary logic for interacting with those fields ... to your custom/extended Models. From 07cfc162c0d60e0e1f4470ef677ab07708cfd854 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 25 Jan 2020 13:32:49 -0500 Subject: [PATCH 0190/1013] Update ui-options.md --- docs/advanced-usage/ui-options.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/advanced-usage/ui-options.md b/docs/advanced-usage/ui-options.md index 9a157f506..785ec10b2 100644 --- a/docs/advanced-usage/ui-options.md +++ b/docs/advanced-usage/ui-options.md @@ -7,6 +7,8 @@ weight: 10 The package doesn't come with any screens out of the box, you should build that yourself. Here are some options to get you started: +- If you'd like to build your own UI, and understand the underlying logic for Gates and Roles and Users, the [Laravel 6 User Login and Management With Roles](https://www.youtube.com/watch?v=7PpJsho5aak&list=PLxFwlLOncxFLazmEPiB4N0iYc3Dwst6m4) video series by Mark Twigg of Penguin Digital gives thorough coverage to the topic, the theory, and implementation of a basic Roles system, independent of this Permissions Package. + - [Laravel Nova package by @vyuldashev for managing Roles and Permissions](https://github.com/vyuldashev/nova-permission) - [Laravel Nova package by @paras-malhotra for managing Roles and Permissions and permissions based authorization for Nova Resources](https://github.com/insenseanalytics/laravel-nova-permission) From 36da572a10738e62af0560b38961001cc5978c83 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 25 Jan 2020 13:33:27 -0500 Subject: [PATCH 0191/1013] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3eeab0cb0..c3da9e8fc 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ $user->can('edit articles'); ## Documentation, Installation, and Usage Instructions -See the [documentation site](https://docs.spatie.be/laravel-permission/v3/introduction/) for detailed installation and usage instructions. +See the [DOCUMENTATION](https://docs.spatie.be/laravel-permission/v3/introduction/) for detailed installation and usage instructions. ### Testing From f496ee4072552138ff27f8f90130bb316da1a0fe Mon Sep 17 00:00:00 2001 From: freek Date: Wed, 5 Feb 2020 11:19:26 +0100 Subject: [PATCH 0192/1013] update support us --- .github/FUNDING.yml | 2 +- README.md | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 2bec40976..636ef5381 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -patreon: spatie +custom: https://spatie.be/open-source/support-us diff --git a/README.md b/README.md index c3da9e8fc..ca3c14b5a 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,12 @@ Because all permissions will be registered on [Laravel's gate](https://laravel.c $user->can('edit articles'); ``` +## Support us + +We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). + +We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). + ## Documentation, Installation, and Usage Instructions See the [DOCUMENTATION](https://docs.spatie.be/laravel-permission/v3/introduction/) for detailed installation and usage instructions. @@ -85,13 +91,6 @@ Special thanks to [Alex Vanderbist](https://github.com/AlexVanderbist) who great - [santigarcor/laratrust](https://github.com/santigarcor/laratrust) implements team support - [zizaco/entrust](https://github.com/zizaco/entrust) offers some wildcard pattern matching -## Support us - -Spatie is a web design agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). - -Does your business depend on our contributions? Reach out and support us on [Patreon](https://www.patreon.com/spatie). -All pledges will be dedicated to allocating workforce on maintenance and new awesome stuff. - ## License The MIT License (MIT). Please see [License File](LICENSE.md) for more information. From d0a2fb0f305d1cb1609bd2bb4b0da04bdab8aa6d Mon Sep 17 00:00:00 2001 From: freek Date: Wed, 5 Feb 2020 11:20:50 +0100 Subject: [PATCH 0193/1013] wip --- docs/about-us.md | 2 +- docs/postcardware.md | 10 ---------- docs/support-us.md | 8 ++++++++ 3 files changed, 9 insertions(+), 11 deletions(-) delete mode 100644 docs/postcardware.md create mode 100644 docs/support-us.md diff --git a/docs/about-us.md b/docs/about-us.md index 58a49ee06..4ebc6742f 100644 --- a/docs/about-us.md +++ b/docs/about-us.md @@ -13,4 +13,4 @@ This package is heavily based on [Jeffrey Way](https://twitter.com/jeffrey_way)' on [permissions and roles](https://laracasts.com/series/whats-new-in-laravel-5-1/episodes/16). His original code can be found [in this repo on GitHub](https://github.com/laracasts/laravel-5-roles-and-permissions-demo). -Special thanks to [Alex Vanderbist](https://github.com/AlexVanderbist) who greatly helped with `v2`, and to [Chris Brown](https://github.com/drbyte) for his longtime support helping us maintain the package. +Special thanks to [Alex Vanderbist](https://github.com/AlexVanderbist) who greatly helped with `v2`, and to [Chris Brown](https://github.com/drbyte) for his longtime support helping us maintain the package. diff --git a/docs/postcardware.md b/docs/postcardware.md deleted file mode 100644 index d7d1bb697..000000000 --- a/docs/postcardware.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: Postcardware -weight: 2 ---- - -You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. - -Our address is: Spatie, Samberstraat 69D, 2060 Antwerp, Belgium. - -All postcards will get published on the [open source section](https://spatie.be/en/opensource/postcards) on our website. diff --git a/docs/support-us.md b/docs/support-us.md new file mode 100644 index 000000000..0453692fe --- /dev/null +++ b/docs/support-us.md @@ -0,0 +1,8 @@ +--- +title: Support us +weight: 2 +--- + +We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). + +We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). From fd2177e66d00ddddadb6394aa770d16fbb51eb09 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 14 Feb 2020 12:58:49 -0500 Subject: [PATCH 0194/1013] Update role-permissions.md --- docs/basic-usage/role-permissions.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/basic-usage/role-permissions.md b/docs/basic-usage/role-permissions.md index 96ee19986..bcb6186dd 100644 --- a/docs/basic-usage/role-permissions.md +++ b/docs/basic-usage/role-permissions.md @@ -90,17 +90,18 @@ but `false` for `$user->hasDirectPermission('edit articles')`. This method is useful if one builds a form for setting permissions for roles and users in an application and wants to restrict or change inherited permissions of roles of the user, i.e. allowing to change only direct permissions of the user. -You can check if the user has direct permissions without passing by the role's permissions: +You can check if the user has All or Any of a set of permissions directly assigned: ```php -// Check if the user has all direct permissions +// Check if the user has All direct permissions $user->hasAllDirectPermissions(['edit articles', 'delete articles']); -// Check if the user has any permission directly +// Check if the user has Any permission directly $user->hasAnyDirectPermission(['create articles', 'delete articles']); ``` By following the previous example, when we call `$user->hasAllDirectPermissions(['edit articles', 'delete articles'])` -it returns `true`, because the user has all these direct permissions. When we call +it returns `true`, because the user has all these direct permissions. +When we call `$user->hasAnyDirectPermission('edit articles')`, it returns `true` because the user has one of the provided permissions. From 5a3f08960b874990b6a315c4dc2058f112e72ab7 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 14 Feb 2020 13:04:51 -0500 Subject: [PATCH 0195/1013] Update HasPermissions.php --- src/Traits/HasPermissions.php | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 821f06d9c..58ae0bbdf 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -440,12 +440,16 @@ public function forgetCachedPermissions() } /** - * Check if there are all the direct permissions. - * @param array $permissions + * Check if the model has All of the requested Direct permissions. + * @param array ...$permissions * @return bool */ - public function hasAllDirectPermissions(array $permissions) : bool + public function hasAllDirectPermissions(...$permissions) : bool { + if (is_array($permissions[0])) { + $permissions = $permissions[0]; + } + foreach ($permissions as $permission) { if (! $this->hasDirectPermission($permission)) { return false; @@ -456,12 +460,16 @@ public function hasAllDirectPermissions(array $permissions) : bool } /** - * Check if there are any the direct permissions. - * @param array $permissions + * Check if the model has Any of the requested Direct permissions. + * @param array ...$permissions * @return bool */ - public function hasAnyDirectPermission(array $permissions) : bool + public function hasAnyDirectPermission(...$permissions) : bool { + if (is_array($permissions[0])) { + $permissions = $permissions[0]; + } + foreach ($permissions as $permission) { if ($this->hasDirectPermission($permission)) { return true; From dae1658fcdf84e9ac8bc6c516bf99a191ef038ba Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Fri, 14 Feb 2020 18:18:23 +0000 Subject: [PATCH 0196/1013] Apply fixes from StyleCI --- src/Traits/HasPermissions.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 78ee1c748..747c0b879 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -430,7 +430,7 @@ public function forgetCachedPermissions() * @param array ...$permissions * @return bool */ - public function hasAllDirectPermissions(...$permissions) : bool + public function hasAllDirectPermissions(...$permissions): bool { if (is_array($permissions[0])) { $permissions = $permissions[0]; @@ -450,12 +450,12 @@ public function hasAllDirectPermissions(...$permissions) : bool * @param array ...$permissions * @return bool */ - public function hasAnyDirectPermission(...$permissions) : bool + public function hasAnyDirectPermission(...$permissions): bool { if (is_array($permissions[0])) { $permissions = $permissions[0]; } - + foreach ($permissions as $permission) { if ($this->hasDirectPermission($permission)) { return true; From 4a79d501ea76f1de9e35469e881d00394d229531 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 14 Feb 2020 15:17:47 -0500 Subject: [PATCH 0197/1013] Update blade-directives.md --- docs/basic-usage/blade-directives.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/blade-directives.md b/docs/basic-usage/blade-directives.md index 99eaf3ac2..24c3e8470 100644 --- a/docs/basic-usage/blade-directives.md +++ b/docs/basic-usage/blade-directives.md @@ -3,7 +3,7 @@ title: Using Blade directives weight: 4 --- -#### Blade and Permissions +### Permissions This package doesn't add any **permission**-specific Blade directives. Instead, use Laravel's native `@can` directive to check if a user has a certain permission. From 312440e8ba656f3b1ab8b283894669c6aaa22430 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 14 Feb 2020 15:19:41 -0500 Subject: [PATCH 0198/1013] Update blade-directives.md --- docs/basic-usage/blade-directives.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/basic-usage/blade-directives.md b/docs/basic-usage/blade-directives.md index 24c3e8470..4b459b835 100644 --- a/docs/basic-usage/blade-directives.md +++ b/docs/basic-usage/blade-directives.md @@ -23,9 +23,11 @@ You can use `@can`, `@cannot`, `@canany`, and `@guest` to test for permission-re ### Roles -As discussed in the Best Practices section of the docs, it is strongly recommended to always use permission directives, instead of role directives. +As discussed in the Best Practices section of the docs, **it is strongly recommended to always use permission directives**, instead of role directives. -However, in case you need to test for Roles, this package also adds Blade directives to verify whether the currently logged in user has all or any of a given list of roles. +Additionally, if your reason for testing against Roles is for a Super-Admin, see the Best Practices section of the docs. + +If you actually need to test for Roles, this package offers some Blade directives to verify whether the currently logged in user has all or any of a given list of roles. Optionally you can pass in the `guard` that the check will be performed on as a second argument. From 669c854e0b4ca03e1730dbdb85128369689d616d Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 14 Feb 2020 15:20:27 -0500 Subject: [PATCH 0199/1013] Update blade-directives.md --- docs/basic-usage/blade-directives.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/blade-directives.md b/docs/basic-usage/blade-directives.md index 4b459b835..a6d412e7e 100644 --- a/docs/basic-usage/blade-directives.md +++ b/docs/basic-usage/blade-directives.md @@ -25,7 +25,7 @@ You can use `@can`, `@cannot`, `@canany`, and `@guest` to test for permission-re ### Roles As discussed in the Best Practices section of the docs, **it is strongly recommended to always use permission directives**, instead of role directives. -Additionally, if your reason for testing against Roles is for a Super-Admin, see the Best Practices section of the docs. +Additionally, if your reason for testing against Roles is for a Super-Admin, see the *Defining A Super-Admin* section of the docs. If you actually need to test for Roles, this package offers some Blade directives to verify whether the currently logged in user has all or any of a given list of roles. From 9d0461cb6d87bf8d02ec3661c41fa8d3bc0d13de Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 14 Feb 2020 16:01:32 -0500 Subject: [PATCH 0200/1013] Update docs --- docs/advanced-usage/cache.md | 2 +- docs/advanced-usage/extending.md | 2 +- docs/advanced-usage/phpstorm.md | 2 +- docs/changelog.md | 2 +- docs/installation-lumen.md | 2 +- docs/questions-issues.md | 2 +- docs/upgrading.md | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/advanced-usage/cache.md b/docs/advanced-usage/cache.md index e48fdf331..0ce9b4213 100644 --- a/docs/advanced-usage/cache.md +++ b/docs/advanced-usage/cache.md @@ -1,6 +1,6 @@ --- title: Cache -weight: 4 +weight: 5 --- Role and Permission data are cached to speed up performance. diff --git a/docs/advanced-usage/extending.md b/docs/advanced-usage/extending.md index 94d27960b..3500c4350 100644 --- a/docs/advanced-usage/extending.md +++ b/docs/advanced-usage/extending.md @@ -1,6 +1,6 @@ --- title: Extending -weight: 3 +weight: 4 --- ## Extending User Models diff --git a/docs/advanced-usage/phpstorm.md b/docs/advanced-usage/phpstorm.md index c4ae744b6..b9dcffbe9 100644 --- a/docs/advanced-usage/phpstorm.md +++ b/docs/advanced-usage/phpstorm.md @@ -1,6 +1,6 @@ --- title: Extending PhpStorm -weight: 5 +weight: 7 --- # Extending PhpStorm to support Blade Directives of this package diff --git a/docs/changelog.md b/docs/changelog.md index e78bf2db3..27d734a9d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ --- title: Changelog -weight: 6 +weight: 10 --- All notable changes to laravel-permission are documented [on GitHub](https://github.com/spatie/laravel-permission/blob/master/CHANGELOG.md) diff --git a/docs/installation-lumen.md b/docs/installation-lumen.md index 928e939d7..de5aa131e 100644 --- a/docs/installation-lumen.md +++ b/docs/installation-lumen.md @@ -1,6 +1,6 @@ --- title: Installation in Lumen -weight: 4 +weight: 5 --- NOTE: Lumen is not officially supported by this package. However, the following are some steps which may help get you started. diff --git a/docs/questions-issues.md b/docs/questions-issues.md index d4efccdc8..4c4313290 100644 --- a/docs/questions-issues.md +++ b/docs/questions-issues.md @@ -1,6 +1,6 @@ --- title: Questions and issues -weight: 5 +weight: 9 --- Find yourself stuck using the package? Found a bug? Do you have general questions or suggestions for improving the package? Feel free to [create an issue on GitHub](https://github.com/spatie/laravel-permission/issues), we'll try to address it as soon as possible. diff --git a/docs/upgrading.md b/docs/upgrading.md index 8f5eeec50..7b9afaaf7 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -1,6 +1,6 @@ --- title: Upgrading -weight: 4 +weight: 6 --- ### Upgrading from v2 to v3 From 5a0c47314939f4cea22d977994e52015aebbf5f6 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 14 Feb 2020 16:01:55 -0500 Subject: [PATCH 0201/1013] UUID docs --- docs/advanced-usage/uuid.md | 39 ++++++++ docs/installation-laravel.md | 173 ++++++----------------------------- 2 files changed, 65 insertions(+), 147 deletions(-) create mode 100644 docs/advanced-usage/uuid.md diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md new file mode 100644 index 000000000..6a4436e05 --- /dev/null +++ b/docs/advanced-usage/uuid.md @@ -0,0 +1,39 @@ +--- +title: UUID +weight: 6 +--- + +If you're using UUIDs or GUIDs for your User models there are a few considerations which various users have contributed. As each UUID implementation approach is different, some of these may or may not benefit you. As always, your implementation may vary. + +### Migrations +You will probably want to update the `create_permission_tables.php` migration: + +- Replace `$table->unsignedBigInteger($columnNames['model_morph_key'])` with `$table->uuid($columnNames['model_morph_key'])`. + + +### Configuration (morph key) +You will probably also want to update the configuration `column_names.model_morph_key`: + +- Change to `model_uuid` instead of the default `model_id`. (The default of `model_id` is shown in this snippet below. Change it to match your needs.) + + 'column_names' => [ + /* + * Change this if you want to name the related model primary key other than + * `model_id`. + * + * For example, this would be nice if your primary keys are all UUIDs. In + * that case, name this `model_uuid`. + */ + 'model_morph_key' => 'model_id', + ], + +### Models +You will probably want to Extend the default Role and Permission models into your own namespace, to set some specific properties (see the Extending section of the docs): + +- You may want to set `protected $keyType = "string";` so Laravel doesn't cast it to integer. +- You may want to set `protected $primaryKey = 'guid';` (or `uuid`, etc) if you changed the column name in your migrations. +- Optional: Some people have reported value in setting `public $incrementing = false;`, but others have said this caused them problems. Your implementation may vary. + +### User Models +Troubleshooting tip: In the ***Prerequisites*** section of the docs we remind you that your User model must implement the `Illuminate\Contracts\Auth\Access\Authorizable` contract so that the Gate features are made available to the User object. +In the default User model provided with Laravel, this is done by extending another model (aliased to `Authenticatable`), which extends the base Eloquent model. However, your UUID implementation may need to override that in order to set some of the properties mentioned in the Models section above. If you are running into difficulties, you may want to double-check whether your User model is doing UUIDs consistent with other parts of your app. diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 62d120e11..ca4402eb7 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -5,165 +5,44 @@ weight: 4 This package can be used in Laravel 5.8 or higher. -You can install the package via composer: +1. Consult the Prerequisites page for important considerations regarding your User models! -``` bash -composer require spatie/laravel-permission -``` +2. You can install the package via composer: -The service provider will automatically get registered. Or you may manually add the service provider in your `config/app.php` file: + ``` bash + composer require spatie/laravel-permission + ``` -```php -'providers' => [ - // ... - Spatie\Permission\PermissionServiceProvider::class, -]; -``` +3. Optional: The service provider will automatically get registered. Or you may manually add the service provider in your `config/app.php` file: -You should publish [the migration](https://github.com/spatie/laravel-permission/blob/master/database/migrations/create_permission_tables.php.stub) and [the `config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) with: + ```php + 'providers' => [ + // ... + Spatie\Permission\PermissionServiceProvider::class, + ]; + ``` -```bash -php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" -``` +4. You should publish [the migration](https://github.com/spatie/laravel-permission/blob/master/database/migrations/create_permission_tables.php.stub) and the [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) with: -If you're using UUIDs or GUIDs for your `User` models you can update the `create_permission_tables.php` migration and replace `$table->unsignedBigInteger($columnNames['model_morph_key'])` with `$table->uuid($columnNames['model_morph_key'])`. -For consistency, you can also update the package configuration file to use the `model_uuid` column name instead of the default `model_id` column. + ```bash + php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" + ``` -After the config and migration have been published and configured, you can create the role- and permission-tables by running the migrations: +5. NOTE: If you are using UUIDs, see the Advanced section of the docs on UUID steps, before you continue. It explains some changes you may want to make to the migrations and config file before continuing. It also mentions important considerations after extending this package's models for UUID capability. -```bash -php artisan migrate -``` +6. Run the migrations: After the config and migration have been published and configured, you can create the tables for this package by running: -### Default config file contents - -When published, config file initially contains: - -```php -return [ - - 'models' => [ - - /* - * When using the "HasPermissions" trait from this package, we need to know which - * Eloquent model should be used to retrieve your permissions. Of course, it - * is often just the "Permission" model but you may use whatever you like. - * - * The model you want to use as a Permission model needs to implement the - * `Spatie\Permission\Contracts\Permission` contract. - */ - - 'permission' => Spatie\Permission\Models\Permission::class, - - /* - * When using the "HasRoles" trait from this package, we need to know which - * Eloquent model should be used to retrieve your roles. Of course, it - * is often just the "Role" model but you may use whatever you like. - * - * The model you want to use as a Role model needs to implement the - * `Spatie\Permission\Contracts\Role` contract. - */ - - 'role' => Spatie\Permission\Models\Role::class, - - ], - - 'table_names' => [ - - /* - * When using the "HasRoles" trait from this package, we need to know which - * table should be used to retrieve your roles. We have chosen a basic - * default value but you may easily change it to any table you like. - */ - - 'roles' => 'roles', + ```bash + php artisan migrate + ``` - /* - * When using the "HasPermissions" trait from this package, we need to know which - * table should be used to retrieve your permissions. We have chosen a basic - * default value but you may easily change it to any table you like. - */ +7. Add the necessary trait to your User model. - 'permissions' => 'permissions', + Consult the Basic Usage section of the docs for how to get started using the features of this package. - /* - * When using the "HasPermissions" trait from this package, we need to know which - * table should be used to retrieve your models permissions. We have chosen a - * basic default value but you may easily change it to any table you like. - */ - 'model_has_permissions' => 'model_has_permissions', - - /* - * When using the "HasRoles" trait from this package, we need to know which - * table should be used to retrieve your models roles. We have chosen a - * basic default value but you may easily change it to any table you like. - */ - - 'model_has_roles' => 'model_has_roles', - - /* - * When using the "HasRoles" trait from this package, we need to know which - * table should be used to retrieve your roles permissions. We have chosen a - * basic default value but you may easily change it to any table you like. - */ - - 'role_has_permissions' => 'role_has_permissions', - ], - - 'column_names' => [ - - /* - * Change this if you want to name the related model primary key other than - * `model_id`. - * - * For example, this would be nice if your primary keys are all UUIDs. In - * that case, name this `model_uuid`. - */ - 'model_morph_key' => 'model_id', - ], - - /* - * When set to true, the required permission/role names are added to the exception - * message. This could be considered an information leak in some contexts, so - * the default setting is false here for optimum safety. - */ - - 'display_permission_in_exception' => false, - - 'cache' => [ +--- - /* - * By default all permissions are cached for 24 hours to speed up performance. - * When permissions or roles are updated the cache is flushed automatically. - */ - - 'expiration_time' => \DateInterval::createFromDateString('24 hours'), - - /* - * The cache key used to store all permissions. - */ - - 'key' => 'spatie.permission.cache', - - /* - * When checking for a permission against a model by passing a Permission - * instance to the check, this key determines what attribute on the - * Permissions model is used to cache against. - * - * Ideally, this should match your preferred way of checking permissions, eg: - * `$user->can('view-posts')` would be 'name'. - */ - - 'model_key' => 'name', - - /* - * You may optionally indicate a specific cache driver to use for permission and - * role caching using any of the `store` drivers listed in the cache.php config - * file. Using 'default' here means to use the `default` set in cache.php. - */ +### Default config file contents - 'store' => 'default', - ], -]; -``` +You can view the default config file contents at: https://github.com/spatie/laravel-permission/blob/master/config/permission.php From c8a12de6d2692bf99a9980f19341e23af6dffc87 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 14 Feb 2020 16:02:40 -0500 Subject: [PATCH 0202/1013] Update blade-directives.md --- docs/basic-usage/blade-directives.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/blade-directives.md b/docs/basic-usage/blade-directives.md index a6d412e7e..4a2939c0d 100644 --- a/docs/basic-usage/blade-directives.md +++ b/docs/basic-usage/blade-directives.md @@ -1,5 +1,5 @@ --- -title: Using Blade directives +title: Blade directives weight: 4 --- From ec289f1cdde344d19514b621a1349e7f397d0035 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 14 Feb 2020 16:04:07 -0500 Subject: [PATCH 0203/1013] Update installation-laravel.md --- docs/installation-laravel.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index ca4402eb7..c4a35842c 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -8,11 +8,9 @@ This package can be used in Laravel 5.8 or higher. 1. Consult the Prerequisites page for important considerations regarding your User models! 2. You can install the package via composer: - ``` bash composer require spatie/laravel-permission ``` - 3. Optional: The service provider will automatically get registered. Or you may manually add the service provider in your `config/app.php` file: ```php From 8adc292a918ace0ab339f937e0d06fa798742ded Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 14 Feb 2020 16:05:47 -0500 Subject: [PATCH 0204/1013] Update blade-directives.md --- docs/basic-usage/blade-directives.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/basic-usage/blade-directives.md b/docs/basic-usage/blade-directives.md index 4a2939c0d..1c1aaaaff 100644 --- a/docs/basic-usage/blade-directives.md +++ b/docs/basic-usage/blade-directives.md @@ -3,7 +3,7 @@ title: Blade directives weight: 4 --- -### Permissions +## Permissions This package doesn't add any **permission**-specific Blade directives. Instead, use Laravel's native `@can` directive to check if a user has a certain permission. @@ -22,7 +22,7 @@ or You can use `@can`, `@cannot`, `@canany`, and `@guest` to test for permission-related access. -### Roles +## Roles As discussed in the Best Practices section of the docs, **it is strongly recommended to always use permission directives**, instead of role directives. Additionally, if your reason for testing against Roles is for a Super-Admin, see the *Defining A Super-Admin* section of the docs. From eb46da576ca7c9333af031bfbe017a82a1b1f93a Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 14 Feb 2020 16:10:01 -0500 Subject: [PATCH 0205/1013] Update installation-laravel.md --- docs/installation-laravel.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index c4a35842c..325e5993b 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -8,9 +8,9 @@ This package can be used in Laravel 5.8 or higher. 1. Consult the Prerequisites page for important considerations regarding your User models! 2. You can install the package via composer: - ``` bash - composer require spatie/laravel-permission - ``` + + composer require spatie/laravel-permission + 3. Optional: The service provider will automatically get registered. Or you may manually add the service provider in your `config/app.php` file: ```php From abcac90e6291bdefe31c1778b5096770cc0b7c29 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 14 Feb 2020 16:11:40 -0500 Subject: [PATCH 0206/1013] Update installation-laravel.md --- docs/installation-laravel.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 325e5993b..2c9b21bfd 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -39,8 +39,7 @@ This package can be used in Laravel 5.8 or higher. Consult the Basic Usage section of the docs for how to get started using the features of this package. ---- - ### Default config file contents -You can view the default config file contents at: https://github.com/spatie/laravel-permission/blob/master/config/permission.php +You can view the default config file contents at: +https://github.com/spatie/laravel-permission/blob/master/config/permission.php From d860d64ccb4de959b4c7d0236a34708b57f79994 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 14 Feb 2020 16:12:59 -0500 Subject: [PATCH 0207/1013] Update installation-laravel.md --- docs/installation-laravel.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 2c9b21bfd..93b04a84a 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -19,7 +19,6 @@ This package can be used in Laravel 5.8 or higher. Spatie\Permission\PermissionServiceProvider::class, ]; ``` - 4. You should publish [the migration](https://github.com/spatie/laravel-permission/blob/master/database/migrations/create_permission_tables.php.stub) and the [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) with: ```bash @@ -30,9 +29,7 @@ This package can be used in Laravel 5.8 or higher. 6. Run the migrations: After the config and migration have been published and configured, you can create the tables for this package by running: - ```bash - php artisan migrate - ``` + php artisan migrate 7. Add the necessary trait to your User model. From 845a7d61d0edb82506704a81c879c2912ce45057 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 14 Feb 2020 16:15:35 -0500 Subject: [PATCH 0208/1013] Update installation-laravel.md --- docs/installation-laravel.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 93b04a84a..b57b202ce 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -3,7 +3,7 @@ title: Installation in Laravel weight: 4 --- -This package can be used in Laravel 5.8 or higher. +This package can be used with Laravel 5.8 or higher. 1. Consult the Prerequisites page for important considerations regarding your User models! @@ -19,6 +19,7 @@ This package can be used in Laravel 5.8 or higher. Spatie\Permission\PermissionServiceProvider::class, ]; ``` + 4. You should publish [the migration](https://github.com/spatie/laravel-permission/blob/master/database/migrations/create_permission_tables.php.stub) and the [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) with: ```bash From 6177794049c453ca6123807f884f9175e2d7cabc Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 14 Feb 2020 16:18:09 -0500 Subject: [PATCH 0209/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8cd759dd..21e54ac37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## (pending) +- Added methods to check any/all when querying direct permissions #1245 + ## 3.6.0 - 2020-01-17 - Added Laravel 7.0 support - Allow splat operator for passing roles to `hasAnyRole()` From 0176fb2d2d20c3752176532e7840ac3561e098e2 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 14 Feb 2020 16:23:28 -0500 Subject: [PATCH 0210/1013] Update installation-laravel.md --- docs/installation-laravel.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index b57b202ce..b05c226a1 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -13,7 +13,7 @@ This package can be used with Laravel 5.8 or higher. 3. Optional: The service provider will automatically get registered. Or you may manually add the service provider in your `config/app.php` file: - ```php + ``` 'providers' => [ // ... Spatie\Permission\PermissionServiceProvider::class, @@ -22,7 +22,7 @@ This package can be used with Laravel 5.8 or higher. 4. You should publish [the migration](https://github.com/spatie/laravel-permission/blob/master/database/migrations/create_permission_tables.php.stub) and the [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) with: - ```bash + ``` php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" ``` @@ -32,12 +32,11 @@ This package can be used with Laravel 5.8 or higher. php artisan migrate -7. Add the necessary trait to your User model. - - Consult the Basic Usage section of the docs for how to get started using the features of this package. +7. Add the necessary trait to your User model: Consult the Basic Usage section of the docs for how to get started using the features of this package. ### Default config file contents -You can view the default config file contents at: +You can view the default config file contents at: + https://github.com/spatie/laravel-permission/blob/master/config/permission.php From 6e0002f76c30dca9411a21b7c8ea6a740fcba6fc Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 14 Feb 2020 16:29:37 -0500 Subject: [PATCH 0211/1013] Update using-policies.md --- docs/best-practices/using-policies.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/best-practices/using-policies.md b/docs/best-practices/using-policies.md index c8f8f9695..056ada600 100644 --- a/docs/best-practices/using-policies.md +++ b/docs/best-practices/using-policies.md @@ -1,6 +1,10 @@ --- -title: Using policies +title: Model Policies weight: 2 --- -The best way to incorporate access control for access to app features is with Model Policies. This way your application logic can be combined with your permission rules, keeping your implementation simpler. You can find an example of implementing a model policy with this Laravel Permissions package in this demo app: https://github.com/drbyte/spatie-permissions-demo/blob/master/app/Policies/PostPolicy.php +The best way to incorporate access control for application features is with [Laravel's Model Policies](https://laravel.com/docs/authorization#creating-policies). + +Using Policies allows you to simplify things by abstracting your "control" rules into one place, where your application logic can be combined with your permission rules. + +You can find an example of implementing a model policy with this Laravel Permissions package in this demo app: https://github.com/drbyte/spatie-permissions-demo/blob/master/app/Policies/PostPolicy.php From 04f6bafd3629b4880414c031d7636d2c050dd65c Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 15 Feb 2020 08:42:39 -0500 Subject: [PATCH 0212/1013] Update installation-laravel.md --- docs/installation-laravel.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index b05c226a1..b24a15782 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -7,11 +7,13 @@ This package can be used with Laravel 5.8 or higher. 1. Consult the Prerequisites page for important considerations regarding your User models! -2. You can install the package via composer: +2. This package publishes a `config/permission.php` file. If you already have a file by that name, you must rename or remove it. + +3. You can install the package via composer: composer require spatie/laravel-permission -3. Optional: The service provider will automatically get registered. Or you may manually add the service provider in your `config/app.php` file: +4. Optional: The service provider will automatically get registered. Or you may manually add the service provider in your `config/app.php` file: ``` 'providers' => [ @@ -20,19 +22,19 @@ This package can be used with Laravel 5.8 or higher. ]; ``` -4. You should publish [the migration](https://github.com/spatie/laravel-permission/blob/master/database/migrations/create_permission_tables.php.stub) and the [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) with: +5. You should publish [the migration](https://github.com/spatie/laravel-permission/blob/master/database/migrations/create_permission_tables.php.stub) and the [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) with: ``` php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" ``` -5. NOTE: If you are using UUIDs, see the Advanced section of the docs on UUID steps, before you continue. It explains some changes you may want to make to the migrations and config file before continuing. It also mentions important considerations after extending this package's models for UUID capability. +6. NOTE: If you are using UUIDs, see the Advanced section of the docs on UUID steps, before you continue. It explains some changes you may want to make to the migrations and config file before continuing. It also mentions important considerations after extending this package's models for UUID capability. -6. Run the migrations: After the config and migration have been published and configured, you can create the tables for this package by running: +7. Run the migrations: After the config and migration have been published and configured, you can create the tables for this package by running: php artisan migrate -7. Add the necessary trait to your User model: Consult the Basic Usage section of the docs for how to get started using the features of this package. +8. Add the necessary trait to your User model: Consult the Basic Usage section of the docs for how to get started using the features of this package. ### Default config file contents From 9b5abb290505c4c5467867152f7773e4a8366091 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 15 Feb 2020 08:44:24 -0500 Subject: [PATCH 0213/1013] Update prerequisites.md --- docs/prerequisites.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/prerequisites.md b/docs/prerequisites.md index 943929bf1..57d133439 100644 --- a/docs/prerequisites.md +++ b/docs/prerequisites.md @@ -28,3 +28,6 @@ class User extends Authenticatable Additionally, your `User` model/object MUST NOT have a `role` or `roles` property (or field in the database), nor a `roles()` method on it. Those will interfere with the properties and methods added by the `HasRoles` trait provided by this package, thus causing unexpected outcomes when this package's methods are used to inspect roles and permissions. Similarly, your `User` model/object MUST NOT have a `permission` or `permissions` property (or field in the database), nor a `permissions()` method on it. Those will interfere with the properties and methods added by the `HasPermissions` trait provided by this package (which is invoked via the `HasRoles` trait). + +This package publishes a `config/permission.php` file. If you already have a file by that name, you must rename or remove it, as it will conflict with this package. You could optionally merge your own values with those required by this package, as long as the keys that this package expects are present. See the source file for more details. + From c15f2d23ecaa5f3ba8a76d80f922f372cf6098f2 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 15 Feb 2020 10:05:52 -0500 Subject: [PATCH 0214/1013] Update installation-lumen.md --- docs/installation-lumen.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/installation-lumen.md b/docs/installation-lumen.md index de5aa131e..23e466e0f 100644 --- a/docs/installation-lumen.md +++ b/docs/installation-lumen.md @@ -35,7 +35,7 @@ $app->routeMiddleware([ ]); ``` -Also in `bootstrap/app.php` register the config file, service provider, and cache alias: +... and in the same file, in the ServiceProviders section, register the package configuration, service provider, and cache alias: ```php $app->configure('permission'); @@ -48,9 +48,9 @@ If you are using guards you will need to uncomment the AuthServiceProvider line: $app->register(App\Providers\AuthServiceProvider::class); ``` -Now, ensure your database configuration is set. +Ensure your database configuration is set in your `.env` (or `config/database.php` if you have one). -Then run the migrations: +Run the migrations to create the tables for this package: ```bash php artisan migrate @@ -59,3 +59,8 @@ php artisan migrate NOTE: Remember that Laravel's authorization layer requires that your `User` model implement the `Illuminate\Contracts\Auth\Access\Authorizable` contract. In Lumen you will then also need to use the `Laravel\Lumen\Auth\Authorizable` trait. +NOTE: If you are working with a fresh install of Lumen, then you probably also need a migration file for your Users table. You can create your own, or you can copy a basic one from Laravel: + +https://github.com/laravel/laravel/blob/master/database/migrations/2014_10_12_000000_create_users_table.php + +(You will need to run `php artisan migrate` after adding this file.) From c9e297389c9473e0dafc48b41dfcce9bb16eb6e0 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 15 Feb 2020 10:18:16 -0500 Subject: [PATCH 0215/1013] Update installation-lumen.md --- docs/installation-lumen.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/installation-lumen.md b/docs/installation-lumen.md index 23e466e0f..b2b350b51 100644 --- a/docs/installation-lumen.md +++ b/docs/installation-lumen.md @@ -43,7 +43,7 @@ $app->alias('cache', \Illuminate\Cache\CacheManager::class); // if you don't ha $app->register(Spatie\Permission\PermissionServiceProvider::class); ``` -If you are using guards you will need to uncomment the AuthServiceProvider line: +... and in the same file, since the Authorization layer uses guards you will need to uncomment the AuthServiceProvider line: ```php $app->register(App\Providers\AuthServiceProvider::class); ``` @@ -56,11 +56,16 @@ Run the migrations to create the tables for this package: php artisan migrate ``` - +--- +### User Model NOTE: Remember that Laravel's authorization layer requires that your `User` model implement the `Illuminate\Contracts\Auth\Access\Authorizable` contract. In Lumen you will then also need to use the `Laravel\Lumen\Auth\Authorizable` trait. +--- +### User Table NOTE: If you are working with a fresh install of Lumen, then you probably also need a migration file for your Users table. You can create your own, or you can copy a basic one from Laravel: https://github.com/laravel/laravel/blob/master/database/migrations/2014_10_12_000000_create_users_table.php (You will need to run `php artisan migrate` after adding this file.) + +Remember to update your ModelFactory.php to match the fields in the migration you create/copy. From 239bb55fd465948b7718e2ce4aeda83f7ea85d07 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 15 Feb 2020 11:47:24 -0500 Subject: [PATCH 0216/1013] Remove old Lumen dependencies Since this package supports Laravel 5.8+ and Lumen's use of Illuminate/Support allows the publishes and mergeConfigFrom methods, there's no longer a need to be so specific about detecting Lumen in relation to those. Thus the code can be simplified. This allows removing the `isNotLumen` helper as well, which avoids the risk of false-positive responses from a clashing global helper function of the same name. --- src/PermissionServiceProvider.php | 12 +++++------- src/helpers.php | 12 ------------ 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 2a13ab5bf..c8757b2ce 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -14,7 +14,7 @@ class PermissionServiceProvider extends ServiceProvider { public function boot(PermissionRegistrar $permissionLoader, Filesystem $filesystem) { - if (isNotLumen()) { + if (function_exists('config_path')) { // function not available and 'publish' not relevant in Lumen $this->publishes([ __DIR__.'/../config/permission.php' => config_path('permission.php'), ], 'config'); @@ -44,12 +44,10 @@ public function boot(PermissionRegistrar $permissionLoader, Filesystem $filesyst public function register() { - if (isNotLumen()) { - $this->mergeConfigFrom( - __DIR__.'/../config/permission.php', - 'permission' - ); - } + $this->mergeConfigFrom( + __DIR__.'/../config/permission.php', + 'permission' + ); $this->registerBladeExtensions(); } diff --git a/src/helpers.php b/src/helpers.php index e9f3c80ce..7f79e5764 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -18,15 +18,3 @@ function getModelForGuard(string $guard) })->get($guard); } } - -if (! function_exists('isNotLumen')) { - /** - * check if application is lumen. - * - * @return bool - */ - function isNotLumen(): bool - { - return ! preg_match('/lumen/i', app()->version()); - } -} From ed1bf16f234358e37d1473146391029c9f26a79c Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 15 Feb 2020 11:53:26 -0500 Subject: [PATCH 0217/1013] Update run-tests.yml --- .github/workflows/run-tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 5eea95ee9..61ce7a498 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,6 +1,6 @@ name: "Run Tests" -on: [push] +on: [push, pull_request] jobs: test: @@ -9,8 +9,8 @@ jobs: strategy: fail-fast: true matrix: - php: [7.2, 7.3, 7.4] - laravel: [5.8.*, 6.*] + php: [7.4, 7.3, 7.2] + laravel: [6.*, 5.8.*] dependency-version: [prefer-lowest, prefer-stable] include: - laravel: 6.* From 8e7adbcf0b94c1d5d0de8543162f3d00d2daade3 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 15 Feb 2020 11:54:54 -0500 Subject: [PATCH 0218/1013] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21e54ac37..f7bdcf1b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to `laravel-permission` will be documented in this file ## (pending) - Added methods to check any/all when querying direct permissions #1245 +- Removed older Lumen dependencies #1371 ## 3.6.0 - 2020-01-17 - Added Laravel 7.0 support From 8176444c69d299c56416b036601c0cfedd20808f Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 15 Feb 2020 11:57:10 -0500 Subject: [PATCH 0219/1013] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7bdcf1b4..e98138dd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to `laravel-permission` will be documented in this file -## (pending) +## 3.7.0 - 2020-02-15 - Added methods to check any/all when querying direct permissions #1245 - Removed older Lumen dependencies #1371 From 681a59a707e89cbab4c6d919ecb353c6210eb1f1 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 15 Feb 2020 12:32:22 -0500 Subject: [PATCH 0220/1013] Apply flatten to remaining methods, add tests --- src/Traits/HasPermissions.php | 8 ++------ tests/HasPermissionsTest.php | 7 +++++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 0e1da7405..fe54b5502 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -420,9 +420,7 @@ public function forgetCachedPermissions() */ public function hasAllDirectPermissions(...$permissions): bool { - if (is_array($permissions[0])) { - $permissions = $permissions[0]; - } + $permissions = collect($permissions)->flatten(); foreach ($permissions as $permission) { if (! $this->hasDirectPermission($permission)) { @@ -440,9 +438,7 @@ public function hasAllDirectPermissions(...$permissions): bool */ public function hasAnyDirectPermission(...$permissions): bool { - if (is_array($permissions[0])) { - $permissions = $permissions[0]; - } + $permissions = collect($permissions)->flatten(); foreach ($permissions as $permission) { if ($this->hasDirectPermission($permission)) { diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index 03c7d0ad7..93652a038 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -267,6 +267,7 @@ public function it_can_determine_that_the_user_has_any_of_the_permissions_direct $this->testUser->revokePermissionTo($this->testUserPermission); $this->assertTrue($this->testUser->hasAnyPermission('edit-articles', 'edit-news')); + $this->assertFalse($this->testUser->hasAnyPermission('edit-blog', 'Edit News', ['Edit News'])); } /** @test */ @@ -293,6 +294,7 @@ public function it_can_determine_that_the_user_has_any_of_the_permissions_via_ro $this->testUser->assignRole('testRole'); $this->assertTrue($this->testUser->hasAnyPermission('edit-news', 'edit-articles')); + $this->assertFalse($this->testUser->hasAnyPermission('edit-blog', 'Edit News', ['Edit News'])); } /** @test */ @@ -305,6 +307,7 @@ public function it_can_determine_that_the_user_has_all_of_the_permissions_direct $this->testUser->revokePermissionTo('edit-articles'); $this->assertFalse($this->testUser->hasAllPermissions('edit-articles', 'edit-news')); + $this->assertFalse($this->testUser->hasAllPermissions(['edit-articles', 'edit-news'], 'edit-blog')); } /** @test */ @@ -507,7 +510,9 @@ public function it_can_check_many_direct_permissions() { $this->testUser->givePermissionTo(['edit-articles', 'edit-news']); $this->assertTrue($this->testUser->hasAllDirectPermissions(['edit-news', 'edit-articles'])); + $this->assertTrue($this->testUser->hasAllDirectPermissions('edit-news', 'edit-articles')); $this->assertFalse($this->testUser->hasAllDirectPermissions(['edit-articles', 'edit-news', 'edit-blog'])); + $this->assertFalse($this->testUser->hasAllDirectPermissions(['edit-articles', 'edit-news'], 'edit-blog')); } /** @test */ @@ -515,5 +520,7 @@ public function it_can_check_if_there_is_any_of_the_direct_permissions_given() { $this->testUser->givePermissionTo(['edit-articles', 'edit-news']); $this->assertTrue($this->testUser->hasAnyDirectPermission(['edit-news', 'edit-blog'])); + $this->assertTrue($this->testUser->hasAnyDirectPermission('edit-news', 'edit-blog')); + $this->assertFalse($this->testUser->hasAnyDirectPermission('edit-blog', 'Edit News', ['Edit News'])); } } From 3aff5437b3e66e877dcc3158f6cfaaced8e468ab Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 15 Feb 2020 12:52:58 -0500 Subject: [PATCH 0221/1013] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e98138dd6..bcc0af24e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to `laravel-permission` will be documented in this file +## 3.7.1 - 2020-02-15 +- Internal refactoring of scopes to use whereIn instead of orWhere #1334, #1335 +- Internal refactoring to flatten collection on splat #1341 + ## 3.7.0 - 2020-02-15 - Added methods to check any/all when querying direct permissions #1245 - Removed older Lumen dependencies #1371 From 777d027e750ecefb6e23995f4012ccfc82c81e9d Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 17 Feb 2020 12:03:31 -0500 Subject: [PATCH 0222/1013] Adjust for Lumen limitations --- src/PermissionServiceProvider.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index c8757b2ce..e79a62156 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -22,10 +22,10 @@ public function boot(PermissionRegistrar $permissionLoader, Filesystem $filesyst $this->publishes([ __DIR__.'/../database/migrations/create_permission_tables.php.stub' => $this->getMigrationFileName($filesystem), ], 'migrations'); - - $this->registerMacroHelpers(); } + $this->registerMacroHelpers(); + $this->commands([ Commands\CacheReset::class, Commands\CreateRole::class, @@ -117,6 +117,10 @@ protected function registerBladeExtensions() protected function registerMacroHelpers() { + if (! method_exists(Route::class, 'macro')) { // Lumen + return; + } + Route::macro('role', function ($roles = []) { if (! is_array($roles)) { $roles = [$roles]; From ce5d82fc5f5300575876c42131f9e9ae2e18f6aa Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 17 Feb 2020 12:06:57 -0500 Subject: [PATCH 0223/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcc0af24e..bc2ef35d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 3.7.2 - 2020-02-17 +- Refine test for Lumen dependency. Ref #1371, Fixes #1372. + ## 3.7.1 - 2020-02-15 - Internal refactoring of scopes to use whereIn instead of orWhere #1334, #1335 - Internal refactoring to flatten collection on splat #1341 From 603efaeee59c5ac274cc48c6c9511d7319bca669 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 17 Feb 2020 14:33:21 -0500 Subject: [PATCH 0224/1013] Create new-app.md --- docs/basic-usage/new-app.md | 168 ++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 docs/basic-usage/new-app.md diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md new file mode 100644 index 000000000..41717e9ce --- /dev/null +++ b/docs/basic-usage/new-app.md @@ -0,0 +1,168 @@ +--- +title: Example App +weight: 90 +--- + +## Creating A Demo App + +If you want to just try out the features of this package you can get started with the following. + +The examples on this page are primarily added for assistance in creating a quick demo app for troubleshooting purposes, to post the repo on github for convenient sharing to collaborate or get support. + +### Initial setup: + +```sh +cd ~/Sites +laravel new mypermissionsdemo +cd mypermissionsdemo +git init +git add . +git commit -m "Fresh Laravel Install" + +# Environment +cp -n .env.example .env +sed -i '' 's/DB_CONNECTION=mysql/DB_CONNECTION=sqlite/' .env +sed -i '' 's/DB_DATABASE=laravel/#DB_DATABASE=laravel/' .env +touch database/database.sqlite + +# Package +composer require spatie/laravel-permission +php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" +git add . +git commit -m "Add Spatie Laravel Permissions package" +php artisan migrate:fresh + +# Add `HasRoles` trait to User model +sed -i '' $'s/use Notifiable;/use Notifiable;\\\n use \\\\Spatie\\\\Permission\\\\Traits\\\\HasRoles;/' app/User.php +git add . && git commit -m "Add HasRoles trait" + +# Add Laravel's basic auth scaffolding +composer require laravel/ui --dev +php artisan ui bootstrap --auth +# npm install && npm run prod +git add . && git commit -m "Setup auth scaffold" +``` + +### Add some basic permissions +- Add a new file, `/database/seeds/PermissionsDemoSeeder.php` such as the following (You could create it with `php artisan make:seed` and then edit the file accordingly): + +```php +forgetCachedPermissions(); + + // create permissions + Permission::create(['name' => 'edit articles']); + Permission::create(['name' => 'delete articles']); + Permission::create(['name' => 'publish articles']); + Permission::create(['name' => 'unpublish articles']); + + // create roles and assign existing permissions + $role1 = Role::create(['name' => 'writer']); + $role1->givePermissionTo('edit articles'); + $role1->givePermissionTo('delete articles'); + + $role2 = Role::create(['name' => 'admin']); + $role2->givePermissionTo('publish articles'); + $role2->givePermissionTo('unpublish articles'); + + $role3 = Role::create(['name' => 'super-admin']); + // gets all permissions via Gate::before rule; see AuthServiceProvider + + // create demo users + $user = Factory(App\User::class)->create([ + 'name' => 'Example User', + 'email' => 'test@example.com', + // factory default password is 'secret' + ]); + $user->assignRole($role1); + + $user = Factory(App\User::class)->create([ + 'name' => 'Example Admin User', + 'email' => 'admin@example.com', + // factory default password is 'secret' + ]); + $user->assignRole($role2); + + $user = Factory(App\User::class)->create([ + 'name' => 'Example Super-Admin User', + 'email' => 'superadmin@example.com', + // factory default password is 'secret' + ]); + $user->assignRole($role3); + } +} + +``` + +- re-migrate and seed the database: + +```sh +composer dump-autoload +php artisan migrate:fresh --seed --seeder=PermissionsDemoSeeder +``` + +### Grant Super-Admin access +Super-Admins are a common feature. Using the following approach allows that when your Super-Admin user is logged in, all permission-checks in your app which call `can()` or `@can()` will return true. +- Add a Gate::before check in your `AuthServiceProvider`: + +```diff + public function boot() + { + $this->registerPolicies(); + + // + ++ // Implicitly grant "Super Admin" role all permission checks using can() ++ Gate::before(function ($user, $ability) { ++ if ($user->hasRole('Super-Admin')) { ++ return true; ++ } ++ }); + } +``` + + +### Application Code +The permissions created in the seeder above imply that there will be some sort of Posts or Article features, and that various users will have various access control levels to manage/view those objects. + +Your app will have Models, Controllers, routes, Views, Factories, Policies, Tests, middleware, and maybe additional Seeders. + +You can see examples of these in the demo app at https://github.com/drbyte/spatie-permissions-demo/ + +## Sharing +To share your app on Github for easy collaboration: +- create a new public repository on Github, without any extras like readme/etc. +- follow github's sample code for linking your local repo and uploading the code. It will look like this: + +```sh +git remote add origin git@github.com:YOURUSERNAME/REPONAME.git +git push -u origin master +``` +The above only needs to be done once. + +- then add the rest of your code by making new commits: + +```sh +git add . +git commit -m "Explain what your commit is about here" +git push origin master +``` +Repeat the above process whenever you change code that you want to share. + +Those are the basics! From e12fe69d62ff45dd127b56d42b86993338f38600 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 17 Feb 2020 14:43:06 -0500 Subject: [PATCH 0225/1013] Update new-app.md --- docs/basic-usage/new-app.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index 41717e9ce..b29e31849 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -119,6 +119,7 @@ php artisan migrate:fresh --seed --seeder=PermissionsDemoSeeder ### Grant Super-Admin access Super-Admins are a common feature. Using the following approach allows that when your Super-Admin user is logged in, all permission-checks in your app which call `can()` or `@can()` will return true. + - Add a Gate::before check in your `AuthServiceProvider`: ```diff From 4407533a6cbf665be947d1f33d4490a35705a9fe Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 17 Feb 2020 14:44:12 -0500 Subject: [PATCH 0226/1013] Update new-app.md --- docs/basic-usage/new-app.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index b29e31849..9d2a1b800 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -148,6 +148,7 @@ You can see examples of these in the demo app at https://github.com/drbyte/spati ## Sharing To share your app on Github for easy collaboration: + - create a new public repository on Github, without any extras like readme/etc. - follow github's sample code for linking your local repo and uploading the code. It will look like this: From 87335fea2a46718d8f56084db47de53d0e715766 Mon Sep 17 00:00:00 2001 From: Ash <1744544+ashgibson@users.noreply.github.com> Date: Tue, 18 Feb 2020 17:08:44 +1100 Subject: [PATCH 0227/1013] Updated middleware.md Added documentation on how to use built in Laravel authorization middleware. --- docs/basic-usage/middleware.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index 54f9cff8f..e5540ddaf 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -3,6 +3,14 @@ title: Using a middleware weight: 7 --- +You can use the built-in Laravel middleware from `\Illuminate\Auth\Middleware\Authorize::class` + +```php +Route::group(['middleware' => ['can:publish articles']], function () { + // +}); +``` + This package comes with `RoleMiddleware`, `PermissionMiddleware` and `RoleOrPermissionMiddleware` middleware. You can add them inside your `app/Http/Kernel.php` file. ```php From 5c4e1744e533f7f79cf787975851deb52a07a557 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 18 Feb 2020 08:39:39 -0500 Subject: [PATCH 0228/1013] Update middleware.md --- docs/basic-usage/middleware.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index e5540ddaf..9014b0f2a 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -3,7 +3,9 @@ title: Using a middleware weight: 7 --- -You can use the built-in Laravel middleware from `\Illuminate\Auth\Middleware\Authorize::class` +## Default Middleware + +For checking against a single permission (see Best Practices) using `can`, you can use the built-in Laravel middleware provided by `\Illuminate\Auth\Middleware\Authorize::class` like this: ```php Route::group(['middleware' => ['can:publish articles']], function () { @@ -11,6 +13,8 @@ Route::group(['middleware' => ['can:publish articles']], function () { }); ``` +## Package Middleware + This package comes with `RoleMiddleware`, `PermissionMiddleware` and `RoleOrPermissionMiddleware` middleware. You can add them inside your `app/Http/Kernel.php` file. ```php From 0ddfedc0203405df5562fd54e7f987245902fba1 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 18 Feb 2020 08:41:00 -0500 Subject: [PATCH 0229/1013] Update middleware.md Laravel 7 uses Throwable instead of Exception --- docs/basic-usage/middleware.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index 9014b0f2a..5c42f3e58 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -86,7 +86,7 @@ public function __construct() If you want to override the default `403` response, you can catch the `UnauthorizedException` using your app's exception handler: ```php -public function render($request, Exception $exception) +public function render($request, Throwable $exception) { if ($exception instanceof \Spatie\Permission\Exceptions\UnauthorizedException) { // Code here ... From cff803fb54ff6aaf395e1c32a9bdaa77ecac3286 Mon Sep 17 00:00:00 2001 From: Matt Haught Date: Tue, 18 Feb 2020 16:14:52 -0500 Subject: [PATCH 0230/1013] clear class permissions var on boot (#1378) For long running processes like swoole, PermissionRegistrar $permissions needs to be cleared when the service provider is booted. --- src/PermissionRegistrar.php | 10 ++++++++++ src/PermissionServiceProvider.php | 1 + 2 files changed, 11 insertions(+) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 66aa4e978..581e459e9 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -104,6 +104,16 @@ public function forgetCachedPermissions() return $this->cache->forget(self::$cacheKey); } + /** + * Clear class permissions. + * This is only intended to be called by the PermissionServiceProvider on boot, + * so that long-running instances like Swoole don't keep old data in memory. + */ + public function clearClassPermissions() + { + $this->permissions = null; + } + /** * Get the permissions based on the passed params. * diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index e79a62156..edd46bc26 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -35,6 +35,7 @@ public function boot(PermissionRegistrar $permissionLoader, Filesystem $filesyst $this->registerModelBindings(); + $permissionLoader->clearClassPermissions(); $permissionLoader->registerPermissions(); $this->app->singleton(PermissionRegistrar::class, function ($app) use ($permissionLoader) { From 2339b6fae8f8aa5af047e1a0c54b6fb37546058a Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 18 Feb 2020 16:16:24 -0500 Subject: [PATCH 0231/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc2ef35d3..93d3aca18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 3.8.0 - 2020-02-18 +- Clear in-memory permissions on boot, for benefit of long running processes like Swoole. #1378 + ## 3.7.2 - 2020-02-17 - Refine test for Lumen dependency. Ref #1371, Fixes #1372. From 37b0e2da4f680b42b305ff0b170a9ec8b7db3790 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 18 Feb 2020 19:42:50 -0500 Subject: [PATCH 0232/1013] Update exceptions.md Updated for Laravel 7 consistency --- docs/advanced-usage/exceptions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-usage/exceptions.md b/docs/advanced-usage/exceptions.md index d7c8f9b79..fcc7f9d9e 100644 --- a/docs/advanced-usage/exceptions.md +++ b/docs/advanced-usage/exceptions.md @@ -12,7 +12,7 @@ You can find all the exceptions added by this package in the code here: https:// **app/Exceptions/Handler.php** ```php -public function render($request, Exception $exception) +public function render($request, Throwable $exception) { if ($exception instanceof \Spatie\Permission\Exceptions\UnauthorizedException) { return response()->json([ From 3d40b0c0e3e209ac2843b1d02a8e8eacda2306a8 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 18 Feb 2020 19:43:37 -0500 Subject: [PATCH 0233/1013] Update middleware.md Remove duplicate Exceptions section (see exceptions.md) --- docs/basic-usage/middleware.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index 5c42f3e58..a10aa3f36 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -81,17 +81,3 @@ public function __construct() $this->middleware(['role_or_permission:super-admin|edit articles']); } ``` - -### Catching role and permission failures -If you want to override the default `403` response, you can catch the `UnauthorizedException` using your app's exception handler: - -```php -public function render($request, Throwable $exception) -{ - if ($exception instanceof \Spatie\Permission\Exceptions\UnauthorizedException) { - // Code here ... - } - - return parent::render($request, $exception); -} -``` From 6063707fa7e08c33efce9b7b4198c1c1c1ba1462 Mon Sep 17 00:00:00 2001 From: Stefan Riehl Date: Thu, 20 Feb 2020 09:56:05 +0100 Subject: [PATCH 0234/1013] implement wildcard permission feature --- config/permission.php | 7 + docs/advanced-usage/extending.md | 2 +- docs/basic-usage/wildcard-permissions.md | 62 +++++++ ...WildcardPermissionNotProperlyFormatted.php | 13 ++ src/Traits/HasPermissions.php | 42 ++++- src/WildcardPermission.php | 124 +++++++++++++ tests/HasPermissionsTest.php | 4 +- tests/WildcardHasPermissionsTest.php | 106 +++++++++++ tests/WildcardMiddlewareTest.php | 167 ++++++++++++++++++ tests/WildcardRouteTest.php | 60 +++++++ 10 files changed, 580 insertions(+), 7 deletions(-) create mode 100644 docs/basic-usage/wildcard-permissions.md create mode 100644 src/Exceptions/WildcardPermissionNotProperlyFormatted.php create mode 100644 src/WildcardPermission.php create mode 100644 tests/WildcardHasPermissionsTest.php create mode 100644 tests/WildcardMiddlewareTest.php create mode 100644 tests/WildcardRouteTest.php diff --git a/config/permission.php b/config/permission.php index 1a0b35a2f..74410d745 100644 --- a/config/permission.php +++ b/config/permission.php @@ -92,6 +92,13 @@ 'display_permission_in_exception' => false, + /* + * By default, wildcard permission usage is disabled. + * When set to true, wildcard permission handling is enabled. + */ + + 'enable_wildcard_permission' => false, + 'cache' => [ /* diff --git a/docs/advanced-usage/extending.md b/docs/advanced-usage/extending.md index 3500c4350..90ab421b5 100644 --- a/docs/advanced-usage/extending.md +++ b/docs/advanced-usage/extending.md @@ -10,7 +10,7 @@ If you are creating your own User models and wish Authorization features to be a ## Extending Role and Permission Models -If you are extending or replacing the role/permission models, you will need to specify your new models in this package's `config/permissions.php` file. +If you are extending or replacing the role/permission models, you will need to specify your new models in this package's `config/permission.php` file. First be sure that you've published the configuration file (see the Installation instructions), and edit it to update the `models.role` and `models.permission` values to point to your new models. diff --git a/docs/basic-usage/wildcard-permissions.md b/docs/basic-usage/wildcard-permissions.md new file mode 100644 index 000000000..111e95057 --- /dev/null +++ b/docs/basic-usage/wildcard-permissions.md @@ -0,0 +1,62 @@ +--- +title: Wildcard permissions +weight: 3 +--- + +Wildcard permissions can be enabled in the permission config file: + +```php +// config/permission.php +'enable_wildcard_permissions' => true, +``` + +When enabled, wildcard permissions offers you a flexible representation for a variety of permission schemes. The idea + behind wildcard permissions is based on the default permission implementation of + [Apache Shiro](https://shiro.apache.org/permissions.html). + +A wildcard permission string is made of one or more parts separated by dots (.). + +```php +$permission = 'posts.create.1'; +``` + +The meaning of each part of the string depends on the application layer. + +> You can use as many parts as you like. So you are not limited to the three-tiered structure, even though +this is the common use-case, representing {resource}.{action}.{target}. + +### Using Wildcards + +Each part can also have wildcards (*). So let's say we assign the following permission to a user: + +```php +$user->givePermissionTo('posts.*'); +// is the same as +$user->givePermissionTo('posts'); +``` + +Everyone who is assigned to this permission will be allowed every action on posts. It is not necessary to use a +wildcard on the last part of the string. This is automatically assumed. + +```php +// will be true +$user->can('posts.create'); +$user->can('posts.edit'); +$user->can('posts.delete'); +``` + +Besides the use of parts and wildcards, subparts can also be used. Subparts are divided with commas (,). This is a +powerful feature, that let's you create complex permission schemes. + +```php +// user can only do the actions create, update and view on both resources posts and users +$user->givePermissionTo('posts,users.create,update,view'); + +// user can do the actions create, update, view on any available resource +$user->givePermissionTo('*.create,update,view'); + +// user can do any action on posts with ids 1, 4 and 6 +$user->givePermissionTo('posts.*.1,4,6'); +``` + +> As said before, the meaning of each part is determined by the application layer! So, you are free to use each part as you like. And you can use as many parts and subparts as you want. diff --git a/src/Exceptions/WildcardPermissionNotProperlyFormatted.php b/src/Exceptions/WildcardPermissionNotProperlyFormatted.php new file mode 100644 index 000000000..721157e6c --- /dev/null +++ b/src/Exceptions/WildcardPermissionNotProperlyFormatted.php @@ -0,0 +1,13 @@ +getPermissionClass(); if (is_string($permission)) { - $permission = $permissionClass->findByName( - $permission, - $guardName ?? $this->getDefaultGuardName() - ); + try { + $permission = $permissionClass->findByName( + $permission, + $guardName ?? $this->getDefaultGuardName() + ); + } catch (PermissionDoesNotExist $e) { + // the permission as string is not present in the database + // so check wildcard permission if enabled in config + // Use sensitive default for backwards compatibility + if (config('permission.enable_wildcard_permission', false)) { + return $this->hasWildcardPermission($permission); + } + + throw new PermissionDoesNotExist(); + } } if (is_int($permission)) { @@ -133,6 +145,28 @@ public function hasPermissionTo($permission, $guardName = null): bool return $this->hasDirectPermission($permission) || $this->hasPermissionViaRole($permission); } + /** + * Validates a wildcard permission against all permissions of a user. + * + * @param string $permission + * + * @return bool + */ + protected function hasWildcardPermission(string $permission): bool + { + $permissionToVerify = new WildcardPermission($permission); + + foreach ($this->getAllPermissions() as $permission) { + $permission = new WildcardPermission($permission->name); + + if ($permission->implies($permissionToVerify)) { + return true; + } + } + + return false; + } + /** * @deprecated since 2.35.0 * @alias of hasPermissionTo() diff --git a/src/WildcardPermission.php b/src/WildcardPermission.php new file mode 100644 index 000000000..22d142ba0 --- /dev/null +++ b/src/WildcardPermission.php @@ -0,0 +1,124 @@ +permission = $permission; + $this->parts = collect(); + + $this->setParts(); + } + + /** + * @param string|WildcardPermission $permission + * + * @return bool + */ + public function implies($permission): bool + { + if (is_string($permission)) { + $permission = new self($permission); + } + + $otherParts = $permission->getParts(); + + $i = 0; + foreach ($otherParts as $otherPart) { + if ($this->getParts()->count() - 1 < $i) { + return true; + } + + if (! $this->parts->get($i)->contains(self::WILDCARD_TOKEN) + && ! $this->containsAll($this->parts->get($i), $otherPart)) { + return false; + } + + $i++; + } + + for ($i; $i < $this->parts->count(); $i++) { + if (! $this->parts->get($i)->contains(self::WILDCARD_TOKEN)) { + return false; + } + } + + return true; + } + + /** + * @param Collection $part + * @param Collection $otherPart + * + * @return bool + */ + protected function containsAll(Collection $part, Collection $otherPart): bool + { + foreach ($otherPart->toArray() as $item) { + if (! $part->contains($item)) { + return false; + } + } + + return true; + } + + /** + * @return Collection + */ + public function getParts(): Collection + { + return $this->parts; + } + + /** + * Sets the different parts and subparts from permission string. + * + * @return void + */ + protected function setParts(): void + { + if (empty($this->permission) || $this->permission == null) { + throw WildcardPermissionNotProperlyFormatted::create($this->permission); + } + + $parts = collect(explode(self::PART_DELIMITER, $this->permission)); + + $parts->each(function ($item, $key) { + $subParts = collect(explode(self::SUBPART_DELIMITER, $item)); + + if ($subParts->isEmpty() || $subParts->contains('')) { + throw WildcardPermissionNotProperlyFormatted::create($this->permission); + } + + $this->parts->add($subParts); + }); + + if ($this->parts->isEmpty()) { + throw WildcardPermissionNotProperlyFormatted::create($this->permission); + } + } +} diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index 93652a038..f572e1c46 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -234,7 +234,7 @@ public function it_throws_an_exception_when_the_permission_does_not_exist() { $this->expectException(PermissionDoesNotExist::class); - $this->testUser->hasPermissionTo('does-not-exist'); + $this->testUser->hasPermissionTo(7892); } /** @test */ @@ -242,7 +242,7 @@ public function it_throws_an_exception_when_the_permission_does_not_exist_for_th { $this->expectException(PermissionDoesNotExist::class); - $this->testUser->hasPermissionTo('admin-permission'); + $this->testUser->hasPermissionTo(83743847); } /** @test */ diff --git a/tests/WildcardHasPermissionsTest.php b/tests/WildcardHasPermissionsTest.php new file mode 100644 index 000000000..6b05bb77b --- /dev/null +++ b/tests/WildcardHasPermissionsTest.php @@ -0,0 +1,106 @@ +set('permission.enable_wildcard_permission', true); + + $user1 = User::create(['email' => 'user1@test.com']); + + $permission1 = Permission::create(['name' => 'articles.edit,view,create']); + $permission2 = Permission::create(['name' => 'news.*']); + $permission3 = Permission::create(['name' => 'posts.*']); + + $user1->givePermissionTo([$permission1, $permission2, $permission3]); + + $this->assertTrue($user1->hasPermissionTo('posts.create')); + $this->assertTrue($user1->hasPermissionTo('posts.create.123')); + $this->assertTrue($user1->hasPermissionTo('posts.*')); + $this->assertTrue($user1->hasPermissionTo('articles.view')); + $this->assertFalse($user1->hasPermissionTo('projects.view')); + } + + /** @test */ + public function it_can_check_wildcard_permissions_via_roles() + { + app('config')->set('permission.enable_wildcard_permission', true); + + $user1 = User::create(['email' => 'user1@test.com']); + + $user1->assignRole('testRole'); + + $permission1 = Permission::create(['name' => 'articles,projects.edit,view,create']); + $permission2 = Permission::create(['name' => 'news.*.456']); + $permission3 = Permission::create(['name' => 'posts']); + + $this->testUserRole->givePermissionTo([$permission1, $permission2, $permission3]); + + $this->assertTrue($user1->hasPermissionTo('posts.create')); + $this->assertTrue($user1->hasPermissionTo('news.create.456')); + $this->assertTrue($user1->hasPermissionTo('projects.create')); + $this->assertTrue($user1->hasPermissionTo('articles.view')); + $this->assertFalse($user1->hasPermissionTo('articles.list')); + $this->assertFalse($user1->hasPermissionTo('projects.list')); + } + + /** @test */ + public function it_can_check_non_wildcard_permissions() + { + app('config')->set('permission.enable_wildcard_permission', true); + + $user1 = User::create(['email' => 'user1@test.com']); + + $permission1 = Permission::create(['name' => 'edit articles']); + $permission2 = Permission::create(['name' => 'create news']); + $permission3 = Permission::create(['name' => 'update comments']); + + $user1->givePermissionTo([$permission1, $permission2, $permission3]); + + $this->assertTrue($user1->hasPermissionTo('edit articles')); + $this->assertTrue($user1->hasPermissionTo('create news')); + $this->assertTrue($user1->hasPermissionTo('update comments')); + } + + /** @test */ + public function it_can_verify_complex_wildcard_permissions() + { + app('config')->set('permission.enable_wildcard_permission', true); + + $user1 = User::create(['email' => 'user1@test.com']); + + $permission1 = Permission::create(['name' => '*.create,update,delete.*.test,course,finance']); + $permission2 = Permission::create(['name' => 'papers,posts,projects,orders.*.test,test1,test2.*']); + $permission3 = Permission::create(['name' => 'User::class.create,edit,view']); + + $user1->givePermissionTo([$permission1, $permission2, $permission3]); + + $this->assertTrue($user1->hasPermissionTo('invoices.delete.367463.finance')); + $this->assertTrue($user1->hasPermissionTo('projects.update.test2.test3')); + $this->assertTrue($user1->hasPermissionTo('User::class.edit')); + $this->assertFalse($user1->hasPermissionTo('User::class.delete')); + $this->assertFalse($user1->hasPermissionTo('User::class.*')); + } + + /** @test */ + public function it_throws_exception_when_wildcard_permission_is_not_properly_formatted() + { + app('config')->set('permission.enable_wildcard_permission', true); + + $user1 = User::create(['email' => 'user1@test.com']); + + $permission = Permission::create(['name' => '*..']); + + $user1->givePermissionTo([$permission]); + + $this->expectException(WildcardPermissionNotProperlyFormatted::class); + + $user1->hasPermissionTo('invoices.*'); + } +} diff --git a/tests/WildcardMiddlewareTest.php b/tests/WildcardMiddlewareTest.php new file mode 100644 index 000000000..8ee103fce --- /dev/null +++ b/tests/WildcardMiddlewareTest.php @@ -0,0 +1,167 @@ +roleMiddleware = new RoleMiddleware(); + + $this->permissionMiddleware = new PermissionMiddleware(); + + $this->roleOrPermissionMiddleware = new RoleOrPermissionMiddleware(); + + app('config')->set('permission.enable_wildcard_permission', true); + } + + /** @test */ + public function a_guest_cannot_access_a_route_protected_by_the_permission_middleware() + { + $this->assertEquals( + $this->runMiddleware( + $this->permissionMiddleware, 'articles.edit' + ), 403); + } + + /** @test */ + public function a_user_can_access_a_route_protected_by_permission_middleware_if_have_this_permission() + { + Auth::login($this->testUser); + + Permission::create(['name' => 'articles']); + + $this->testUser->givePermissionTo('articles'); + + $this->assertEquals( + $this->runMiddleware( + $this->permissionMiddleware, 'articles.edit' + ), 200); + } + + /** @test */ + public function a_user_can_access_a_route_protected_by_this_permission_middleware_if_have_one_of_the_permissions() + { + Auth::login($this->testUser); + + Permission::create(['name' => 'articles.*.test']); + + $this->testUser->givePermissionTo('articles.*.test'); + + $this->assertEquals( + $this->runMiddleware( + $this->permissionMiddleware, 'news.edit|articles.create.test' + ), 200); + + $this->assertEquals( + $this->runMiddleware( + $this->permissionMiddleware, ['news.edit', 'articles.create.test'] + ), 200); + } + + /** @test */ + public function a_user_cannot_access_a_route_protected_by_the_permission_middleware_if_have_a_different_permission() + { + Auth::login($this->testUser); + + Permission::create(['name' => 'articles.*']); + + $this->testUser->givePermissionTo('articles.*'); + + $this->assertEquals( + $this->runMiddleware( + $this->permissionMiddleware, 'news.edit' + ), 403); + } + + /** @test */ + public function a_user_cannot_access_a_route_protected_by_permission_middleware_if_have_not_permissions() + { + Auth::login($this->testUser); + + $this->assertEquals( + $this->runMiddleware( + $this->permissionMiddleware, 'articles.edit|news.edit' + ), 403); + } + + /** @test */ + public function a_user_can_access_a_route_protected_by_permission_or_role_middleware_if_has_this_permission_or_role() + { + Auth::login($this->testUser); + + Permission::create(['name' => 'articles.*']); + + $this->testUser->assignRole('testRole'); + $this->testUser->givePermissionTo('articles.*'); + + $this->assertEquals( + $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|news.edit|articles.create'), + 200 + ); + + $this->testUser->removeRole('testRole'); + + $this->assertEquals( + $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|articles.edit'), + 200 + ); + + $this->testUser->revokePermissionTo('articles.*'); + $this->testUser->assignRole('testRole'); + + $this->assertEquals( + $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|articles.edit'), + 200 + ); + + $this->assertEquals( + $this->runMiddleware($this->roleOrPermissionMiddleware, ['testRole', 'articles.edit']), + 200 + ); + } + + /** @test */ + public function the_required_permissions_can_be_fetched_from_the_exception() + { + Auth::login($this->testUser); + + $requiredPermissions = []; + + try { + $this->permissionMiddleware->handle(new Request(), function () { + return (new Response())->setContent(''); + }, 'permission.some'); + } catch (UnauthorizedException $e) { + $requiredPermissions = $e->getRequiredPermissions(); + } + + $this->assertEquals(['permission.some'], $requiredPermissions); + } + + protected function runMiddleware($middleware, $parameter) + { + try { + return $middleware->handle(new Request(), function () { + return (new Response())->setContent(''); + }, $parameter)->status(); + } catch (UnauthorizedException $e) { + return $e->getStatusCode(); + } + } +} diff --git a/tests/WildcardRouteTest.php b/tests/WildcardRouteTest.php new file mode 100644 index 000000000..77b75f1d8 --- /dev/null +++ b/tests/WildcardRouteTest.php @@ -0,0 +1,60 @@ +set('permission.enable_wildcard_permission', true); + + $router = $this->getRouter(); + + $router->get('permission-test', $this->getRouteResponse()) + ->name('permission.test') + ->permission(['articles.edit', 'articles.save']); + + $this->assertEquals(['permission:articles.edit|articles.save'], $this->getLastRouteMiddlewareFromRouter($router)); + } + + /** @test */ + public function test_role_and_permission_function_together() + { + app('config')->set('permission.enable_wildcard_permission', true); + + $router = $this->getRouter(); + + $router->get('role-permission-test', $this->getRouteResponse()) + ->name('role-permission.test') + ->role('superadmin|admin') + ->permission('user.create|user.edit'); + + $this->assertEquals( + [ + 'role:superadmin|admin', + 'permission:user.create|user.edit', + ], + $this->getLastRouteMiddlewareFromRouter($router) + ); + } + + protected function getLastRouteMiddlewareFromRouter($router) + { + return last($router->getRoutes()->get())->middleware(); + } + + protected function getRouter() + { + return app('router'); + } + + protected function getRouteResponse() + { + return function () { + return (new Response())->setContent(''); + }; + } +} From b63ae8ce628e0c69e65b4c844af389fe45d2602b Mon Sep 17 00:00:00 2001 From: Rias Date: Thu, 20 Feb 2020 10:23:26 +0100 Subject: [PATCH 0235/1013] Create deploy-docs.yml --- .github/workflows/deploy-docs.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/deploy-docs.yml diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 000000000..93cb059bb --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,19 @@ +name: deploy-docs + +on: + push: + paths: + - 'docs/**' + +jobs: + deploy: + + runs-on: ubuntu-latest + + name: Deploy docs + + steps: + - name: Netlify deploy + uses: wei/curl@v1 + with: + args: -X POST -d {} ${{ secrets.NETLIFY_DEPLOY_URL }}?trigger_title=laravel-permission From 3972346f51d7669401bafc68d0e02db989f97596 Mon Sep 17 00:00:00 2001 From: Stefan Riehl Date: Sat, 22 Feb 2020 18:40:48 +0100 Subject: [PATCH 0236/1013] implement new wildcard permission lookup --- config/permission.php | 3 +- docs/basic-usage/wildcard-permissions.md | 10 +- .../WildcardPermissionInvalidArgument.php | 13 +++ src/Models/Role.php | 4 + src/Traits/HasPermissions.php | 56 +++++---- tests/HasPermissionsTest.php | 4 +- tests/WildcardHasPermissionsTest.php | 77 +++++++++++++ tests/WildcardRoleTest.php | 107 ++++++++++++++++++ 8 files changed, 244 insertions(+), 30 deletions(-) create mode 100644 src/Exceptions/WildcardPermissionInvalidArgument.php create mode 100644 tests/WildcardRoleTest.php diff --git a/config/permission.php b/config/permission.php index 74410d745..940fcfc8d 100644 --- a/config/permission.php +++ b/config/permission.php @@ -93,8 +93,7 @@ 'display_permission_in_exception' => false, /* - * By default, wildcard permission usage is disabled. - * When set to true, wildcard permission handling is enabled. + * By default wildcard permission lookups are disabled. */ 'enable_wildcard_permission' => false, diff --git a/docs/basic-usage/wildcard-permissions.md b/docs/basic-usage/wildcard-permissions.md index 111e95057..0f9bd6a34 100644 --- a/docs/basic-usage/wildcard-permissions.md +++ b/docs/basic-usage/wildcard-permissions.md @@ -7,11 +7,11 @@ Wildcard permissions can be enabled in the permission config file: ```php // config/permission.php -'enable_wildcard_permissions' => true, +'enable_wildcard_permission' => true, ``` When enabled, wildcard permissions offers you a flexible representation for a variety of permission schemes. The idea - behind wildcard permissions is based on the default permission implementation of + behind wildcard permissions is inspired by the default permission implementation of [Apache Shiro](https://shiro.apache.org/permissions.html). A wildcard permission string is made of one or more parts separated by dots (.). @@ -27,7 +27,7 @@ this is the common use-case, representing {resource}.{action}.{target}. ### Using Wildcards -Each part can also have wildcards (*). So let's say we assign the following permission to a user: +Each part can also contains wildcards (*). So let's say we assign the following permission to a user: ```php $user->givePermissionTo('posts.*'); @@ -45,8 +45,10 @@ $user->can('posts.edit'); $user->can('posts.delete'); ``` +### Subparts + Besides the use of parts and wildcards, subparts can also be used. Subparts are divided with commas (,). This is a -powerful feature, that let's you create complex permission schemes. +powerful feature that lets you create complex permission schemes. ```php // user can only do the actions create, update and view on both resources posts and users diff --git a/src/Exceptions/WildcardPermissionInvalidArgument.php b/src/Exceptions/WildcardPermissionInvalidArgument.php new file mode 100644 index 000000000..ce57d736f --- /dev/null +++ b/src/Exceptions/WildcardPermissionInvalidArgument.php @@ -0,0 +1,13 @@ +hasWildcardPermission($permission, $this->getDefaultGuardName()); + } + $permissionClass = $this->getPermissionClass(); if (is_string($permission)) { diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 792fc9e43..fb86289ea 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -2,6 +2,8 @@ namespace Spatie\Permission\Traits; +use Spatie\Permission\Exceptions\WildcardPermissionInvalidArgument; +use Spatie\Permission\Exceptions\WildcardPermissionNotProperlyFormatted; use Spatie\Permission\Guard; use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Builder; @@ -111,24 +113,17 @@ protected function convertToPermissionModels($permissions): array */ public function hasPermissionTo($permission, $guardName = null): bool { + if (config('permission.enable_wildcard_permission', false)) { + return $this->hasWildcardPermission($permission, $guardName); + } + $permissionClass = $this->getPermissionClass(); if (is_string($permission)) { - try { - $permission = $permissionClass->findByName( - $permission, - $guardName ?? $this->getDefaultGuardName() - ); - } catch (PermissionDoesNotExist $e) { - // the permission as string is not present in the database - // so check wildcard permission if enabled in config - // Use sensitive default for backwards compatibility - if (config('permission.enable_wildcard_permission', false)) { - return $this->hasWildcardPermission($permission); - } - - throw new PermissionDoesNotExist(); - } + $permission = $permissionClass->findByName( + $permission, + $guardName ?? $this->getDefaultGuardName() + ); } if (is_int($permission)) { @@ -138,7 +133,7 @@ public function hasPermissionTo($permission, $guardName = null): bool ); } - if (! $permission instanceof Permission) { + if (!$permission instanceof Permission) { throw new PermissionDoesNotExist; } @@ -148,18 +143,35 @@ public function hasPermissionTo($permission, $guardName = null): bool /** * Validates a wildcard permission against all permissions of a user. * - * @param string $permission + * @param string|int|\Spatie\Permission\Contracts\Permission $permission + * @param string|null $guardName * * @return bool */ - protected function hasWildcardPermission(string $permission): bool + protected function hasWildcardPermission($permission, $guardName = null): bool { - $permissionToVerify = new WildcardPermission($permission); + $guardName = $guardName ?? $this->getDefaultGuardName(); + + if (is_int($permission)) { + $permission = $this->getPermissionClass()->findById($permission, $guardName); + } + + if ($permission instanceof Permission) { + $permission = $permission->name; + } + + if (! is_string($permission)) { + throw WildcardPermissionInvalidArgument::create(); + } + + foreach ($this->getAllPermissions() as $userPermission) { + if ($guardName !== $userPermission->guard_name) { + continue; + } - foreach ($this->getAllPermissions() as $permission) { - $permission = new WildcardPermission($permission->name); + $userPermission = new WildcardPermission($userPermission->name); - if ($permission->implies($permissionToVerify)) { + if ($userPermission->implies($permission)) { return true; } } diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index f572e1c46..68586c580 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -234,7 +234,7 @@ public function it_throws_an_exception_when_the_permission_does_not_exist() { $this->expectException(PermissionDoesNotExist::class); - $this->testUser->hasPermissionTo(7892); + $this->testUser->hasPermissionTo('does-not-exist'); } /** @test */ @@ -242,7 +242,7 @@ public function it_throws_an_exception_when_the_permission_does_not_exist_for_th { $this->expectException(PermissionDoesNotExist::class); - $this->testUser->hasPermissionTo(83743847); + $this->testUser->hasPermissionTo('does-not-exist'); } /** @test */ diff --git a/tests/WildcardHasPermissionsTest.php b/tests/WildcardHasPermissionsTest.php index 6b05bb77b..58807c0d0 100644 --- a/tests/WildcardHasPermissionsTest.php +++ b/tests/WildcardHasPermissionsTest.php @@ -2,8 +2,11 @@ namespace Spatie\Permission\Test; +use Spatie\Permission\Exceptions\PermissionDoesNotExist; +use Spatie\Permission\Exceptions\WildcardPermissionInvalidArgument; use Spatie\Permission\Models\Permission; use Spatie\Permission\Exceptions\WildcardPermissionNotProperlyFormatted; +use Spatie\Permission\Models\Role; class WildcardHasPermissionsTest extends TestCase { @@ -103,4 +106,78 @@ public function it_throws_exception_when_wildcard_permission_is_not_properly_for $user1->hasPermissionTo('invoices.*'); } + + /** @test */ + public function it_can_verify_permission_instances_not_assigned_to_user() + { + app('config')->set('permission.enable_wildcard_permission', true); + + $user = User::create(['email' => 'user@test.com']); + + $userPermission = Permission::create(['name' => 'posts.*']); + $permissionToVerify = Permission::create(['name' => 'posts.create']); + + $user->givePermissionTo([$userPermission]); + + $this->assertTrue($user->hasPermissionTo('posts.create')); + $this->assertTrue($user->hasPermissionTo('posts.create.123')); + $this->assertTrue($user->hasPermissionTo($permissionToVerify->id)); + $this->assertTrue($user->hasPermissionTo($permissionToVerify)); + } + + /** @test */ + public function it_can_verify_permission_instances_assigned_to_user() + { + app('config')->set('permission.enable_wildcard_permission', true); + + $user = User::create(['email' => 'user@test.com']); + + $userPermission = Permission::create(['name' => 'posts.*']); + $permissionToVerify = Permission::create(['name' => 'posts.create']); + + $user->givePermissionTo([$userPermission, $permissionToVerify]); + + $this->assertTrue($user->hasPermissionTo('posts.create')); + $this->assertTrue($user->hasPermissionTo('posts.create.123')); + $this->assertTrue($user->hasPermissionTo($permissionToVerify)); + $this->assertTrue($user->hasPermissionTo($userPermission)); + } + + /** @test */ + public function it_can_verify_integers_as_strings() + { + app('config')->set('permission.enable_wildcard_permission', true); + + $user = User::create(['email' => 'user@test.com']); + + $userPermission = Permission::create(['name' => '8']); + + $user->givePermissionTo([$userPermission]); + + $this->assertTrue($user->hasPermissionTo('8')); + } + + /** @test */ + public function it_throws_exception_when_permission_has_invalid_arguments() + { + app('config')->set('permission.enable_wildcard_permission', true); + + $user = User::create(['email' => 'user@test.com']); + + $this->expectException(WildcardPermissionInvalidArgument::class); + + $user->hasPermissionTo(['posts.create']); + } + + /** @test */ + public function it_throws_exception_when_permission_id_not_exists() + { + app('config')->set('permission.enable_wildcard_permission', true); + + $user = User::create(['email' => 'user@test.com']); + + $this->expectException(PermissionDoesNotExist::class); + + $user->hasPermissionTo(6); + } } diff --git a/tests/WildcardRoleTest.php b/tests/WildcardRoleTest.php new file mode 100644 index 000000000..6da77d662 --- /dev/null +++ b/tests/WildcardRoleTest.php @@ -0,0 +1,107 @@ +set('permission.enable_wildcard_permission', true); + + Permission::create(['name' => 'other-permission']); + + Permission::create(['name' => 'wrong-guard-permission', 'guard_name' => 'admin']); + } + + /** @test */ + public function it_can_be_given_a_permission() + { + Permission::create(['name' => 'posts.*']); + $this->testUserRole->givePermissionTo('posts.*'); + + $this->assertTrue($this->testUserRole->hasPermissionTo('posts.create')); + } + + /** @test */ + public function it_can_be_given_multiple_permissions_using_an_array() + { + Permission::create(['name' => 'posts.*']); + Permission::create(['name' => 'news.*']); + + $this->testUserRole->givePermissionTo(['posts.*', 'news.*']); + + $this->assertTrue($this->testUserRole->hasPermissionTo('posts.create')); + $this->assertTrue($this->testUserRole->hasPermissionTo('news.create')); + } + + /** @test */ + public function it_can_be_given_multiple_permissions_using_multiple_arguments() + { + Permission::create(['name' => 'posts.*']); + Permission::create(['name' => 'news.*']); + + $this->testUserRole->givePermissionTo('posts.*', 'news.*'); + + $this->assertTrue($this->testUserRole->hasPermissionTo('posts.edit.123')); + $this->assertTrue($this->testUserRole->hasPermissionTo('news.view.1')); + } + + /** @test */ + public function it_can_be_given_a_permission_using_objects() + { + $this->testUserRole->givePermissionTo($this->testUserPermission); + + $this->assertTrue($this->testUserRole->hasPermissionTo($this->testUserPermission)); + } + + /** @test */ + public function it_returns_false_if_it_does_not_have_the_permission() + { + $this->assertFalse($this->testUserRole->hasPermissionTo('other-permission')); + } + + /** @test */ + public function it_returns_false_if_permission_does_not_exists() + { + $this->assertFalse($this->testUserRole->hasPermissionTo('doesnt-exist')); + } + + /** @test */ + public function it_returns_false_if_it_does_not_have_a_permission_object() + { + $permission = app(Permission::class)->findByName('other-permission'); + + $this->assertFalse($this->testUserRole->hasPermissionTo($permission)); + } + + /** @test */ + public function it_creates_permission_object_with_findOrCreate_if_it_does_not_have_a_permission_object() + { + $permission = app(Permission::class)->findOrCreate('another-permission'); + + $this->assertFalse($this->testUserRole->hasPermissionTo($permission)); + + $this->testUserRole->givePermissionTo($permission); + + $this->testUserRole = $this->testUserRole->fresh(); + + $this->assertTrue($this->testUserRole->hasPermissionTo('another-permission')); + } + + /** @test */ + public function it_returns_false_when_a_permission_of_the_wrong_guard_is_passed_in() + { + $permission = app(Permission::class)->findByName('wrong-guard-permission', 'admin'); + + $this->assertFalse($this->testUserRole->hasPermissionTo($permission)); + } +} From 409249a79d28ebc1190b6babd3ff6532d73dc64d Mon Sep 17 00:00:00 2001 From: Stefan Riehl Date: Sat, 22 Feb 2020 18:48:41 +0100 Subject: [PATCH 0237/1013] fix styleci --- src/Exceptions/WildcardPermissionInvalidArgument.php | 2 +- src/Traits/HasPermissions.php | 5 ++--- tests/WildcardHasPermissionsTest.php | 3 +-- tests/WildcardRoleTest.php | 5 ----- 4 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/Exceptions/WildcardPermissionInvalidArgument.php b/src/Exceptions/WildcardPermissionInvalidArgument.php index ce57d736f..44f092401 100644 --- a/src/Exceptions/WildcardPermissionInvalidArgument.php +++ b/src/Exceptions/WildcardPermissionInvalidArgument.php @@ -8,6 +8,6 @@ class WildcardPermissionInvalidArgument extends InvalidArgumentException { public static function create() { - return new static("Wildcard permission must be string, permission id or permission instance"); + return new static('Wildcard permission must be string, permission id or permission instance'); } } diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index fb86289ea..c66cfe752 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -2,8 +2,6 @@ namespace Spatie\Permission\Traits; -use Spatie\Permission\Exceptions\WildcardPermissionInvalidArgument; -use Spatie\Permission\Exceptions\WildcardPermissionNotProperlyFormatted; use Spatie\Permission\Guard; use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Builder; @@ -13,6 +11,7 @@ use Spatie\Permission\Exceptions\GuardDoesNotMatch; use Illuminate\Database\Eloquent\Relations\MorphToMany; use Spatie\Permission\Exceptions\PermissionDoesNotExist; +use Spatie\Permission\Exceptions\WildcardPermissionInvalidArgument; trait HasPermissions { @@ -133,7 +132,7 @@ public function hasPermissionTo($permission, $guardName = null): bool ); } - if (!$permission instanceof Permission) { + if (! $permission instanceof Permission) { throw new PermissionDoesNotExist; } diff --git a/tests/WildcardHasPermissionsTest.php b/tests/WildcardHasPermissionsTest.php index 58807c0d0..3cf12d6c6 100644 --- a/tests/WildcardHasPermissionsTest.php +++ b/tests/WildcardHasPermissionsTest.php @@ -2,11 +2,10 @@ namespace Spatie\Permission\Test; +use Spatie\Permission\Models\Permission; use Spatie\Permission\Exceptions\PermissionDoesNotExist; use Spatie\Permission\Exceptions\WildcardPermissionInvalidArgument; -use Spatie\Permission\Models\Permission; use Spatie\Permission\Exceptions\WildcardPermissionNotProperlyFormatted; -use Spatie\Permission\Models\Role; class WildcardHasPermissionsTest extends TestCase { diff --git a/tests/WildcardRoleTest.php b/tests/WildcardRoleTest.php index 6da77d662..6d69c9d2c 100644 --- a/tests/WildcardRoleTest.php +++ b/tests/WildcardRoleTest.php @@ -2,12 +2,7 @@ namespace Spatie\Permission\Test; -use Spatie\Permission\Contracts\Role; use Spatie\Permission\Models\Permission; -use Spatie\Permission\Exceptions\RoleDoesNotExist; -use Spatie\Permission\Exceptions\GuardDoesNotMatch; -use Spatie\Permission\Exceptions\RoleAlreadyExists; -use Spatie\Permission\Exceptions\PermissionDoesNotExist; class WildcardRoleTest extends TestCase { From 47400a888bc193f3a60cf1eb4362f1e5fcce8963 Mon Sep 17 00:00:00 2001 From: Stefan Riehl Date: Wed, 26 Feb 2020 10:13:34 +0100 Subject: [PATCH 0238/1013] correct typo --- docs/basic-usage/wildcard-permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/wildcard-permissions.md b/docs/basic-usage/wildcard-permissions.md index 0f9bd6a34..deb763f0f 100644 --- a/docs/basic-usage/wildcard-permissions.md +++ b/docs/basic-usage/wildcard-permissions.md @@ -27,7 +27,7 @@ this is the common use-case, representing {resource}.{action}.{target}. ### Using Wildcards -Each part can also contains wildcards (*). So let's say we assign the following permission to a user: +Each part can also contain wildcards (*). So let's say we assign the following permission to a user: ```php $user->givePermissionTo('posts.*'); From ac0fc4cd809b1fe7ecbe9876e03bb5849601bf66 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 26 Feb 2020 11:23:16 -0500 Subject: [PATCH 0239/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93d3aca18..19c3c1fff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 3.9.0 - 2020-02-26 +- Add Wildcard Permissions feature #1381 (see PR or docs for details) + ## 3.8.0 - 2020-02-18 - Clear in-memory permissions on boot, for benefit of long running processes like Swoole. #1378 From 602951f814ee8ec3672330edb848f0197a0ce7e9 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 27 Feb 2020 13:47:06 -0500 Subject: [PATCH 0240/1013] Update using-policies.md --- docs/best-practices/using-policies.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/best-practices/using-policies.md b/docs/best-practices/using-policies.md index 056ada600..be943fed4 100644 --- a/docs/best-practices/using-policies.md +++ b/docs/best-practices/using-policies.md @@ -7,4 +7,6 @@ The best way to incorporate access control for application features is with [Lar Using Policies allows you to simplify things by abstracting your "control" rules into one place, where your application logic can be combined with your permission rules. +Jeffrey Way explains the concept simply in the [Laravel 6 Authorization Filters](https://laracasts.com/series/laravel-6-from-scratch/episodes/51) video and some related lessons in that chapter. He also mentions how to set up a super-admin, both in a model policy and globally in your application. + You can find an example of implementing a model policy with this Laravel Permissions package in this demo app: https://github.com/drbyte/spatie-permissions-demo/blob/master/app/Policies/PostPolicy.php From 0cdea444cde7e6da505851b9e6b861e23417b22f Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 27 Feb 2020 13:48:25 -0500 Subject: [PATCH 0241/1013] Update super-admin.md --- docs/basic-usage/super-admin.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/basic-usage/super-admin.md b/docs/basic-usage/super-admin.md index beab973c2..d48ad254f 100644 --- a/docs/basic-usage/super-admin.md +++ b/docs/basic-usage/super-admin.md @@ -31,6 +31,8 @@ class AuthServiceProvider extends ServiceProvider NOTE: `Gate::before` rules need to return `null` rather than `false`, else it will interfere with normal policy operation. [See more.](https://laracasts.com/discuss/channels/laravel/policy-gets-never-called#reply=492526) +Jeffrey Way explains the concept of a super-admin (and a model owner, and model policies) in the [Laravel 6 Authorization Filters](https://laracasts.com/series/laravel-6-from-scratch/episodes/51) video and some related lessons in that chapter. + ## `Gate::after` From ed7c39b3a772c06937098701f35705e70c0b4620 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 27 Feb 2020 16:21:37 -0500 Subject: [PATCH 0242/1013] Update wildcard-permissions.md --- docs/basic-usage/wildcard-permissions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/basic-usage/wildcard-permissions.md b/docs/basic-usage/wildcard-permissions.md index deb763f0f..03675b122 100644 --- a/docs/basic-usage/wildcard-permissions.md +++ b/docs/basic-usage/wildcard-permissions.md @@ -25,6 +25,8 @@ The meaning of each part of the string depends on the application layer. > You can use as many parts as you like. So you are not limited to the three-tiered structure, even though this is the common use-case, representing {resource}.{action}.{target}. +> NOTE: You must actually create the permissions before you can assign them. + ### Using Wildcards Each part can also contain wildcards (*). So let's say we assign the following permission to a user: From 01ea7dc4ab92c83f986f8f4539478d2b6778ec90 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 27 Feb 2020 19:57:20 -0500 Subject: [PATCH 0243/1013] Update deploy-docs.yml Skip deploy if secret not defined (such as when in a forked repo) --- .github/workflows/deploy-docs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 93cb059bb..e87931df7 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -8,6 +8,8 @@ on: jobs: deploy: + if: secrets.NETLIFY_DEPLOY_URL != '' + runs-on: ubuntu-latest name: Deploy docs From 5a5dcfcdad353d18bd21a94b3242332f5f54c35d Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 28 Feb 2020 06:02:30 -0500 Subject: [PATCH 0244/1013] Update deploy-docs.yml Only fire on master branch --- .github/workflows/deploy-docs.yml | 4 ++-- docs/changelog.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index e87931df7..056cec269 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -2,14 +2,14 @@ name: deploy-docs on: push: + branches: + - master paths: - 'docs/**' jobs: deploy: - if: secrets.NETLIFY_DEPLOY_URL != '' - runs-on: ubuntu-latest name: Deploy docs diff --git a/docs/changelog.md b/docs/changelog.md index 27d734a9d..b07e4b489 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,3 +4,4 @@ weight: 10 --- All notable changes to laravel-permission are documented [on GitHub](https://github.com/spatie/laravel-permission/blob/master/CHANGELOG.md) + From 29d5d608870a4de5384a60dd90270c6e40cf115e Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 28 Feb 2020 07:29:09 -0500 Subject: [PATCH 0245/1013] update changelog.md --- docs/changelog.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index b07e4b489..27d734a9d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,4 +4,3 @@ weight: 10 --- All notable changes to laravel-permission are documented [on GitHub](https://github.com/spatie/laravel-permission/blob/master/CHANGELOG.md) - From f5ce7714a2a8f1b0bc9d441f8d19b5c31a227816 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 2 Mar 2020 12:55:35 -0500 Subject: [PATCH 0246/1013] Minor updates to tests - reformatted slightly for clarity - added extended case for thoroughness --- tests/PermissionTest.php | 5 ++++- tests/RoleTest.php | 9 ++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/PermissionTest.php b/tests/PermissionTest.php index f1f009400..35f02d0b0 100644 --- a/tests/PermissionTest.php +++ b/tests/PermissionTest.php @@ -27,7 +27,10 @@ public function it_belongs_to_a_guard() /** @test */ public function it_belongs_to_the_default_guard_by_default() { - $this->assertEquals($this->app['config']->get('auth.defaults.guard'), $this->testUserPermission->guard_name); + $this->assertEquals( + $this->app['config']->get('auth.defaults.guard'), + $this->testUserPermission->guard_name + ); } /** @test */ diff --git a/tests/RoleTest.php b/tests/RoleTest.php index 207a4c6ef..01703213c 100644 --- a/tests/RoleTest.php +++ b/tests/RoleTest.php @@ -30,6 +30,10 @@ public function it_has_user_models_of_the_right_class() $this->assertCount(1, $this->testUserRole->users); $this->assertTrue($this->testUserRole->users->first()->is($this->testUser)); $this->assertInstanceOf(User::class, $this->testUserRole->users->first()); + + $this->assertCount(1, $this->testAdminRole->users); + $this->assertTrue($this->testAdminRole->users->first()->is($this->testAdmin)); + $this->assertInstanceOf(Admin::class, $this->testAdminRole->users->first()); } /** @test */ @@ -230,6 +234,9 @@ public function it_belongs_to_a_guard() /** @test */ public function it_belongs_to_the_default_guard_by_default() { - $this->assertEquals($this->app['config']->get('auth.defaults.guard'), $this->testUserRole->guard_name); + $this->assertEquals( + $this->app['config']->get('auth.defaults.guard'), + $this->testUserRole->guard_name + ); } } From 5a5c41c0816fed4d22995ad3d9a30545d414a3ac Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 2 Mar 2020 14:51:25 -0500 Subject: [PATCH 0247/1013] Fix docblock --- src/Commands/CreateRole.php | 3 +++ src/PermissionRegistrar.php | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Commands/CreateRole.php b/src/Commands/CreateRole.php index 975a21baa..b43db26e0 100644 --- a/src/Commands/CreateRole.php +++ b/src/Commands/CreateRole.php @@ -26,6 +26,9 @@ public function handle() $this->info("Role `{$role->name}` created"); } + /** + * @param array|null|string $string + */ protected function makePermissions($string = null) { if (empty($string)) { diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 581e459e9..a0ee15054 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -26,7 +26,7 @@ class PermissionRegistrar /** @var \Illuminate\Support\Collection */ protected $permissions; - /** @var DateInterval|int */ + /** @var \DateInterval|int */ public static $cacheExpirationTime; /** @var string */ From 61760ac38c6d37b5dc913a1ef6f481e82ccddcb5 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 2 Mar 2020 16:04:51 -0500 Subject: [PATCH 0248/1013] Update HasPermissionsTest.php --- tests/HasPermissionsTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index 68586c580..b49a9ea58 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -242,11 +242,11 @@ public function it_throws_an_exception_when_the_permission_does_not_exist_for_th { $this->expectException(PermissionDoesNotExist::class); - $this->testUser->hasPermissionTo('does-not-exist'); + $this->testUser->hasPermissionTo('does-not-exist', 'web'); } /** @test */ - public function it_can_work_with_a_user_that_does_not_have_any_permissions_at_all() + public function it_can_reject_a_user_that_does_not_have_any_permissions_at_all() { $user = new User(); From 4674735041203e051e286026d9b4a6ab25bc5a11 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 2 Mar 2020 16:27:41 -0500 Subject: [PATCH 0249/1013] Avoid odd config merge issue from #1370 Really not sure what environmental condition is causing this to be needed suddenly. Need more details to reproduce the problem consistently in order to find a more elegant solution. --- src/PermissionServiceProvider.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index edd46bc26..7f843af5f 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -57,6 +57,10 @@ protected function registerModelBindings() { $config = $this->app->config['permission.models']; + if (! $config) { // for odd config merge issue in #1370 + return; + } + $this->app->bind(PermissionContract::class, $config['permission']); $this->app->bind(RoleContract::class, $config['role']); } From e3a95d2a954b4608c256095db5c8c0c416bcb71e Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 2 Mar 2020 16:32:49 -0500 Subject: [PATCH 0250/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19c3c1fff..869313251 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 3.10.0 - 2020-03-02 +- Ugly patch to handle intermittent error: `Trying to access array offset on value of type null` in #1370 + ## 3.9.0 - 2020-02-26 - Add Wildcard Permissions feature #1381 (see PR or docs for details) From 95aa658a9b5040c86c92b96b13ceab6dda19ed6a Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 2 Mar 2020 17:16:19 -0500 Subject: [PATCH 0251/1013] Update run-tests.yml --- .github/workflows/run-tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 61ce7a498..0189d8038 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -10,9 +10,11 @@ jobs: fail-fast: true matrix: php: [7.4, 7.3, 7.2] - laravel: [6.*, 5.8.*] + laravel: [7.*, 6.*, 5.8.*] dependency-version: [prefer-lowest, prefer-stable] include: + - laravel: 7.* + testbench: 5.* - laravel: 6.* testbench: 4.* - laravel: 5.8.* From bb3de6e04ae940120c3bef8bf5188ff29941733b Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 2 Mar 2020 17:18:10 -0500 Subject: [PATCH 0252/1013] Update run-tests.yml --- .github/workflows/run-tests.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 0189d8038..355782e33 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -10,11 +10,9 @@ jobs: fail-fast: true matrix: php: [7.4, 7.3, 7.2] - laravel: [7.*, 6.*, 5.8.*] + laravel: [6.*, 5.8.*] dependency-version: [prefer-lowest, prefer-stable] include: - - laravel: 7.* - testbench: 5.* - laravel: 6.* testbench: 4.* - laravel: 5.8.* @@ -33,7 +31,7 @@ jobs: key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} - name: Setup PHP - uses: shivammathur/setup-php@v1 + uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick From 8d899ec93449e4fd11408753ec7b4ac1e4b877f3 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 2 Mar 2020 17:19:52 -0500 Subject: [PATCH 0253/1013] Update run-tests.yml --- .github/workflows/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 355782e33..ab84ec26a 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -39,7 +39,7 @@ jobs: - name: Install dependencies run: | - composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:^4.3.4" --no-interaction --no-update + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest - name: Execute tests From 9e6d9558c3175289df097045d81692ee0c3522a0 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 2 Mar 2020 17:25:15 -0500 Subject: [PATCH 0254/1013] Update run-tests.yml --- .github/workflows/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index ab84ec26a..ddb046aeb 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -39,7 +39,7 @@ jobs: - name: Install dependencies run: | - composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:>=4.3.4" --no-interaction --no-update composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest - name: Execute tests From 13da600606391eaaaf0a07da076070cb302a7545 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 2 Mar 2020 17:29:24 -0500 Subject: [PATCH 0255/1013] Upgrade phpunit support --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 796562f25..e116df603 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ }, "require-dev": { "orchestra/testbench": "^3.8|^4.0|^5.0", - "phpunit/phpunit": "^8.0", + "phpunit/phpunit": "^8.0|^9.0", "predis/predis": "^1.1" }, "autoload": { From b2394f75c441ab11d5c5368b920c77467edb367e Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 2 Mar 2020 17:32:52 -0500 Subject: [PATCH 0256/1013] Update run-tests.yml --- .github/workflows/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index ddb046aeb..ba07f673c 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -34,7 +34,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick + extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv coverage: none - name: Install dependencies From 3161d2fab09232690d3139d16b66696c1cc99db9 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 2 Mar 2020 17:38:52 -0500 Subject: [PATCH 0257/1013] Update composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e116df603..7066150f7 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ } ], "require": { - "php" : "^7.2", + "php" : "^7.2.5", "illuminate/auth": "^5.8|^6.0|^7.0", "illuminate/container": "^5.8|^6.0|^7.0", "illuminate/contracts": "^5.8|^6.0|^7.0", From 1954ca4f9aafe5b33c2ab727551f08506c5c9d69 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 2 Mar 2020 17:40:19 -0500 Subject: [PATCH 0258/1013] Update run-tests.yml --- .github/workflows/run-tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index ba07f673c..8b2e37fb1 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -10,9 +10,11 @@ jobs: fail-fast: true matrix: php: [7.4, 7.3, 7.2] - laravel: [6.*, 5.8.*] + laravel: [7.*, 6.*, 5.8.*] dependency-version: [prefer-lowest, prefer-stable] include: + - laravel: 7.* + testbench: 5.* - laravel: 6.* testbench: 4.* - laravel: 5.8.* From 8c4f78795e1b0f1fb542eec7a9745877f47a60a2 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 2 Mar 2020 16:40:39 -0500 Subject: [PATCH 0259/1013] Allow guardName() as a function with priority over guard_name property Fixes #1203 --- src/Guard.php | 6 +++++- tests/Manager.php | 31 +++++++++++++++++++++++++++++++ tests/MultipleGuardsTest.php | 17 +++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 tests/Manager.php diff --git a/src/Guard.php b/src/Guard.php index 48ac56b3c..df63b432e 100644 --- a/src/Guard.php +++ b/src/Guard.php @@ -15,7 +15,11 @@ class Guard public static function getNames($model): Collection { if (is_object($model)) { - $guardName = $model->guard_name ?? null; + if (\method_exists($model, 'guardName')) { + $guardName = $model->guardName(); + } else { + $guardName = $model->guard_name ?? null; + } } if (! isset($guardName)) { diff --git a/tests/Manager.php b/tests/Manager.php new file mode 100644 index 000000000..e2928e18d --- /dev/null +++ b/tests/Manager.php @@ -0,0 +1,31 @@ + ['driver' => 'abc'], ]); } + + /** @test */ + public function it_can_honour_guardName_function_on_model_for_overriding_guard_name_property() + { + $user = Manager::create(['email' => 'manager@test.com']); + $user->givePermissionTo(Permission::create([ + 'name' => 'do_that', + 'guard_name' => 'api', + ])); + + // Manager test user has the guardName override method, which returns 'api' + $this->assertTrue($user->checkPermissionTo('do_that', 'api')); + $this->assertTrue($user->hasPermissionTo('do_that', 'api')); + + // Manager test user has the $guard_name property set to 'web' + $this->assertFalse($user->checkPermissionTo('do_that', 'web')); + } } From 1fe850897ba76bc66ccbd797f0963632f36d59b2 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 3 Mar 2020 12:09:02 -0500 Subject: [PATCH 0260/1013] Update multiple-guards.md --- docs/basic-usage/multiple-guards.md | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/docs/basic-usage/multiple-guards.md b/docs/basic-usage/multiple-guards.md index 69db92425..564c232ea 100644 --- a/docs/basic-usage/multiple-guards.md +++ b/docs/basic-usage/multiple-guards.md @@ -3,17 +3,24 @@ title: Using multiple guards weight: 6 --- -When using the default Laravel auth configuration all of the above methods will work out of the box, no extra configuration required. +When using the default Laravel auth configuration all of the core methods of this package will work out of the box, no extra configuration required. However, when using multiple guards they will act like namespaces for your permissions and roles. Meaning every guard has its own set of permissions and roles that can be assigned to their user model. +### The Downside To Multiple Guards + +Note that this package requires you to register a permission name for each guard you want to authenticate with. So, "edit-article" would have to be created multiple times for each guard your app uses. An exception will be thrown if you try to authenticate against a non-existing permission+guard combination. Same for roles. + +> **Tip**: If your app uses only a single guard, but is not `web` (Laravel's default, which shows "first" in the auth config file) then change the order of your listed guards in your `config/auth.php` to list your primary guard as the default and as the first in the list of defined guards. While you're editing that file, best to remove any guards you don't use, too. + + ### Using permissions and roles with multiple guards -When creating new permissions and roles, if no guard is specified, then the **first** defined guard in `auth.guards` config array will be used. When creating permissions and roles for specific guards you'll have to specify their `guard_name` on the model: +When creating new permissions and roles, if no guard is specified, then the **first** defined guard in `auth.guards` config array will be used. ```php -// Create a superadmin role for the admin users -$role = Role::create(['guard_name' => 'admin', 'name' => 'superadmin']); +// Create a manager role for users authenticating with the admin guard: +$role = Role::create(['guard_name' => 'admin', 'name' => 'manager']); // Define a `publish articles` permission for the admin users belonging to the admin guard $permission = Permission::create(['guard_name' => 'admin', 'name' => 'publish articles']); @@ -28,19 +35,21 @@ To check if a user has permission for a specific guard: $user->hasPermissionTo('publish articles', 'admin'); ``` -> **Note**: When determining whether a role/permission is valid on a given model, it chooses the guard in this order: first the `$guard_name` property of the model; then the guard in the config (through a provider); then the first-defined guard in the `auth.guards` config array; then the `auth.defaults.guard` config. - -> **Note**: When using other than the default `web` guard, you will need to declare which `guard_name` you wish each model to use by setting the `$guard_name` property in your model. One per model is simplest. +> **Note**: When determining whether a role/permission is valid on a given model, it checks against the first matching guard in this order (it does NOT check role/permission for EACH possibility, just the first match): +- first the guardName() method if it exists on the model; +- then the `$guard_name` property if it exists on the model; +- then the first-defined guard/provider combination in the `auth.guards` config array that matches the logged-in user's guard; +- then the `auth.defaults.guard` config (which is the user's guard if they are logged in, else the default in the file). -> **Note**: If your app uses only a single guard, but is not `web` then change the order of your listed guards in your `config/auth.php` to list your primary guard as the default and as the first in the list of defined guards. ### Assigning permissions and roles to guard users -You can use the same methods to assign permissions and roles to users as described above in [using permissions via roles](#using-permissions-via-roles). Just make sure the `guard_name` on the permission or role matches the guard of the user, otherwise a `GuardDoesNotMatch` exception will be thrown. +You can use the same core methods to assign permissions and roles to users; just make sure the `guard_name` on the permission or role matches the guard of the user, otherwise a `GuardDoesNotMatch` or `Role/PermissionDoesNotExist` exception will be thrown. + ### Using blade directives with multiple guards -You can use all of the blade directives listed in [using blade directives](#using-blade-directives) by passing in the guard you wish to use as the second argument to the directive: +You can use all of the blade directives offered by this package by passing in the guard you wish to use as the second argument to the directive: ```php @role('super-admin', 'admin') From b0861793b5942eeaaa27cd38787fba7666c10d3c Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 3 Mar 2020 12:38:17 -0500 Subject: [PATCH 0261/1013] Avoid config merge issue from #1370 --- CHANGELOG.md | 3 +++ database/migrations/create_permission_tables.php.stub | 8 ++++++++ src/PermissionServiceProvider.php | 2 +- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 869313251..1a6988609 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 3.10.1 - 2020-03-03 +- Update patch to handle intermittent error in #1370 + ## 3.10.0 - 2020-03-02 - Ugly patch to handle intermittent error: `Trying to access array offset on value of type null` in #1370 diff --git a/database/migrations/create_permission_tables.php.stub b/database/migrations/create_permission_tables.php.stub index 1f8507468..8ad013ba9 100644 --- a/database/migrations/create_permission_tables.php.stub +++ b/database/migrations/create_permission_tables.php.stub @@ -16,6 +16,10 @@ class CreatePermissionTables extends Migration $tableNames = config('permission.table_names'); $columnNames = config('permission.column_names'); + if (empty($tableNames)) { + throw new \Exception('Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding.'); + } + Schema::create($tableNames['permissions'], function (Blueprint $table) { $table->bigIncrements('id'); $table->string('name'); @@ -93,6 +97,10 @@ class CreatePermissionTables extends Migration { $tableNames = config('permission.table_names'); + if (empty($tableNames)) { + throw new \Exception('Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.'); + } + Schema::drop($tableNames['role_has_permissions']); Schema::drop($tableNames['model_has_roles']); Schema::drop($tableNames['model_has_permissions']); diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 7f843af5f..55703fbb4 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -57,7 +57,7 @@ protected function registerModelBindings() { $config = $this->app->config['permission.models']; - if (! $config) { // for odd config merge issue in #1370 + if (! $config) { return; } From 2ed5e9bea3feac256c3f70aef69801466570bb6a Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 3 Mar 2020 16:14:17 -0500 Subject: [PATCH 0262/1013] Add middleware tests for multiple guards --- tests/MiddlewareTest.php | 45 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php index 8b42df044..8d7132cb0 100644 --- a/tests/MiddlewareTest.php +++ b/tests/MiddlewareTest.php @@ -45,6 +45,19 @@ public function a_guest_cannot_access_a_route_protected_by_rolemiddleware() ), 403); } + /** @test */ + public function a_user_cannot_access_a_route_protected_by_role_middleware_of_another_guard() + { + Auth::login($this->testUser); + + $this->testUser->assignRole('testRole'); + + $this->assertEquals( + $this->runMiddleware( + $this->roleMiddleware, 'testAdminRole' + ), 403); + } + /** @test */ public function a_user_can_access_a_route_protected_by_role_middleware_if_have_this_role() { @@ -120,6 +133,38 @@ public function a_guest_cannot_access_a_route_protected_by_the_permission_middle ), 403); } + /** @test */ + public function a_user_cannot_access_a_route_protected_by_the_permission_middleware_of_a_different_guard() + { + Auth::login($this->testAdmin); + + $this->testAdmin->givePermissionTo('admin-permission'); + + $this->assertEquals( + $this->runMiddleware( + $this->permissionMiddleware, 'admin-permission' + ), 200); + + $this->assertEquals( + $this->runMiddleware( + $this->permissionMiddleware, 'edit-articles' + ), 403); + + Auth::login($this->testUser); + + $this->testUser->givePermissionTo('edit-articles'); + + $this->assertEquals( + $this->runMiddleware( + $this->permissionMiddleware, 'edit-articles' + ), 200); + + $this->assertEquals( + $this->runMiddleware( + $this->permissionMiddleware, 'admin-permission' + ), 403); + } + /** @test */ public function a_user_can_access_a_route_protected_by_permission_middleware_if_have_this_permission() { From e325fcc2872a6666aa800ba5f24eaab72b63bb70 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 3 Mar 2020 16:22:21 -0500 Subject: [PATCH 0263/1013] Add middleware tests for multiple guards --- tests/MiddlewareTest.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php index 8d7132cb0..459fca3c7 100644 --- a/tests/MiddlewareTest.php +++ b/tests/MiddlewareTest.php @@ -5,6 +5,7 @@ use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; +use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Middlewares\RoleMiddleware; use Spatie\Permission\Exceptions\UnauthorizedException; use Spatie\Permission\Middlewares\PermissionMiddleware; @@ -136,32 +137,38 @@ public function a_guest_cannot_access_a_route_protected_by_the_permission_middle /** @test */ public function a_user_cannot_access_a_route_protected_by_the_permission_middleware_of_a_different_guard() { + // These permissions are created fresh here in reverse order of guard being applied, so they are not "found first" in the db lookup when matching + app(Permission::class)->create(['name' => 'admin-permission2', 'guard_name' => 'web']); + app(Permission::class)->create(['name' => 'admin-permission2', 'guard_name' => 'admin']); + app(Permission::class)->create(['name' => 'edit-articles2', 'guard_name' => 'admin']); + app(Permission::class)->create(['name' => 'edit-articles2', 'guard_name' => 'web']); + Auth::login($this->testAdmin); - $this->testAdmin->givePermissionTo('admin-permission'); + $this->testAdmin->givePermissionTo('admin-permission2'); $this->assertEquals( $this->runMiddleware( - $this->permissionMiddleware, 'admin-permission' + $this->permissionMiddleware, 'admin-permission2' ), 200); $this->assertEquals( $this->runMiddleware( - $this->permissionMiddleware, 'edit-articles' + $this->permissionMiddleware, 'edit-articles2' ), 403); Auth::login($this->testUser); - $this->testUser->givePermissionTo('edit-articles'); + $this->testUser->givePermissionTo('edit-articles2'); $this->assertEquals( $this->runMiddleware( - $this->permissionMiddleware, 'edit-articles' + $this->permissionMiddleware, 'edit-articles2' ), 200); $this->assertEquals( $this->runMiddleware( - $this->permissionMiddleware, 'admin-permission' + $this->permissionMiddleware, 'admin-permission2' ), 403); } From e90ed6242a8fa29735529160b9c21cb77b233e7f Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 3 Mar 2020 16:31:02 -0500 Subject: [PATCH 0264/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a6988609..35ceefdcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 3.11.0 - 2020-03-03 +- Allow guardName() as a function with priority over $guard_name property #1395 + ## 3.10.1 - 2020-03-03 - Update patch to handle intermittent error in #1370 From 8930750aab368d15adb60dc1616682545b6c4700 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 11 Mar 2020 14:58:20 -0400 Subject: [PATCH 0265/1013] Update new-app.md Fixes #1405 --- docs/basic-usage/new-app.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index 9d2a1b800..ca43c5d9c 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -88,21 +88,18 @@ class PermissionsDemoSeeder extends Seeder $user = Factory(App\User::class)->create([ 'name' => 'Example User', 'email' => 'test@example.com', - // factory default password is 'secret' ]); $user->assignRole($role1); $user = Factory(App\User::class)->create([ 'name' => 'Example Admin User', 'email' => 'admin@example.com', - // factory default password is 'secret' ]); $user->assignRole($role2); $user = Factory(App\User::class)->create([ 'name' => 'Example Super-Admin User', 'email' => 'superadmin@example.com', - // factory default password is 'secret' ]); $user->assignRole($role3); } From 2a9ab6e6aac18fb912f34512dbcb2d1ba897d3ed Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 12 Mar 2020 12:44:51 -0400 Subject: [PATCH 0266/1013] Update new-app.md Adds more clarity for beginners as requested in #1405 --- docs/basic-usage/new-app.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index ca43c5d9c..4dabc3054 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -9,6 +9,8 @@ If you want to just try out the features of this package you can get started wit The examples on this page are primarily added for assistance in creating a quick demo app for troubleshooting purposes, to post the repo on github for convenient sharing to collaborate or get support. +If you're new to Laravel or to any of the concepts mentioned here, you can learn more in the [Laravel documentation](https://laravel.com/docs/) and in the free videos at Laracasts such as this series: https://laracasts.com/series/laravel-6-from-scratch/ + ### Initial setup: ```sh From ae48577cbf0f74c6fedd4b6eb7a55a1e40afe3ae Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 14 Mar 2020 16:47:00 -0400 Subject: [PATCH 0267/1013] Create performance.md --- docs/best-practices/performance.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 docs/best-practices/performance.md diff --git a/docs/best-practices/performance.md b/docs/best-practices/performance.md new file mode 100644 index 000000000..f2d302e68 --- /dev/null +++ b/docs/best-practices/performance.md @@ -0,0 +1,23 @@ +--- +title: Performance Tips +weight: 10 +--- + +Often we think in terms of "roles have permissions" so we lookup a Role, and call `$role->givePermissionTo()` +to indicate what users with that role are allowed to do. This is perfectly fine! + +And yet, in some situations, particularly if your app is deleting and adding new permissions frequently, +you may find that things are more performant if you lookup the permission and assign it to the role, like: +`$permission->assignRole($role)`. +The end result is the same, but sometimes it runs quite a lot faster. + +Also, because of the way this package enforces some protections for you, on large databases you may find +that instead of creating permissions with `Permission::create([attributes])` it might be faster to +`$permission = Permission::make([attributes]); $permission->saveOrFail();` + +On small apps, most of the above will be moot, and unnecessary. + +As always, if you choose to bypass the provided object methods for adding/removing/syncing roles and permissions +by manipulating Role and Permission objects directly in the database, +you will need to manually reset the cache with the PermissionRegistrar's method for that, +as described in the Cache section of the docs. From 27e2a1031cc8015479e5b0fc16ecc23518b0449f Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 28 Mar 2020 20:23:22 -0400 Subject: [PATCH 0268/1013] Update wildcard-permissions.md --- docs/basic-usage/wildcard-permissions.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/basic-usage/wildcard-permissions.md b/docs/basic-usage/wildcard-permissions.md index 03675b122..7db49edfc 100644 --- a/docs/basic-usage/wildcard-permissions.md +++ b/docs/basic-usage/wildcard-permissions.md @@ -25,15 +25,17 @@ The meaning of each part of the string depends on the application layer. > You can use as many parts as you like. So you are not limited to the three-tiered structure, even though this is the common use-case, representing {resource}.{action}.{target}. -> NOTE: You must actually create the permissions before you can assign them. +> NOTE: You must actually create the permissions (eg: `posts.create.1`) before you can assign them, and must also create any wildcard permission patterns (eg: `posts.create.*`) before you can check for them. ### Using Wildcards Each part can also contain wildcards (*). So let's say we assign the following permission to a user: ```php +Permission::create('posts.*'); $user->givePermissionTo('posts.*'); // is the same as +Permission::create('posts'); $user->givePermissionTo('posts'); ``` From 551aa2f950e71505d05445bb63d84dd494d86165 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 29 Mar 2020 00:22:58 -0400 Subject: [PATCH 0269/1013] Update wildcard-permissions.md --- docs/basic-usage/wildcard-permissions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/basic-usage/wildcard-permissions.md b/docs/basic-usage/wildcard-permissions.md index 7db49edfc..6e756e866 100644 --- a/docs/basic-usage/wildcard-permissions.md +++ b/docs/basic-usage/wildcard-permissions.md @@ -32,10 +32,10 @@ this is the common use-case, representing {resource}.{action}.{target}. Each part can also contain wildcards (*). So let's say we assign the following permission to a user: ```php -Permission::create('posts.*'); +Permission::create(['name'=>'posts.*']); $user->givePermissionTo('posts.*'); // is the same as -Permission::create('posts'); +Permission::create(['name'=>'posts']); $user->givePermissionTo('posts'); ``` From 0b916589ed01848165782dab6d37059be6486da3 Mon Sep 17 00:00:00 2001 From: Amanda Wareham Date: Tue, 7 Apr 2020 12:50:56 -0700 Subject: [PATCH 0270/1013] Update PHPstorm blade extending for unlessrole Just adding the `@unlessrole` and `@endunlessrole` --- docs/advanced-usage/phpstorm.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/advanced-usage/phpstorm.md b/docs/advanced-usage/phpstorm.md index b9dcffbe9..e851b0820 100644 --- a/docs/advanced-usage/phpstorm.md +++ b/docs/advanced-usage/phpstorm.md @@ -74,3 +74,19 @@ weight: 7 - Suffix: blank -- + +**unlessrole** + +- has parameter = YES +- Prefix: `check() && !auth()->user()->hasRole(` +- Suffix: `)); ?>` + +-- + +**endunlessrole** + +- has parameter = NO +- Prefix: blank +- Suffix: blank + +-- From 40fc46cbab3c0589a141e36fb58566f02c43531d Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 7 Apr 2020 18:47:02 -0400 Subject: [PATCH 0271/1013] Update using-policies.md --- docs/best-practices/using-policies.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/best-practices/using-policies.md b/docs/best-practices/using-policies.md index be943fed4..4d80c087f 100644 --- a/docs/best-practices/using-policies.md +++ b/docs/best-practices/using-policies.md @@ -7,6 +7,6 @@ The best way to incorporate access control for application features is with [Lar Using Policies allows you to simplify things by abstracting your "control" rules into one place, where your application logic can be combined with your permission rules. -Jeffrey Way explains the concept simply in the [Laravel 6 Authorization Filters](https://laracasts.com/series/laravel-6-from-scratch/episodes/51) video and some related lessons in that chapter. He also mentions how to set up a super-admin, both in a model policy and globally in your application. +Jeffrey Way explains the concept simply in the [Laravel 6 Authorization Filters](https://laracasts.com/series/laravel-6-from-scratch/episodes/51) and [policies](https://laracasts.com/series/laravel-6-from-scratch/episodes/63) videos and in other related lessons in that chapter. He also mentions how to set up a super-admin, both in a model policy and globally in your application. You can find an example of implementing a model policy with this Laravel Permissions package in this demo app: https://github.com/drbyte/spatie-permissions-demo/blob/master/app/Policies/PostPolicy.php From 93621d0e1c1d243b15777dad98d882019b643c46 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 13 Apr 2020 15:18:26 -0400 Subject: [PATCH 0272/1013] Update uuid.md --- docs/advanced-usage/uuid.md | 99 +++++++++++++++++++++++++++++++++---- 1 file changed, 89 insertions(+), 10 deletions(-) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index 6a4436e05..ad080240d 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -3,18 +3,66 @@ title: UUID weight: 6 --- -If you're using UUIDs or GUIDs for your User models there are a few considerations which various users have contributed. As each UUID implementation approach is different, some of these may or may not benefit you. As always, your implementation may vary. +If you're using UUIDs or GUIDs for your User models there are a few considerations to note. + +THIS IS NOT A LESSON ON HOW TO IMPLEMENT UUID IN YOUR APP. + +Since each UUID implementation approach is different, some of these may or may not benefit you. As always, your implementation may vary. + ### Migrations You will probably want to update the `create_permission_tables.php` migration: -- Replace `$table->unsignedBigInteger($columnNames['model_morph_key'])` with `$table->uuid($columnNames['model_morph_key'])`. +- If your User models are using `uuid` instead of `unsignedBigInteger` then you'll need to reflect the change in the migration provided by this package. Something like this would be typical, for both `model_has_permissions` and `model_has_roles`: +```diff +-$table->unsignedBigInteger($columnNames['model_morph_key']) ++$table->uuid($columnNames['model_morph_key']) +``` +- If you also want the roles and permissions to use a UUID for their `id` value, then you'll need to also change the id fields accordingly, and manually set the primary key. LEAVE THE FIELD NAME AS `id` unless you also change it in dozens of other places. + ```diff + Schema::create($tableNames['permissions'], function (Blueprint $table) { + - $table->bigIncrements('id'); + + $table->uuid('id'); + $table->string('name'); + $table->string('guard_name'); + $table->timestamps(); + + + $table->primary('id'); + }); + + Schema::create($tableNames['roles'], function (Blueprint $table) { + - $table->bigIncrements('id'); + + $table->uuid('id'); + $table->string('name'); + $table->string('guard_name'); + $table->timestamps(); + + $table->primary('id'); + }); -### Configuration (morph key) -You will probably also want to update the configuration `column_names.model_morph_key`: + Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) { + - $table->bigIncrements('permission_id'); + + $table->uuid('permission_id'); + ... -- Change to `model_uuid` instead of the default `model_id`. (The default of `model_id` is shown in this snippet below. Change it to match your needs.) + Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames) { + - $table->bigIncrements('role_id'); + + $table->uuid('role_id'); + ... + + Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames) { + - $table->bigIncrements('permission_id'); + - $table->bigIncrements('role_id'); + + $table->uuid('permission_id'); + + $table->uuid('role_id'); + ``` + + +### Configuration (OPTIONAL) +You might want to change the pivot table field name from `model_id` to `model_uuid`, just for semantic purposes. +For this, in the configuration file edit `column_names.model_morph_key`: + +- OPTIONAL: Change to `model_uuid` instead of the default `model_id`. (The default of `model_id` is shown in this snippet below. Change it to match your needs.) 'column_names' => [ /* @@ -26,14 +74,45 @@ You will probably also want to update the configuration `column_names.model_morp */ 'model_morph_key' => 'model_id', ], +- If you extend the models into your app, be sure to list those models in your configuration file. See the Extending section of the documentation and the Models section below. ### Models -You will probably want to Extend the default Role and Permission models into your own namespace, to set some specific properties (see the Extending section of the docs): +If you want all the role/permission objects to have a UUID instead of an integer, you will need to Extend the default Role and Permission models into your own namespace in order to set some specific properties. (See the Extending section of the docs, where it explains requirements of Extending, as well as the configuratoin settings you need to update.) + +- You may want to set `protected $keyType = 'string';` so Laravel handles joins as strings and doesn't cast to integer. +- OPTIONAL: If you changed the field name in your migrations, you must set `protected $primaryKey = 'uuid';` to match. +- Usually for UUID you will also set `public $incrementing = false;`. Remove it if it causes problems for you. + +It is common to use a trait to handle the $keyType and $incrementing settings, as well as add a boot event trigger to ensure new records are assigned a uuid. You would `use` this trait in your User and extended Role/Permission models. An example `UuidTrait` is shown here for inspiration. Adjust to suit your needs. + + ```php + {$model->getKeyName()} = (string) Str::uuid(); + }); + } + } + ``` -- You may want to set `protected $keyType = "string";` so Laravel doesn't cast it to integer. -- You may want to set `protected $primaryKey = 'guid';` (or `uuid`, etc) if you changed the column name in your migrations. -- Optional: Some people have reported value in setting `public $incrementing = false;`, but others have said this caused them problems. Your implementation may vary. ### User Models Troubleshooting tip: In the ***Prerequisites*** section of the docs we remind you that your User model must implement the `Illuminate\Contracts\Auth\Access\Authorizable` contract so that the Gate features are made available to the User object. -In the default User model provided with Laravel, this is done by extending another model (aliased to `Authenticatable`), which extends the base Eloquent model. However, your UUID implementation may need to override that in order to set some of the properties mentioned in the Models section above. If you are running into difficulties, you may want to double-check whether your User model is doing UUIDs consistent with other parts of your app. + +In the default User model provided with Laravel, this is done by extending another model (aliased to `Authenticatable`), which extends the base Eloquent model. + +However, your app's UUID implementation may need to override that in order to set some of the properties mentioned in the Models section above. + +If you are running into difficulties, you may want to double-check whether your User model is doing UUIDs consistent with other parts of your app. From ca1aeabf2395b7f657ab60b3e09225a21bb6a880 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 13 Apr 2020 15:19:03 -0400 Subject: [PATCH 0273/1013] Update uuid.md --- docs/advanced-usage/uuid.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index ad080240d..d3e0b6289 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -85,7 +85,7 @@ If you want all the role/permission objects to have a UUID instead of an integer It is common to use a trait to handle the $keyType and $incrementing settings, as well as add a boot event trigger to ensure new records are assigned a uuid. You would `use` this trait in your User and extended Role/Permission models. An example `UuidTrait` is shown here for inspiration. Adjust to suit your needs. - ```php +```php Date: Mon, 13 Apr 2020 15:19:43 -0400 Subject: [PATCH 0274/1013] Update uuid.md --- docs/advanced-usage/uuid.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index d3e0b6289..4e9d6a370 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -14,10 +14,10 @@ Since each UUID implementation approach is different, some of these may or may n You will probably want to update the `create_permission_tables.php` migration: - If your User models are using `uuid` instead of `unsignedBigInteger` then you'll need to reflect the change in the migration provided by this package. Something like this would be typical, for both `model_has_permissions` and `model_has_roles`: -```diff --$table->unsignedBigInteger($columnNames['model_morph_key']) -+$table->uuid($columnNames['model_morph_key']) -``` + ```diff + - $table->unsignedBigInteger($columnNames['model_morph_key']) + + $table->uuid($columnNames['model_morph_key']) + ``` - If you also want the roles and permissions to use a UUID for their `id` value, then you'll need to also change the id fields accordingly, and manually set the primary key. LEAVE THE FIELD NAME AS `id` unless you also change it in dozens of other places. ```diff Schema::create($tableNames['permissions'], function (Blueprint $table) { From 0d468c2330aedc4dba842b127db962186f35e122 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 13 Apr 2020 15:28:24 -0400 Subject: [PATCH 0275/1013] Update uuid.md --- docs/advanced-usage/uuid.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index 4e9d6a370..64a6f9c33 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -14,6 +14,7 @@ Since each UUID implementation approach is different, some of these may or may n You will probably want to update the `create_permission_tables.php` migration: - If your User models are using `uuid` instead of `unsignedBigInteger` then you'll need to reflect the change in the migration provided by this package. Something like this would be typical, for both `model_has_permissions` and `model_has_roles`: + ```diff - $table->unsignedBigInteger($columnNames['model_morph_key']) + $table->uuid($columnNames['model_morph_key']) From 76c28c369f27c7fb27ce77ab59c33e47ed623a4a Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 13 Apr 2020 15:32:48 -0400 Subject: [PATCH 0276/1013] Update uuid.md --- docs/advanced-usage/uuid.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index 64a6f9c33..97102f51e 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -15,11 +15,13 @@ You will probably want to update the `create_permission_tables.php` migration: - If your User models are using `uuid` instead of `unsignedBigInteger` then you'll need to reflect the change in the migration provided by this package. Something like this would be typical, for both `model_has_permissions` and `model_has_roles`: - ```diff +```diff - $table->unsignedBigInteger($columnNames['model_morph_key']) + $table->uuid($columnNames['model_morph_key']) - ``` +``` + - If you also want the roles and permissions to use a UUID for their `id` value, then you'll need to also change the id fields accordingly, and manually set the primary key. LEAVE THE FIELD NAME AS `id` unless you also change it in dozens of other places. + ```diff Schema::create($tableNames['permissions'], function (Blueprint $table) { - $table->bigIncrements('id'); From 5233f9eeb205dbbbf57cfa692000d4d2d0b63237 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 13 Apr 2020 15:39:39 -0400 Subject: [PATCH 0277/1013] Update uuid.md --- docs/advanced-usage/uuid.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index 97102f51e..bfcbc6845 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -15,10 +15,10 @@ You will probably want to update the `create_permission_tables.php` migration: - If your User models are using `uuid` instead of `unsignedBigInteger` then you'll need to reflect the change in the migration provided by this package. Something like this would be typical, for both `model_has_permissions` and `model_has_roles`: -```diff + ```diff - $table->unsignedBigInteger($columnNames['model_morph_key']) + $table->uuid($columnNames['model_morph_key']) -``` + ``` - If you also want the roles and permissions to use a UUID for their `id` value, then you'll need to also change the id fields accordingly, and manually set the primary key. LEAVE THE FIELD NAME AS `id` unless you also change it in dozens of other places. From fbfe4ef7faad7fe074e58fb1525cc59f913c1636 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 13 Apr 2020 15:42:41 -0400 Subject: [PATCH 0278/1013] Update uuid.md --- docs/advanced-usage/uuid.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index bfcbc6845..ceecfd227 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -13,7 +13,7 @@ Since each UUID implementation approach is different, some of these may or may n ### Migrations You will probably want to update the `create_permission_tables.php` migration: -- If your User models are using `uuid` instead of `unsignedBigInteger` then you'll need to reflect the change in the migration provided by this package. Something like this would be typical, for both `model_has_permissions` and `model_has_roles`: +- If your User models are using `uuid` instead of `unsignedBigInteger` then you'll need to reflect the change in the migration provided by this package. Something like this would be typical, for both `model_has_permissions` and `model_has_roles` tables: ```diff - $table->unsignedBigInteger($columnNames['model_morph_key']) @@ -22,7 +22,7 @@ You will probably want to update the `create_permission_tables.php` migration: - If you also want the roles and permissions to use a UUID for their `id` value, then you'll need to also change the id fields accordingly, and manually set the primary key. LEAVE THE FIELD NAME AS `id` unless you also change it in dozens of other places. - ```diff +```diff Schema::create($tableNames['permissions'], function (Blueprint $table) { - $table->bigIncrements('id'); + $table->uuid('id'); @@ -58,7 +58,7 @@ You will probably want to update the `create_permission_tables.php` migration: - $table->bigIncrements('role_id'); + $table->uuid('permission_id'); + $table->uuid('role_id'); - ``` +``` ### Configuration (OPTIONAL) @@ -112,10 +112,8 @@ It is common to use a trait to handle the $keyType and $incrementing settings, a ### User Models -Troubleshooting tip: In the ***Prerequisites*** section of the docs we remind you that your User model must implement the `Illuminate\Contracts\Auth\Access\Authorizable` contract so that the Gate features are made available to the User object. - +> Troubleshooting tip: In the ***Prerequisites*** section of the docs we remind you that your User model must implement the `Illuminate\Contracts\Auth\Access\Authorizable` contract so that the Gate features are made available to the User object. In the default User model provided with Laravel, this is done by extending another model (aliased to `Authenticatable`), which extends the base Eloquent model. - However, your app's UUID implementation may need to override that in order to set some of the properties mentioned in the Models section above. If you are running into difficulties, you may want to double-check whether your User model is doing UUIDs consistent with other parts of your app. From 36504c0769dd41d0577f2467130f6716df136230 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 13 Apr 2020 15:43:53 -0400 Subject: [PATCH 0279/1013] Update uuid.md --- docs/advanced-usage/uuid.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index ceecfd227..a9ca6b3ba 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -13,14 +13,14 @@ Since each UUID implementation approach is different, some of these may or may n ### Migrations You will probably want to update the `create_permission_tables.php` migration: -- If your User models are using `uuid` instead of `unsignedBigInteger` then you'll need to reflect the change in the migration provided by this package. Something like this would be typical, for both `model_has_permissions` and `model_has_roles` tables: +If your User models are using `uuid` instead of `unsignedBigInteger` then you'll need to reflect the change in the migration provided by this package. Something like this would be typical, for both `model_has_permissions` and `model_has_roles` tables: ```diff - $table->unsignedBigInteger($columnNames['model_morph_key']) + $table->uuid($columnNames['model_morph_key']) ``` -- If you also want the roles and permissions to use a UUID for their `id` value, then you'll need to also change the id fields accordingly, and manually set the primary key. LEAVE THE FIELD NAME AS `id` unless you also change it in dozens of other places. +OPTIONAL: If you also want the roles and permissions to use a UUID for their `id` value, then you'll need to also change the id fields accordingly, and manually set the primary key. LEAVE THE FIELD NAME AS `id` unless you also change it in dozens of other places. ```diff Schema::create($tableNames['permissions'], function (Blueprint $table) { From 0260c710bba9837c320ed889cc248ac2cc039726 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 13 Apr 2020 15:44:59 -0400 Subject: [PATCH 0280/1013] Update uuid.md --- docs/advanced-usage/uuid.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index a9ca6b3ba..d72bf8070 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -15,10 +15,10 @@ You will probably want to update the `create_permission_tables.php` migration: If your User models are using `uuid` instead of `unsignedBigInteger` then you'll need to reflect the change in the migration provided by this package. Something like this would be typical, for both `model_has_permissions` and `model_has_roles` tables: - ```diff +```diff - $table->unsignedBigInteger($columnNames['model_morph_key']) + $table->uuid($columnNames['model_morph_key']) - ``` +``` OPTIONAL: If you also want the roles and permissions to use a UUID for their `id` value, then you'll need to also change the id fields accordingly, and manually set the primary key. LEAVE THE FIELD NAME AS `id` unless you also change it in dozens of other places. From 457f1944095afcc0c87cd7d1618db77ff43b914b Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 13 Apr 2020 15:46:25 -0400 Subject: [PATCH 0281/1013] Update uuid.md --- docs/advanced-usage/uuid.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index d72bf8070..5e9803dfc 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -16,48 +16,48 @@ You will probably want to update the `create_permission_tables.php` migration: If your User models are using `uuid` instead of `unsignedBigInteger` then you'll need to reflect the change in the migration provided by this package. Something like this would be typical, for both `model_has_permissions` and `model_has_roles` tables: ```diff - - $table->unsignedBigInteger($columnNames['model_morph_key']) - + $table->uuid($columnNames['model_morph_key']) +- $table->unsignedBigInteger($columnNames['model_morph_key']) ++ $table->uuid($columnNames['model_morph_key']) ``` OPTIONAL: If you also want the roles and permissions to use a UUID for their `id` value, then you'll need to also change the id fields accordingly, and manually set the primary key. LEAVE THE FIELD NAME AS `id` unless you also change it in dozens of other places. ```diff Schema::create($tableNames['permissions'], function (Blueprint $table) { - - $table->bigIncrements('id'); - + $table->uuid('id'); +- $table->bigIncrements('id'); ++ $table->uuid('id'); $table->string('name'); $table->string('guard_name'); $table->timestamps(); - + $table->primary('id'); ++ $table->primary('id'); }); Schema::create($tableNames['roles'], function (Blueprint $table) { - - $table->bigIncrements('id'); - + $table->uuid('id'); +- $table->bigIncrements('id'); ++ $table->uuid('id'); $table->string('name'); $table->string('guard_name'); $table->timestamps(); - + $table->primary('id'); ++ $table->primary('id'); }); Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) { - - $table->bigIncrements('permission_id'); - + $table->uuid('permission_id'); +- $table->bigIncrements('permission_id'); ++ $table->uuid('permission_id'); ... Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames) { - - $table->bigIncrements('role_id'); - + $table->uuid('role_id'); +- $table->bigIncrements('role_id'); ++ $table->uuid('role_id'); ... Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames) { - - $table->bigIncrements('permission_id'); - - $table->bigIncrements('role_id'); - + $table->uuid('permission_id'); - + $table->uuid('role_id'); +- $table->bigIncrements('permission_id'); +- $table->bigIncrements('role_id'); ++ $table->uuid('permission_id'); ++ $table->uuid('role_id'); ``` From d1dac5f1a05408c640e42910cf4cf7f8aa3f8815 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 13 Apr 2020 15:47:09 -0400 Subject: [PATCH 0282/1013] Update uuid.md --- docs/advanced-usage/uuid.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index 5e9803dfc..630b15216 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -5,7 +5,7 @@ weight: 6 If you're using UUIDs or GUIDs for your User models there are a few considerations to note. -THIS IS NOT A LESSON ON HOW TO IMPLEMENT UUID IN YOUR APP. +> THIS IS NOT A FULL LESSON ON HOW TO IMPLEMENT UUIDs IN YOUR APP. Since each UUID implementation approach is different, some of these may or may not benefit you. As always, your implementation may vary. From 47ec7c4c76e675c1a500e46dfb308011d3f3a192 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 13 Apr 2020 16:17:43 -0400 Subject: [PATCH 0283/1013] Update extending.md --- docs/advanced-usage/extending.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/advanced-usage/extending.md b/docs/advanced-usage/extending.md index 90ab421b5..c7185ab92 100644 --- a/docs/advanced-usage/extending.md +++ b/docs/advanced-usage/extending.md @@ -4,7 +4,9 @@ weight: 4 --- ## Extending User Models -Laravel's authorization features are available in models which implement the `Illuminate\Foundation\Auth\Access\Authorizable` trait. By default Laravel does this in `\App\User` by extending `Illuminate\Foundation\Auth\User`, in which the trait and `Illuminate\Contracts\Auth\Access\Authorizable` contract are declared. +Laravel's authorization features are available in models which implement the `Illuminate\Foundation\Auth\Access\Authorizable` trait. + +By default Laravel does this in `\App\User` by extending `Illuminate\Foundation\Auth\User`, in which the trait and `Illuminate\Contracts\Auth\Access\Authorizable` contract are declared. If you are creating your own User models and wish Authorization features to be available, you need to implement `Illuminate\Contracts\Auth\Access\Authorizable` in one of those ways as well. @@ -16,7 +18,6 @@ First be sure that you've published the configuration file (see the Installation Note the following requirements when extending/replacing the models: - ### Extending If you need to EXTEND the existing `Role` or `Permission` models note that: @@ -31,4 +32,4 @@ If you need to REPLACE the existing `Role` or `Permission` models you need to ke ## Migrations - Adding fields to your models You can add your own migrations to make changes to the role/permission tables, as you would for adding/changing fields in any other tables in your Laravel project. -Following that, you can add any necessary logic for interacting with those fields ... to your custom/extended Models. +Following that, you can add any necessary logic for interacting with those fields into your custom/extended Models. From aaebf40c7616dffeb8cc3993b117ed4ce0088b94 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 16 Apr 2020 22:24:21 -0400 Subject: [PATCH 0284/1013] Update basic-usage.md --- docs/basic-usage/basic-usage.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/basic-usage/basic-usage.md b/docs/basic-usage/basic-usage.md index f191fd68f..5302e1686 100644 --- a/docs/basic-usage/basic-usage.md +++ b/docs/basic-usage/basic-usage.md @@ -92,5 +92,6 @@ Since Role and Permission models are extended from Eloquent models, basic Eloque $all_users_with_all_their_roles = User::with('roles')->get(); $all_users_with_all_direct_permissions = User::with('permissions')->get(); $all_roles_in_database = Role::all()->pluck('name'); +$users_without_any_roles = User::doesntHave('roles')->get(); ``` From f8486664b7856a29b5d1d1578624a621e760e94c Mon Sep 17 00:00:00 2001 From: Pierre Grimaud Date: Sat, 18 Apr 2020 22:05:33 +0200 Subject: [PATCH 0285/1013] Fix typos --- docs/advanced-usage/uuid.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index 630b15216..ce55666cc 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -80,7 +80,7 @@ For this, in the configuration file edit `column_names.model_morph_key`: - If you extend the models into your app, be sure to list those models in your configuration file. See the Extending section of the documentation and the Models section below. ### Models -If you want all the role/permission objects to have a UUID instead of an integer, you will need to Extend the default Role and Permission models into your own namespace in order to set some specific properties. (See the Extending section of the docs, where it explains requirements of Extending, as well as the configuratoin settings you need to update.) +If you want all the role/permission objects to have a UUID instead of an integer, you will need to Extend the default Role and Permission models into your own namespace in order to set some specific properties. (See the Extending section of the docs, where it explains requirements of Extending, as well as the configuration settings you need to update.) - You may want to set `protected $keyType = 'string';` so Laravel handles joins as strings and doesn't cast to integer. - OPTIONAL: If you changed the field name in your migrations, you must set `protected $primaryKey = 'uuid';` to match. From b7bac19f70cb3633565c54b91436e02d1c0dc690 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 19 Apr 2020 17:25:37 -0400 Subject: [PATCH 0286/1013] Create timestamps.md Closes #1240 Closes #1396 Closes #302 --- docs/advanced-usage/timestamps.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 docs/advanced-usage/timestamps.md diff --git a/docs/advanced-usage/timestamps.md b/docs/advanced-usage/timestamps.md new file mode 100644 index 000000000..a19247658 --- /dev/null +++ b/docs/advanced-usage/timestamps.md @@ -0,0 +1,20 @@ +--- +title: Timestamps +weight: 8 +--- + +### Excluding Timestamps from JSON + +If you want to exclude timestamps from JSON output of role/permission pivots, you can extend the Role and Permission models into your own App namespace and mark the pivot as hidden: + +```php + protected $hidden = ['pivot']; + ``` + +### Adding Timestamps to Pivots + +If you want to add timestamps to your pivot tables, you can do it with a few steps: + - update the tables by calling `$table->timestamps();` in a migration + - extend the Permission and Role models and add `->withTimestamps();` to the BelongsToMany relationshps for `roles()` and `permissions()` + - update your User models (wherever you use the HasRoles or HasPermissions traits) by adding `->withTimestamps();` to the BelongsToMany relationshps for `roles()` and `permissions()` + From 23a91c69298505c85cb7894486640b8ae9a26876 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 5 May 2020 14:46:45 -0400 Subject: [PATCH 0287/1013] Update uuid.md --- docs/advanced-usage/uuid.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index ce55666cc..fd3362e79 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -104,7 +104,7 @@ It is common to use a trait to handle the $keyType and $incrementing settings, a parent::boot(); static::creating(function ($model) { - $model->{$model->getKeyName()} = (string) Str::uuid(); + $model->{$model->getKeyName()} = $model->{$model->getKeyName()} ?: (string) Str::orderedUuid(); }); } } From 7a0680d0c5f8f401342f059d07ea233fbd50a43d Mon Sep 17 00:00:00 2001 From: yoeriwalstra Date: Wed, 6 May 2020 11:46:42 +0200 Subject: [PATCH 0288/1013] add display_role_in_exception key with default value false to permission config --- config/permission.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/config/permission.php b/config/permission.php index 940fcfc8d..1a4207e6c 100644 --- a/config/permission.php +++ b/config/permission.php @@ -85,13 +85,21 @@ ], /* - * When set to true, the required permission/role names are added to the exception + * When set to true, the required permission names are added to the exception * message. This could be considered an information leak in some contexts, so * the default setting is false here for optimum safety. */ 'display_permission_in_exception' => false, + /* + * When set to true, the required role names are added to the exception + * message. This could be considered an information leak in some contexts, so + * the default setting is false here for optimum safety. + */ + + 'display_role_in_exception' => false, + /* * By default wildcard permission lookups are disabled. */ From 6e9803b0c75867ec64084942ccaceecaef37b2c0 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 12 May 2020 16:38:24 -0400 Subject: [PATCH 0289/1013] Update other.md --- docs/advanced-usage/other.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/advanced-usage/other.md b/docs/advanced-usage/other.md index a6bc996d1..ebcf5627b 100644 --- a/docs/advanced-usage/other.md +++ b/docs/advanced-usage/other.md @@ -2,3 +2,7 @@ title: Other weight: 8 --- + +Schema Diagram: + +You can find a schema diagram at https://drawsql.app/templates/laravel-permission From b9f81fe91c768b9f458a38b09a74bc739ead495b Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 14 May 2020 12:39:33 -0400 Subject: [PATCH 0290/1013] Ensure artisan Show command uses configured models Fixes issue reported by @vimiso in #374 --- src/Commands/Show.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Commands/Show.php b/src/Commands/Show.php index ad5023d08..e8a53946b 100644 --- a/src/Commands/Show.php +++ b/src/Commands/Show.php @@ -4,8 +4,8 @@ use Illuminate\Console\Command; use Illuminate\Support\Collection; -use Spatie\Permission\Models\Role; -use Spatie\Permission\Models\Permission; +use Spatie\Permission\Contracts\Role as RoleContract; +use Spatie\Permission\Contracts\Permission as PermissionContract; class Show extends Command { @@ -17,23 +17,26 @@ class Show extends Command public function handle() { + $permissionClass = app(PermissionContract::class); + $roleClass = app(RoleContract::class); + $style = $this->argument('style') ?? 'default'; $guard = $this->argument('guard'); if ($guard) { $guards = Collection::make([$guard]); } else { - $guards = Permission::pluck('guard_name')->merge(Role::pluck('guard_name'))->unique(); + $guards = $permissionClass::pluck('guard_name')->merge($roleClass::pluck('guard_name'))->unique(); } foreach ($guards as $guard) { $this->info("Guard: $guard"); - $roles = Role::whereGuardName($guard)->orderBy('name')->get()->mapWithKeys(function (Role $role) { + $roles = $roleClass::whereGuardName($guard)->orderBy('name')->get()->mapWithKeys(function ($role) { return [$role->name => $role->permissions->pluck('name')]; }); - $permissions = Permission::whereGuardName($guard)->orderBy('name')->pluck('name'); + $permissions = $permissionClass::whereGuardName($guard)->orderBy('name')->pluck('name'); $body = $permissions->map(function ($permission) use ($roles) { return $roles->map(function (Collection $role_permissions) use ($permission) { From ee3d83e6c85f480e595ae4d96aab656063efe85a Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 14 May 2020 12:42:35 -0400 Subject: [PATCH 0291/1013] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35ceefdcb..d90e03f4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to `laravel-permission` will be documented in this file +## 3.12.0 - 2020-05-14 +- Add missing config setting for `display_role_in_exception` +- Ensure artisan `permission:show` command uses configured models + ## 3.11.0 - 2020-03-03 - Allow guardName() as a function with priority over $guard_name property #1395 From 81d5d778293d34f778911830ca236261efc6057a Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 14 May 2020 12:59:57 -0400 Subject: [PATCH 0292/1013] Update cache.md --- docs/advanced-usage/cache.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/advanced-usage/cache.md b/docs/advanced-usage/cache.md index 0ce9b4213..f4eb375d0 100644 --- a/docs/advanced-usage/cache.md +++ b/docs/advanced-usage/cache.md @@ -23,6 +23,9 @@ $permission->syncRoles(params); HOWEVER, if you manipulate permission/role data directly in the database instead of calling the supplied methods, then you will not see the changes reflected in the application unless you manually reset the cache. +Additionally, because the Role and Permission models are Eloquent models which implement the `RefreshesPermissionCache` trait, creating and deleting Roles and Permissions will automatically clear the cache. If you have created your own models which do not extend the default models then you will need to implement the trait yourself. + + ### Manual cache reset To manually reset the cache for this package, you can run the following in your app code: ```php From 2d9f43b030084ee9d69753e5f9c4e55a8c779041 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 19 May 2020 20:27:53 -0400 Subject: [PATCH 0293/1013] Remind devs not to cache config in development Published configs can't be accessed if config is cached. Don't cache in development environment! Fixes #1464 Ref #1411 Ref #1370 --- database/migrations/create_permission_tables.php.stub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/create_permission_tables.php.stub b/database/migrations/create_permission_tables.php.stub index 8ad013ba9..3f9448d0b 100644 --- a/database/migrations/create_permission_tables.php.stub +++ b/database/migrations/create_permission_tables.php.stub @@ -17,7 +17,7 @@ class CreatePermissionTables extends Migration $columnNames = config('permission.column_names'); if (empty($tableNames)) { - throw new \Exception('Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding.'); + throw new \Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.'); } Schema::create($tableNames['permissions'], function (Blueprint $table) { From 49b8063fbb9ec52ebef98cc6ec527a80d8853141 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 19 May 2020 20:31:29 -0400 Subject: [PATCH 0294/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d90e03f4c..8aa18f8fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 3.13.0 - 2020-05-19 +- Provide migration error text to stop caching local config when installing packages. + ## 3.12.0 - 2020-05-14 - Add missing config setting for `display_role_in_exception` - Ensure artisan `permission:show` command uses configured models From 2732babfdd00e272f61e3d43a7739b1a884404ea Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 19 May 2020 20:36:30 -0400 Subject: [PATCH 0295/1013] Update installation-laravel.md --- docs/installation-laravel.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index b24a15782..9cddc73fc 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -30,11 +30,17 @@ This package can be used with Laravel 5.8 or higher. 6. NOTE: If you are using UUIDs, see the Advanced section of the docs on UUID steps, before you continue. It explains some changes you may want to make to the migrations and config file before continuing. It also mentions important considerations after extending this package's models for UUID capability. -7. Run the migrations: After the config and migration have been published and configured, you can create the tables for this package by running: +7. Clear your config cache. This package requires access to the `permission` config. Generally it's bad practice to do config-caching in a development environment. If you've been caching configurations locally, clear your config cache with either of these commands: + + php optimize:clear + # or + php config:clear + +8. Run the migrations: After the config and migration have been published and configured, you can create the tables for this package by running: php artisan migrate -8. Add the necessary trait to your User model: Consult the Basic Usage section of the docs for how to get started using the features of this package. +9. Add the necessary trait to your User model: Consult the Basic Usage section of the docs for how to get started using the features of this package. ### Default config file contents From be87e2918a9fad30e5257bc336c89225c3e6eb5a Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 19 May 2020 20:37:00 -0400 Subject: [PATCH 0296/1013] Update installation-laravel.md --- docs/installation-laravel.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 9cddc73fc..616438c5f 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -32,9 +32,9 @@ This package can be used with Laravel 5.8 or higher. 7. Clear your config cache. This package requires access to the `permission` config. Generally it's bad practice to do config-caching in a development environment. If you've been caching configurations locally, clear your config cache with either of these commands: - php optimize:clear + php artisan optimize:clear # or - php config:clear + php artisan config:clear 8. Run the migrations: After the config and migration have been published and configured, you can create the tables for this package by running: From a9e6f9047606b55ac9bc0bd9d2761d847caddf87 Mon Sep 17 00:00:00 2001 From: Adriaan Marain Date: Wed, 27 May 2020 15:12:47 +0200 Subject: [PATCH 0297/1013] Update README with new "Support us" section --- README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ca3c14b5a..5f1c1ee59 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,14 @@ # Associate users with permissions and roles +## Support us + +Learn how to create a package like this one, by watching our premium video course: + +[![Laravel Package training](https://spatie.be/github/package-training.jpg)](https://laravelpackage.training) + +We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). + +We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). ### Sponsor @@ -36,17 +45,10 @@ Because all permissions will be registered on [Laravel's gate](https://laravel.c $user->can('edit articles'); ``` -## Support us - -We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). - -We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). - ## Documentation, Installation, and Usage Instructions See the [DOCUMENTATION](https://docs.spatie.be/laravel-permission/v3/introduction/) for detailed installation and usage instructions. - ### Testing ``` bash From caf3abd7258fe5c150cba9ee8efdcb2a8248b9ac Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 27 May 2020 10:37:36 -0400 Subject: [PATCH 0298/1013] Update seeding.md --- docs/advanced-usage/seeding.md | 57 +++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/docs/advanced-usage/seeding.md b/docs/advanced-usage/seeding.md index 2b3b548c7..0e04e40f1 100644 --- a/docs/advanced-usage/seeding.md +++ b/docs/advanced-usage/seeding.md @@ -3,7 +3,17 @@ title: Database Seeding weight: 2 --- -You may discover that it is best to flush this package's cache before seeding, to avoid cache conflict errors. This can be done directly in a seeder class. Here is a sample seeder, which first clears the cache, creates permissions and then assigns permissions to roles (the order of these steps is intentional): +## Flush cache before seeding + +You may discover that it is best to flush this package's cache before seeding, to avoid cache conflict errors. + +```php +// reset cached roles and permissions +app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions(); +``` + +This can be done directly in a seeder class. +Here is a sample seeder, which first clears the cache, creates permissions and then assigns permissions to roles (the order of these steps is intentional): ```php use Illuminate\Database\Seeder; @@ -38,3 +48,48 @@ class RolesAndPermissionsSeeder extends Seeder } } ``` + +## Speeding up seeding for large data sets + +When seeding large quantities of roles or permissions you may consider using Eloquent's `insert` command instead of `create`, as this bypasses all the internal checks that this package does when calling `create` (including extra queries to verify existence, test guards, etc). + +```php + $arrayOfPermissionNames = ['writer', 'editor']; + $permissions = collect($arrayOfPermissionNames)->map(function ($permission) { + return ['name' => $permission, 'guard_name' => 'web']; + }); + + Permission::insert($permissions->toArray()); +``` + +Alternatively you could use `DB::insert`, as long as you also provide all the required data fields. One example of this is shown below ... but note that this example hard-codes the table names and field names, thus does not respect any customizations you may have in your permissions config file. + +```php +$permissionsByRole = [ + 'admin' => ['restore posts', 'force delete posts'], + 'editor' => ['create a post', 'update a post', 'delete a post'], + 'viewer' => ['view all posts', 'view a post'] +]; + +$insertPermissions = fn ($role) => collect($permissionsByRole[$role]) + ->map(fn ($name) => DB::table()->insertGetId(['name' => $name])) + ->toArray(); + +$permissionIdsByRole = [ + 'admin' => $insertPermissions('admin'), + 'editor' => $insertPermissions('editor'), + 'viewer' => $insertPermissions('viewer') +]; + +foreach ($permissionIdsByRole as $role => $permissionIds) { + $role = Role::whereName($role)->first(); + + DB::table('role_has_permissions') + ->insert( + collect($permissionIds)->map(fn ($id) => [ + 'role_id' => $role->id, + 'permission_id' => $id + ])->toArray() + ); +} +``` From e25986c298b921e884a0a93a6f09f81c0c9cdb5a Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Sun, 31 May 2020 01:13:24 +0200 Subject: [PATCH 0299/1013] Update README.md --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 5f1c1ee59..4a24bea95 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,5 @@ # Associate users with permissions and roles -## Support us - -Learn how to create a package like this one, by watching our premium video course: - -[![Laravel Package training](https://spatie.be/github/package-training.jpg)](https://laravelpackage.training) - -We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). - -We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). - ### Sponsor @@ -45,6 +35,16 @@ Because all permissions will be registered on [Laravel's gate](https://laravel.c $user->can('edit articles'); ``` +## Support us + +Learn how to create a package like this one, by watching our premium video course: + +[![Laravel Package training](https://spatie.be/github/package-training.jpg)](https://laravelpackage.training) + +We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). + +We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). + ## Documentation, Installation, and Usage Instructions See the [DOCUMENTATION](https://docs.spatie.be/laravel-permission/v3/introduction/) for detailed installation and usage instructions. From 51c2afca0cb92bb5153d51acc6e12a8a5f439b7a Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 6 Jun 2020 17:12:47 -0400 Subject: [PATCH 0300/1013] Added note to clarify that wildcard * means "ALL", not "ANY" Fixes #1408 --- docs/basic-usage/wildcard-permissions.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/basic-usage/wildcard-permissions.md b/docs/basic-usage/wildcard-permissions.md index 6e756e866..93ae097e4 100644 --- a/docs/basic-usage/wildcard-permissions.md +++ b/docs/basic-usage/wildcard-permissions.md @@ -25,11 +25,15 @@ The meaning of each part of the string depends on the application layer. > You can use as many parts as you like. So you are not limited to the three-tiered structure, even though this is the common use-case, representing {resource}.{action}.{target}. -> NOTE: You must actually create the permissions (eg: `posts.create.1`) before you can assign them, and must also create any wildcard permission patterns (eg: `posts.create.*`) before you can check for them. +> NOTE: You must actually create the permissions (eg: `posts.create.1`) before you can assign them + +> NOTE: You must create any wildcard permission patterns (eg: `posts.create.*`) before you can check for them. ### Using Wildcards -Each part can also contain wildcards (*). So let's say we assign the following permission to a user: +> ALERT: The `*` means "ALL". It does **not** mean "ANY". + +Each part can also contain wildcards (`*`). So let's say we assign the following permission to a user: ```php Permission::create(['name'=>'posts.*']); From 782af0045cf95ae9eaf8d96b338349e1572a4cd2 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 6 Jun 2020 17:17:16 -0400 Subject: [PATCH 0301/1013] Additional clarity that the `*` wildcard means ALL, not ANY --- docs/basic-usage/wildcard-permissions.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/basic-usage/wildcard-permissions.md b/docs/basic-usage/wildcard-permissions.md index 93ae097e4..31662e844 100644 --- a/docs/basic-usage/wildcard-permissions.md +++ b/docs/basic-usage/wildcard-permissions.md @@ -25,9 +25,9 @@ The meaning of each part of the string depends on the application layer. > You can use as many parts as you like. So you are not limited to the three-tiered structure, even though this is the common use-case, representing {resource}.{action}.{target}. -> NOTE: You must actually create the permissions (eg: `posts.create.1`) before you can assign them +> NOTE: You must actually create the permissions (eg: `posts.create.1`) before you can assign them or check for them. -> NOTE: You must create any wildcard permission patterns (eg: `posts.create.*`) before you can check for them. +> NOTE: You must create any wildcard permission patterns (eg: `posts.create.*`) before you can assign them or check for them. ### Using Wildcards @@ -53,6 +53,13 @@ $user->can('posts.edit'); $user->can('posts.delete'); ``` +### Meaning of the `*` Asterisk + +The `*` means "ALL". It does **not** mean "ANY". + +Thus `can('post.*')` will only pass if the user has been assigned `post.*` explicitly. + + ### Subparts Besides the use of parts and wildcards, subparts can also be used. Subparts are divided with commas (,). This is a From 68a10bed1f44c3c900add1053ec5d01a1ca9bcc5 Mon Sep 17 00:00:00 2001 From: Adriaan Marain Date: Fri, 26 Jun 2020 10:00:13 +0200 Subject: [PATCH 0302/1013] Update address --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a24bea95..20da102db 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ If you discover any security-related issues, please email [freek@spatie.be](mail You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. -Our address is: Spatie, Samberstraat 69D, 2060 Antwerp, Belgium. +Our address is: Spatie, Kruikstraat 22, 2018 Antwerp, Belgium. We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards). From 3ca266565bd74bad19b9f116cc1417a5acba66f4 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 3 Jul 2020 09:55:01 -0400 Subject: [PATCH 0303/1013] mention InfyOm generator Replaces and closes #1519 --- docs/advanced-usage/ui-options.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/advanced-usage/ui-options.md b/docs/advanced-usage/ui-options.md index 785ec10b2..bc5bb5244 100644 --- a/docs/advanced-usage/ui-options.md +++ b/docs/advanced-usage/ui-options.md @@ -16,3 +16,5 @@ The package doesn't come with any screens out of the box, you should build that - [How to create a UI for managing the permissions and roles](http://www.qcode.in/easy-roles-and-permissions-in-laravel-5-4/) - [Laravel User Management for managing users, roles, permissions, departments and authorization](https://github.com/Mekaeil/LaravelUserManagement) by [Mekaeil](https://github.com/Mekaeil) + +- [Generating UI boilerplate using InfyOm](https://youtu.be/hlGu2pa1bdU) by [Shailesh](https://github.com/shailesh-ladumor) From b005ab34b0d4c566cfd868781a81bef19b848524 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 3 Jul 2020 09:57:47 -0400 Subject: [PATCH 0304/1013] Update ui-options.md --- docs/advanced-usage/ui-options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-usage/ui-options.md b/docs/advanced-usage/ui-options.md index bc5bb5244..347142464 100644 --- a/docs/advanced-usage/ui-options.md +++ b/docs/advanced-usage/ui-options.md @@ -17,4 +17,4 @@ The package doesn't come with any screens out of the box, you should build that - [Laravel User Management for managing users, roles, permissions, departments and authorization](https://github.com/Mekaeil/LaravelUserManagement) by [Mekaeil](https://github.com/Mekaeil) -- [Generating UI boilerplate using InfyOm](https://youtu.be/hlGu2pa1bdU) by [Shailesh](https://github.com/shailesh-ladumor) +- [Generating UI boilerplate using InfyOm](https://youtu.be/hlGu2pa1bdU) video tutorial by [Shailesh](https://github.com/shailesh-ladumor) From bf539b1f290b097061f564f683411de6e490d9ad Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 23 Jul 2020 10:44:59 -0400 Subject: [PATCH 0305/1013] Update cache.md Ref #1535 --- docs/advanced-usage/cache.md | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/docs/advanced-usage/cache.md b/docs/advanced-usage/cache.md index f4eb375d0..874ea594a 100644 --- a/docs/advanced-usage/cache.md +++ b/docs/advanced-usage/cache.md @@ -5,9 +5,9 @@ weight: 5 Role and Permission data are cached to speed up performance. -While we recommend not changing the cache "key" name, if you wish to alter the expiration time you may do so in the `config/permission.php` file, in the `cache` array. +### Automatic Cache Refresh Using Built-In Functions -When you use the built-in functions for manipulating roles and permissions, the cache is automatically reset for you, and relations are automatically reloaded for the current model record: +When you **use the built-in functions** for manipulating roles and permissions, the cache is automatically reset for you, and relations are automatically reloaded for the current model record: ```php $user->assignRole('writer'); @@ -36,12 +36,27 @@ Or you can use an Artisan command: ```bash php artisan permission:cache-reset ``` +(This command is effectively an alias for `artisan cache:forget spatie.permission.cache` but respects the package config as well.) -### Cache Identifier +### Cache Expiration Time + +The default cache `expiration_time` is `24 hours`. +If you wish to alter the expiration time you may do so in the `config/permission.php` file, in the `cache` array. + + +### Cache Key + +The default cache key is `spatie.permission.cache`. +We recommend not changing the cache "key" name. Usually changing it is a bad idea. More likely setting the cache `prefix` is better, as mentioned above. + + +### Cache Identifier / Prefix + +Laravel Tip: If you are leveraging a caching service such as `redis` or `memcached` and there are other sites running on your server, you could run into cache clashes between apps. + +To prevent other applications from accidentally using/changing your cached data, it is prudent to set your own cache `prefix` in Laravel's `/config/cache.php` to something unique for each application which shares the same caching service. + +Most multi-tenant "packages" take care of this for you when switching tenants. -TIP: If you are leveraging a caching service such as `redis` or `memcached` and there are other sites -running on your server, you could run into cache clashes between apps. It is prudent to set your own -cache `prefix` in Laravel's `/config/cache.php` to something unique for each application. -This will prevent other applications from accidentally using/changing your cached data. From 95fb0a69b862e812f7cc1ceafc2d6a78390c72d8 Mon Sep 17 00:00:00 2001 From: WimWidgets Date: Tue, 11 Aug 2020 17:15:56 +0200 Subject: [PATCH 0306/1013] Change relationship type for users --- src/Models/Permission.php | 3 +-- src/Models/Role.php | 3 +-- src/Traits/HasPermissions.php | 4 ++-- src/Traits/HasRoles.php | 4 ++-- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 825924685..1d08e9d0b 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -8,7 +8,6 @@ use Illuminate\Database\Eloquent\Model; use Spatie\Permission\PermissionRegistrar; use Spatie\Permission\Traits\RefreshesPermissionCache; -use Illuminate\Database\Eloquent\Relations\MorphToMany; use Spatie\Permission\Exceptions\PermissionDoesNotExist; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Spatie\Permission\Exceptions\PermissionAlreadyExists; @@ -59,7 +58,7 @@ public function roles(): BelongsToMany /** * A permission belongs to some users of the model associated with its guard. */ - public function users(): MorphToMany + public function users(): BelongsToMany { return $this->morphedByMany( getModelForGuard($this->attributes['guard_name']), diff --git a/src/Models/Role.php b/src/Models/Role.php index 8f30606cb..cb6d1f002 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -10,7 +10,6 @@ use Spatie\Permission\Exceptions\RoleAlreadyExists; use Spatie\Permission\Contracts\Role as RoleContract; use Spatie\Permission\Traits\RefreshesPermissionCache; -use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Role extends Model implements RoleContract @@ -56,7 +55,7 @@ public function permissions(): BelongsToMany /** * A role belongs to some users of the model associated with its guard. */ - public function users(): MorphToMany + public function users(): BelongsToMany { return $this->morphedByMany( getModelForGuard($this->attributes['guard_name']), diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index c66cfe752..400c85bf8 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -2,6 +2,7 @@ namespace Spatie\Permission\Traits; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Spatie\Permission\Guard; use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Builder; @@ -9,7 +10,6 @@ use Spatie\Permission\PermissionRegistrar; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Exceptions\GuardDoesNotMatch; -use Illuminate\Database\Eloquent\Relations\MorphToMany; use Spatie\Permission\Exceptions\PermissionDoesNotExist; use Spatie\Permission\Exceptions\WildcardPermissionInvalidArgument; @@ -40,7 +40,7 @@ public function getPermissionClass() /** * A model may have multiple direct permissions. */ - public function permissions(): MorphToMany + public function permissions(): BelongsToMany { return $this->morphToMany( config('permission.models.permission'), diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index cced46cf1..ce325541c 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -2,11 +2,11 @@ namespace Spatie\Permission\Traits; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Support\Collection; use Spatie\Permission\Contracts\Role; use Illuminate\Database\Eloquent\Builder; use Spatie\Permission\PermissionRegistrar; -use Illuminate\Database\Eloquent\Relations\MorphToMany; trait HasRoles { @@ -37,7 +37,7 @@ public function getRoleClass() /** * A model may have multiple roles. */ - public function roles(): MorphToMany + public function roles(): BelongsToMany { return $this->morphToMany( config('permission.models.role'), From fbc3386aa4676ab791b9881343afe2447fe97c34 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 11 Aug 2020 11:44:09 -0400 Subject: [PATCH 0307/1013] Update .styleci.yml --- .styleci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.styleci.yml b/.styleci.yml index 5e3aeba5a..e5db67cae 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -5,7 +5,6 @@ enabled: - long_list_syntax disabled: -- self_accessor - short_list_syntax - alpha_ordered_imports - single_class_element_per_statement From 57cf539b79095a563c782e11f2510f908be796dc Mon Sep 17 00:00:00 2001 From: Igor Valkenburg Date: Sat, 4 Jul 2020 20:50:08 +0200 Subject: [PATCH 0308/1013] Changed PermissionRegistrar::initializeCache() public to easily reinitialize cache --- src/PermissionRegistrar.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index a0ee15054..4efab6f30 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -49,7 +49,7 @@ public function __construct(CacheManager $cacheManager) $this->initializeCache(); } - protected function initializeCache() + public function initializeCache() { self::$cacheExpirationTime = config('permission.cache.expiration_time', config('permission.cache_expiration_time')); From bce7d9b5e8316560c71fad9eed7b7e15e464c96f Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 15 Aug 2020 15:19:18 -0400 Subject: [PATCH 0309/1013] Declare table relations earlier to improve guarded/fillable detection accuracy Relates to requirements from Laravel's security patches in August 2020, in Laravel 6.18.35, 7.24.0 https://blog.laravel.com/security-release-laravel-61835-7240 Co-authored-by: ReeceM <2767904+ReeceM@users.noreply.github.com> --- src/Models/Permission.php | 5 ++++- src/Models/Role.php | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 825924685..a0f762582 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -26,8 +26,11 @@ public function __construct(array $attributes = []) $attributes['guard_name'] = $attributes['guard_name'] ?? config('auth.defaults.guard'); parent::__construct($attributes); + } - $this->setTable(config('permission.table_names.permissions')); + public function getTable() + { + return config('permission.table_names.permissions', parent::getTable()); } public static function create(array $attributes = []) diff --git a/src/Models/Role.php b/src/Models/Role.php index 8f30606cb..05ab72b48 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -25,8 +25,11 @@ public function __construct(array $attributes = []) $attributes['guard_name'] = $attributes['guard_name'] ?? config('auth.defaults.guard'); parent::__construct($attributes); + } - $this->setTable(config('permission.table_names.roles')); + public function getTable() + { + return config('permission.table_names.roles', parent::getTable()); } public static function create(array $attributes = []) From 20b97813e20e9fac5b235f98c7dc782b96d9df73 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 15 Aug 2020 16:28:07 -0400 Subject: [PATCH 0310/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8aa18f8fe..84ea56af2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 3.14.0 - 2020-08-15 +- Declare table relations earlier to improve guarded/fillable detection accuracy (relates to Aug 2020 Laravel security patch) + ## 3.13.0 - 2020-05-19 - Provide migration error text to stop caching local config when installing packages. From 72f9d2dc202323f2a535ca326b817ae55534ac2f Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 15 Aug 2020 16:32:30 -0400 Subject: [PATCH 0311/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84ea56af2..2a9d49aea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 3.15.0 - 2020-08-15 +- Change `users` relationship type to BelongsToMany + ## 3.14.0 - 2020-08-15 - Declare table relations earlier to improve guarded/fillable detection accuracy (relates to Aug 2020 Laravel security patch) From 53fe7986d560adb2efda46be7ce76709d9db780f Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Sat, 15 Aug 2020 20:32:37 +0000 Subject: [PATCH 0312/1013] Apply fixes from StyleCI --- src/Traits/HasPermissions.php | 2 +- src/Traits/HasRoles.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 400c85bf8..804685795 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -2,7 +2,6 @@ namespace Spatie\Permission\Traits; -use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Spatie\Permission\Guard; use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Builder; @@ -11,6 +10,7 @@ use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Exceptions\GuardDoesNotMatch; use Spatie\Permission\Exceptions\PermissionDoesNotExist; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Spatie\Permission\Exceptions\WildcardPermissionInvalidArgument; trait HasPermissions diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index ce325541c..4bc56e991 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -2,11 +2,11 @@ namespace Spatie\Permission\Traits; -use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Support\Collection; use Spatie\Permission\Contracts\Role; use Illuminate\Database\Eloquent\Builder; use Spatie\Permission\PermissionRegistrar; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; trait HasRoles { From 250018247e004909426cf57e65f75b76404406e6 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 18 Aug 2020 12:46:02 -0400 Subject: [PATCH 0313/1013] Add PHP 8 to the test suite --- .github/workflows/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 8b2e37fb1..fa5969204 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: true matrix: - php: [7.4, 7.3, 7.2] + php: [8.0, 7.4, 7.3, 7.2] laravel: [7.*, 6.*, 5.8.*] dependency-version: [prefer-lowest, prefer-stable] include: From 6a320cba6a900e2a9bbd3c5dd3c47d43ba309cbb Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 18 Aug 2020 12:48:29 -0400 Subject: [PATCH 0314/1013] Add PHP 8 to the test suite --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 7066150f7..7d70cf968 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ } ], "require": { - "php" : "^7.2.5", + "php" : "^7.2.5|^8.0", "illuminate/auth": "^5.8|^6.0|^7.0", "illuminate/container": "^5.8|^6.0|^7.0", "illuminate/contracts": "^5.8|^6.0|^7.0", From 849c38d56619fa289f474af9cc664d7d03696a1f Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 18 Aug 2020 12:52:21 -0400 Subject: [PATCH 0315/1013] Add PHP 8 to the test suite --- .github/workflows/run-tests-php8.yml | 44 ++++++++++++++++++++++++++++ .github/workflows/run-tests.yml | 2 +- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/run-tests-php8.yml diff --git a/.github/workflows/run-tests-php8.yml b/.github/workflows/run-tests-php8.yml new file mode 100644 index 000000000..299f140b3 --- /dev/null +++ b/.github/workflows/run-tests-php8.yml @@ -0,0 +1,44 @@ +name: "Run Tests" + +on: [push, pull_request] + +jobs: + test: + + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + php: [8.0] + laravel: [7.*] + dependency-version: [prefer-lowest, prefer-stable] + include: + - laravel: 7.* + testbench: 5.* + + name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} + + steps: + - name: Checkout code + uses: actions/checkout@v1 + + - name: Cache dependencies + uses: actions/cache@v1 + with: + path: ~/.composer/cache/files + key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv + coverage: none + + - name: Install dependencies + run: | + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:>=4.3.4" --no-interaction --no-update + composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest + + - name: Execute tests + run: vendor/bin/phpunit diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index fa5969204..8b2e37fb1 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: true matrix: - php: [8.0, 7.4, 7.3, 7.2] + php: [7.4, 7.3, 7.2] laravel: [7.*, 6.*, 5.8.*] dependency-version: [prefer-lowest, prefer-stable] include: From ed0b3858973c08da904a11eb455cc5f187997a92 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 18 Aug 2020 12:54:36 -0400 Subject: [PATCH 0316/1013] Revert PHP8 test suite --- .github/workflows/run-tests-php8.yml | 44 ---------------------------- 1 file changed, 44 deletions(-) delete mode 100644 .github/workflows/run-tests-php8.yml diff --git a/.github/workflows/run-tests-php8.yml b/.github/workflows/run-tests-php8.yml deleted file mode 100644 index 299f140b3..000000000 --- a/.github/workflows/run-tests-php8.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: "Run Tests" - -on: [push, pull_request] - -jobs: - test: - - runs-on: ubuntu-latest - strategy: - fail-fast: true - matrix: - php: [8.0] - laravel: [7.*] - dependency-version: [prefer-lowest, prefer-stable] - include: - - laravel: 7.* - testbench: 5.* - - name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - - steps: - - name: Checkout code - uses: actions/checkout@v1 - - - name: Cache dependencies - uses: actions/cache@v1 - with: - path: ~/.composer/cache/files - key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv - coverage: none - - - name: Install dependencies - run: | - composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:>=4.3.4" --no-interaction --no-update - composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest - - - name: Execute tests - run: vendor/bin/phpunit From b6831bb9114a1cdbf7d5ace00871d3f6179e132a Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 18 Aug 2020 12:58:17 -0400 Subject: [PATCH 0317/1013] Add Laravel 8 support --- .github/workflows/run-tests-L8.yml | 44 ++++++++++++++++++++++++++++++ composer.json | 12 ++++---- 2 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/run-tests-L8.yml diff --git a/.github/workflows/run-tests-L8.yml b/.github/workflows/run-tests-L8.yml new file mode 100644 index 000000000..de0b3fd6d --- /dev/null +++ b/.github/workflows/run-tests-L8.yml @@ -0,0 +1,44 @@ +name: "Run Tests-L8" + +on: [push, pull_request] + +jobs: + test: + + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + php: [7.4, 7.3] + laravel: [8.*] + dependency-version: [prefer-lowest, prefer-stable] + include: + - laravel: 8.* + testbench: 6.* + + name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} + + steps: + - name: Checkout code + uses: actions/checkout@v1 + + - name: Cache dependencies + uses: actions/cache@v1 + with: + path: ~/.composer/cache/files + key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv + coverage: none + + - name: Install dependencies + run: | + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:>=4.3.4" --no-interaction --no-update + composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest + + - name: Execute tests + run: vendor/bin/phpunit diff --git a/composer.json b/composer.json index 7d70cf968..4ccee4679 100644 --- a/composer.json +++ b/composer.json @@ -22,14 +22,14 @@ } ], "require": { - "php" : "^7.2.5|^8.0", - "illuminate/auth": "^5.8|^6.0|^7.0", - "illuminate/container": "^5.8|^6.0|^7.0", - "illuminate/contracts": "^5.8|^6.0|^7.0", - "illuminate/database": "^5.8|^6.0|^7.0" + "php" : "^7.2.5", + "illuminate/auth": "^5.8|^6.0|^7.0|^8.0", + "illuminate/container": "^5.8|^6.0|^7.0|^8.0", + "illuminate/contracts": "^5.8|^6.0|^7.0|^8.0", + "illuminate/database": "^5.8|^6.0|^7.0|^8.0" }, "require-dev": { - "orchestra/testbench": "^3.8|^4.0|^5.0", + "orchestra/testbench": "^3.8|^4.0|^5.0|^6.0", "phpunit/phpunit": "^8.0|^9.0", "predis/predis": "^1.1" }, From c5082ee84e0d128896b4a6864a8502d8c5f1df08 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 18 Aug 2020 13:14:06 -0400 Subject: [PATCH 0318/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a9d49aea..70f0380a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 3.16.0 - 2020-08-18 +- Added Laravel 8 support + ## 3.15.0 - 2020-08-15 - Change `users` relationship type to BelongsToMany From d53bb7894c33f531a6eb71586a4959d7b4845e39 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 7 Sep 2020 23:06:40 -0400 Subject: [PATCH 0319/1013] Update and rename run-tests.yml to run-tests-L7.yml --- .github/workflows/{run-tests.yml => run-tests-L7.yml} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .github/workflows/{run-tests.yml => run-tests-L7.yml} (98%) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests-L7.yml similarity index 98% rename from .github/workflows/run-tests.yml rename to .github/workflows/run-tests-L7.yml index 8b2e37fb1..623d4775a 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests-L7.yml @@ -1,4 +1,4 @@ -name: "Run Tests" +name: "Run Tests - Older" on: [push, pull_request] From de28466aa5b9b52023ce0d8e96378284e20313a7 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 7 Sep 2020 23:07:46 -0400 Subject: [PATCH 0320/1013] Update run-tests-L8.yml --- .github/workflows/run-tests-L8.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests-L8.yml b/.github/workflows/run-tests-L8.yml index de0b3fd6d..aef65b7c0 100644 --- a/.github/workflows/run-tests-L8.yml +++ b/.github/workflows/run-tests-L8.yml @@ -1,4 +1,4 @@ -name: "Run Tests-L8" +name: "Run Tests - Current" on: [push, pull_request] From 96c25c947fb56dafc149b530e270bf109f411a78 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 8 Sep 2020 19:32:52 -0400 Subject: [PATCH 0321/1013] Update README.md --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 20da102db..87dbc3c68 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,11 @@ [![StyleCI](https://styleci.io/repos/42480275/shield)](https://styleci.io/repos/42480275) [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-permission.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-permission) +## Documentation, Installation, and Usage Instructions + +See the [DOCUMENTATION](https://docs.spatie.be/laravel-permission/v3/introduction/) for detailed installation and usage instructions. + +## What It Does This package allows you to manage user permissions and roles in a database. Once installed you can do stuff like this: @@ -45,10 +50,6 @@ We invest a lot of resources into creating [best in class open source packages]( We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). -## Documentation, Installation, and Usage Instructions - -See the [DOCUMENTATION](https://docs.spatie.be/laravel-permission/v3/introduction/) for detailed installation and usage instructions. - ### Testing ``` bash From 3ae567c51188deab24e6a7ca13b85c705b0f6cdd Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 10 Sep 2020 12:19:44 -0400 Subject: [PATCH 0322/1013] Update cache.md --- docs/advanced-usage/cache.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/advanced-usage/cache.md b/docs/advanced-usage/cache.md index 874ea594a..2c283c59e 100644 --- a/docs/advanced-usage/cache.md +++ b/docs/advanced-usage/cache.md @@ -60,3 +60,6 @@ To prevent other applications from accidentally using/changing your cached data, Most multi-tenant "packages" take care of this for you when switching tenants. +### Bypassing Cache During Development + +In development mode you can bypass ALL of Laravel's caching between visits by setting `CACHE_DRIVER=array` in `.env`. You can see an example of this in the default `phpunit.xml` file that comes with a new Laravel install. Of course, don't do this in production though! From 4689858f26128c62b0164d2d9c03f8ea36d6e3bb Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 10 Sep 2020 14:33:01 -0400 Subject: [PATCH 0323/1013] Update example to reflect Laravel 8 --- docs/basic-usage/new-app.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index 4dabc3054..a96647547 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -36,6 +36,7 @@ php artisan migrate:fresh # Add `HasRoles` trait to User model sed -i '' $'s/use Notifiable;/use Notifiable;\\\n use \\\\Spatie\\\\Permission\\\\Traits\\\\HasRoles;/' app/User.php +sed -i '' $'s/use HasFactory, Notifiable;/use HasFactory, Notifiable;\\\n use \\\\Spatie\\\\Permission\\\\Traits\\\\HasRoles;/' app/Models/User.php git add . && git commit -m "Add HasRoles trait" # Add Laravel's basic auth scaffolding @@ -46,7 +47,7 @@ git add . && git commit -m "Setup auth scaffold" ``` ### Add some basic permissions -- Add a new file, `/database/seeds/PermissionsDemoSeeder.php` such as the following (You could create it with `php artisan make:seed` and then edit the file accordingly): +- Add a new file, `/database/seeders/PermissionsDemoSeeder.php` such as the following (You could create it with `php artisan make:seed` and then edit the file accordingly): ```php Date: Thu, 10 Sep 2020 14:40:26 -0400 Subject: [PATCH 0324/1013] Update other.md --- docs/advanced-usage/other.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-usage/other.md b/docs/advanced-usage/other.md index ebcf5627b..662dff4cf 100644 --- a/docs/advanced-usage/other.md +++ b/docs/advanced-usage/other.md @@ -5,4 +5,4 @@ weight: 8 Schema Diagram: -You can find a schema diagram at https://drawsql.app/templates/laravel-permission +You can find a schema diagram at [https://drawsql.app/templates/laravel-permission](https://drawsql.app/templates/laravel-permission) From 70aeab33972f6421873cf6f68fe8392f3987f26e Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 10 Sep 2020 14:41:37 -0400 Subject: [PATCH 0325/1013] Update new-app.md --- docs/basic-usage/new-app.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index a96647547..9975901e7 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -9,7 +9,7 @@ If you want to just try out the features of this package you can get started wit The examples on this page are primarily added for assistance in creating a quick demo app for troubleshooting purposes, to post the repo on github for convenient sharing to collaborate or get support. -If you're new to Laravel or to any of the concepts mentioned here, you can learn more in the [Laravel documentation](https://laravel.com/docs/) and in the free videos at Laracasts such as this series: https://laracasts.com/series/laravel-6-from-scratch/ +If you're new to Laravel or to any of the concepts mentioned here, you can learn more in the [Laravel documentation](https://laravel.com/docs/) and in the free videos at Laracasts such as in the [https://laracasts.com/series/laravel-6-from-scratch/](Laravel 6 from scratch series). ### Initial setup: From c832030832012c996fbaf6a6e2be0d8007648473 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 10 Sep 2020 14:42:03 -0400 Subject: [PATCH 0326/1013] Update new-app.md --- docs/basic-usage/new-app.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index 9975901e7..9e0aa30cc 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -9,7 +9,7 @@ If you want to just try out the features of this package you can get started wit The examples on this page are primarily added for assistance in creating a quick demo app for troubleshooting purposes, to post the repo on github for convenient sharing to collaborate or get support. -If you're new to Laravel or to any of the concepts mentioned here, you can learn more in the [Laravel documentation](https://laravel.com/docs/) and in the free videos at Laracasts such as in the [https://laracasts.com/series/laravel-6-from-scratch/](Laravel 6 from scratch series). +If you're new to Laravel or to any of the concepts mentioned here, you can learn more in the [Laravel documentation](https://laravel.com/docs/) and in the free videos at Laracasts such as in the (Laravel 6 from scratch series)[https://laracasts.com/series/laravel-6-from-scratch/]. ### Initial setup: From 73a121bec51739ca03667c0b6f14bd38e1648481 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 10 Sep 2020 14:42:23 -0400 Subject: [PATCH 0327/1013] Update new-app.md --- docs/basic-usage/new-app.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index 9e0aa30cc..d54e806bc 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -9,7 +9,7 @@ If you want to just try out the features of this package you can get started wit The examples on this page are primarily added for assistance in creating a quick demo app for troubleshooting purposes, to post the repo on github for convenient sharing to collaborate or get support. -If you're new to Laravel or to any of the concepts mentioned here, you can learn more in the [Laravel documentation](https://laravel.com/docs/) and in the free videos at Laracasts such as in the (Laravel 6 from scratch series)[https://laracasts.com/series/laravel-6-from-scratch/]. +If you're new to Laravel or to any of the concepts mentioned here, you can learn more in the [Laravel documentation](https://laravel.com/docs/) and in the free videos at Laracasts such as in the [Laravel 6 from scratch series](https://laracasts.com/series/laravel-6-from-scratch/). ### Initial setup: From 820374cb52f22990f6bb2289757deca3114024c4 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 10 Sep 2020 15:41:28 -0400 Subject: [PATCH 0328/1013] Explain how to set custom cache Stores and disable cache --- docs/advanced-usage/cache.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/advanced-usage/cache.md b/docs/advanced-usage/cache.md index 2c283c59e..492bd11b5 100644 --- a/docs/advanced-usage/cache.md +++ b/docs/advanced-usage/cache.md @@ -60,6 +60,14 @@ To prevent other applications from accidentally using/changing your cached data, Most multi-tenant "packages" take care of this for you when switching tenants. -### Bypassing Cache During Development +### Custom Cache Store -In development mode you can bypass ALL of Laravel's caching between visits by setting `CACHE_DRIVER=array` in `.env`. You can see an example of this in the default `phpunit.xml` file that comes with a new Laravel install. Of course, don't do this in production though! +You can configure the package to use any of the Cache Stores you've configured in Laravel's `config/cache.php`. This way you can point this package's caching to its own specified resource. + +In `config/permission.php` set `cache.store` to the name of any one of the `config/cache.php` stores you've defined. + +#### Disabling Cache + +Setting `'cache.store' => 'array'` in `config/permission.php` will effectively disable caching by this package between requests (it will only cache in-memory until the current request is completed processing, never persisting it). + +Alternatively, in development mode you can bypass ALL of Laravel's caching between visits by setting `CACHE_DRIVER=array` in `.env`. You can see an example of this in the default `phpunit.xml` file that comes with a new Laravel install. Of course, don't do this in production though! From 086e32a4107da33e6f1ee9ba50c8c269e553a1ab Mon Sep 17 00:00:00 2001 From: Lloric Mayuga Garcia Date: Fri, 11 Sep 2020 08:18:55 +0800 Subject: [PATCH 0329/1013] :lock: Add optional guard in middleware Signed-off-by: Lloric Mayuga Garcia --- src/Middlewares/PermissionMiddleware.php | 6 +- src/Middlewares/RoleMiddleware.php | 6 +- .../RoleOrPermissionMiddleware.php | 6 +- tests/MiddlewareTest.php | 133 +++++++++++++++++- 4 files changed, 140 insertions(+), 11 deletions(-) diff --git a/src/Middlewares/PermissionMiddleware.php b/src/Middlewares/PermissionMiddleware.php index 5b2112428..f1eca5a9a 100644 --- a/src/Middlewares/PermissionMiddleware.php +++ b/src/Middlewares/PermissionMiddleware.php @@ -7,9 +7,9 @@ class PermissionMiddleware { - public function handle($request, Closure $next, $permission) + public function handle($request, Closure $next, $permission, $guard = null) { - if (app('auth')->guest()) { + if (app('auth')->guard($guard)->guest()) { throw UnauthorizedException::notLoggedIn(); } @@ -18,7 +18,7 @@ public function handle($request, Closure $next, $permission) : explode('|', $permission); foreach ($permissions as $permission) { - if (app('auth')->user()->can($permission)) { + if (app('auth')->guard($guard)->user()->can($permission)) { return $next($request); } } diff --git a/src/Middlewares/RoleMiddleware.php b/src/Middlewares/RoleMiddleware.php index 6c238f175..2c679d5c9 100644 --- a/src/Middlewares/RoleMiddleware.php +++ b/src/Middlewares/RoleMiddleware.php @@ -8,9 +8,9 @@ class RoleMiddleware { - public function handle($request, Closure $next, $role) + public function handle($request, Closure $next, $role, $guard = null) { - if (Auth::guest()) { + if (Auth::guard($guard)->guest()) { throw UnauthorizedException::notLoggedIn(); } @@ -18,7 +18,7 @@ public function handle($request, Closure $next, $role) ? $role : explode('|', $role); - if (! Auth::user()->hasAnyRole($roles)) { + if (! Auth::guard($guard)->user()->hasAnyRole($roles)) { throw UnauthorizedException::forRoles($roles); } diff --git a/src/Middlewares/RoleOrPermissionMiddleware.php b/src/Middlewares/RoleOrPermissionMiddleware.php index bb2f94028..f8d5fa3b8 100644 --- a/src/Middlewares/RoleOrPermissionMiddleware.php +++ b/src/Middlewares/RoleOrPermissionMiddleware.php @@ -8,9 +8,9 @@ class RoleOrPermissionMiddleware { - public function handle($request, Closure $next, $roleOrPermission) + public function handle($request, Closure $next, $roleOrPermission, $guard = null) { - if (Auth::guest()) { + if (Auth::guard($guard)->guest()) { throw UnauthorizedException::notLoggedIn(); } @@ -18,7 +18,7 @@ public function handle($request, Closure $next, $roleOrPermission) ? $roleOrPermission : explode('|', $roleOrPermission); - if (! Auth::user()->hasAnyRole($rolesOrPermissions) && ! Auth::user()->hasAnyPermission($rolesOrPermissions)) { + if (! Auth::guard($guard)->user()->hasAnyRole($rolesOrPermissions) && ! Auth::guard($guard)->user()->hasAnyPermission($rolesOrPermissions)) { throw UnauthorizedException::forRolesOrPermissions($rolesOrPermissions); } diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php index 459fca3c7..2ffe607d8 100644 --- a/tests/MiddlewareTest.php +++ b/tests/MiddlewareTest.php @@ -4,6 +4,7 @@ use Illuminate\Http\Request; use Illuminate\Http\Response; +use InvalidArgumentException; use Illuminate\Support\Facades\Auth; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Middlewares\RoleMiddleware; @@ -313,12 +314,140 @@ public function the_required_permissions_can_be_fetched_from_the_exception() $this->assertEquals(['some-permission'], $requiredPermissions); } - protected function runMiddleware($middleware, $parameter) + /** @test */ + public function use_not_existing_custom_guard_in_role_or_permission() + { + $class = null; + + try { + $this->roleOrPermissionMiddleware->handle(new Request(), function () { + return (new Response())->setContent(''); + }, 'testRole', 'xxx'); + } catch (InvalidArgumentException $e) { + $class = get_class($e); + } + + $this->assertEquals(InvalidArgumentException::class, $class); + } + + /** @test */ + public function use_not_existing_custom_guard_in_permission() + { + $class = null; + + try { + $this->permissionMiddleware->handle(new Request(), function () { + return (new Response())->setContent(''); + }, 'edit-articles', 'xxx'); + } catch (InvalidArgumentException $e) { + $class = get_class($e); + } + + $this->assertEquals(InvalidArgumentException::class, $class); + } + + /** @test */ + public function use_not_existing_custom_guard_in_role() + { + $class = null; + + try { + $this->roleMiddleware->handle(new Request(), function () { + return (new Response())->setContent(''); + }, 'testRole', 'xxx'); + } catch (InvalidArgumentException $e) { + $class = get_class($e); + } + + $this->assertEquals(InvalidArgumentException::class, $class); + } + + /** @test */ + public function user_can_not_access_role_with_guard_admin_while_using_default_guard() + { + Auth::login($this->testUser); + + $this->testUser->assignRole('testRole'); + + $this->assertEquals( + $this->runMiddleware( + $this->roleMiddleware, 'testRole', 'admin' + ), 403); + } + + /** @test */ + public function user_can_access_role_with_guard_admin_while_using_default_guard() + { + Auth::guard('admin')->login($this->testAdmin); + + $this->testAdmin->assignRole('testAdminRole'); + + $this->assertEquals( + $this->runMiddleware( + $this->roleMiddleware, 'testAdminRole', 'admin' + ), 200); + } + + /** @test */ + public function user_can_not_access_permission_with_guard_admin_while_using_default_guard() + { + Auth::login($this->testUser); + + $this->testUser->givePermissionTo('edit-articles'); + + $this->assertEquals( + $this->runMiddleware( + $this->permissionMiddleware, 'edit-articles', 'admin' + ), 403); + } + + /** @test */ + public function user_can_access_permission_with_guard_admin_while_using_default_guard() + { + Auth::guard('admin')->login($this->testAdmin); + + $this->testAdmin->givePermissionTo('admin-permission'); + + $this->assertEquals( + $this->runMiddleware( + $this->permissionMiddleware, 'admin-permission', 'admin' + ), 200); + } + + /** @test */ + public function user_can_not_access_permission_or_role_with_guard_admin_while_using_default_guard() + { + Auth::login($this->testUser); + + $this->testUser->assignRole('testRole'); + $this->testUser->givePermissionTo('edit-articles'); + + $this->assertEquals( + $this->runMiddleware( + $this->roleOrPermissionMiddleware, 'edit-articles|testRole', 'admin' + ), 403); + } + + /** @test */ + public function user_can_access_permission_or_role_with_guard_admin_while_using_default_guard() + { + Auth::guard('admin')->login($this->testAdmin); + + $this->testAdmin->assignRole('testAdminRole'); + $this->testAdmin->givePermissionTo('admin-permission'); + + $this->assertEquals( + $this->runMiddleware( + $this->roleOrPermissionMiddleware, 'admin-permission|testAdminRole', 'admin' + ), 200); + } + + protected function runMiddleware($middleware, $parameter, $guard = null) { try { return $middleware->handle(new Request(), function () { return (new Response())->setContent(''); - }, $parameter)->status(); + }, $parameter, $guard)->status(); } catch (UnauthorizedException $e) { return $e->getStatusCode(); } From 056b011718a8eb367a7fd8f3bebb591cc3fa8ba5 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 10 Sep 2020 22:28:07 -0400 Subject: [PATCH 0330/1013] Update extending.md Capturing comments from https://github.com/spatie/laravel-permission/issues/749#issuecomment-670308380 --- docs/advanced-usage/extending.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/advanced-usage/extending.md b/docs/advanced-usage/extending.md index c7185ab92..673ce7f40 100644 --- a/docs/advanced-usage/extending.md +++ b/docs/advanced-usage/extending.md @@ -10,6 +10,17 @@ By default Laravel does this in `\App\User` by extending `Illuminate\Foundation\ If you are creating your own User models and wish Authorization features to be available, you need to implement `Illuminate\Contracts\Auth\Access\Authorizable` in one of those ways as well. +#### Child User Models + +Due to the nature of polymorphism and Eloquent's hard-coded mapping of model names in the database, setting relationships for child models that inherit permissions of the parent can be difficult (even near impossible depending on app requirements, especially when attempting to do inverse mappings). However, one thing you might consider if you need the child model to never have its own permissions/roles but to only use its parent's permissions/roles, is to [override the `getMorphClass` method on the model](https://github.com/laravel/framework/issues/17830#issuecomment-345619085). + +eg: This could be useful, but only if you're willing to give up the child's independence for roles/permissions: +```php + public function getMorphClass() + { + return 'users'; + } +``` ## Extending Role and Permission Models If you are extending or replacing the role/permission models, you will need to specify your new models in this package's `config/permission.php` file. @@ -30,6 +41,7 @@ If you need to REPLACE the existing `Role` or `Permission` models you need to ke - Your `Role` model needs to implement the `Spatie\Permission\Contracts\Role` contract - Your `Permission` model needs to implement the `Spatie\Permission\Contracts\Permission` contract + ## Migrations - Adding fields to your models You can add your own migrations to make changes to the role/permission tables, as you would for adding/changing fields in any other tables in your Laravel project. Following that, you can add any necessary logic for interacting with those fields into your custom/extended Models. From ccb22bfc3622ffac3cf7d19aa8376db3cb75850e Mon Sep 17 00:00:00 2001 From: mdrathik Date: Fri, 11 Sep 2020 14:10:41 +0600 Subject: [PATCH 0331/1013] Update Spatie Logo (#1533) * Update Spatie Logo here was a broken image link, just updated Spatie Logo to fix that * Update README.md Co-authored-by: Alex Vanderbist --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 87dbc3c68..a63729359 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@
- +
If you want to quickly add authentication and authorization to Laravel projects, feel free to check Auth0's Laravel SDK and free plan at https://auth0.com/overview.
From adbcf7849e2c8d2b4e74b7e3188f44d2d9872d75 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 14 Sep 2020 21:16:14 -0400 Subject: [PATCH 0332/1013] Update uuid.md --- docs/advanced-usage/uuid.md | 39 ++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index fd3362e79..a37d1e437 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -82,7 +82,7 @@ For this, in the configuration file edit `column_names.model_morph_key`: ### Models If you want all the role/permission objects to have a UUID instead of an integer, you will need to Extend the default Role and Permission models into your own namespace in order to set some specific properties. (See the Extending section of the docs, where it explains requirements of Extending, as well as the configuration settings you need to update.) -- You may want to set `protected $keyType = 'string';` so Laravel handles joins as strings and doesn't cast to integer. +- You likely want to set `protected $keyType = 'string';` so Laravel handles joins as strings and doesn't cast to integer. - OPTIONAL: If you changed the field name in your migrations, you must set `protected $primaryKey = 'uuid';` to match. - Usually for UUID you will also set `public $incrementing = false;`. Remove it if it causes problems for you. @@ -96,17 +96,27 @@ It is common to use a trait to handle the $keyType and $incrementing settings, a trait UuidTrait { - public $incrementing = false; - protected $keyType = 'string'; - - protected static function boot() + protected static function bootUuidTrait() { parent::boot(); static::creating(function ($model) { + $model->keyType = 'string'; + $model->incrementing = false; + $model->{$model->getKeyName()} = $model->{$model->getKeyName()} ?: (string) Str::orderedUuid(); }); } + + public function getIncrementing() + { + return false; + } + + public function getKeyType() + { + return 'string'; + } } ``` @@ -117,3 +127,22 @@ In the default User model provided with Laravel, this is done by extending anoth However, your app's UUID implementation may need to override that in order to set some of the properties mentioned in the Models section above. If you are running into difficulties, you may want to double-check whether your User model is doing UUIDs consistent with other parts of your app. + + +## REMINDER: + +> THIS IS NOT A FULL LESSON ON HOW TO IMPLEMENT UUIDs IN YOUR APP. + +Again, since each UUID implementation approach is different, some of these may or may not benefit you. As always, your implementation may vary. + + + +### Packages +There are many packages offering UUID features for Eloquent models. You may want to explore whether these are of value to you in your study of implementing UUID in your applications: + +https://github.com/JamesHemery/laravel-uuid +https://github.com/jamesmills/eloquent-uuid +https://github.com/goldspecdigital/laravel-eloquent-uuid +https://github.com/michaeldyrynda/laravel-model-uuid + +Remember: always make sure you understand what a package is doing before you use it! If it's doing "more than what you need" then you're adding more complexity to your application, as well as more things to test and support! From 35d40a45e49f5713f477823b571e05ef6a3a0394 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 16 Sep 2020 12:47:18 -0400 Subject: [PATCH 0333/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70f0380a5..bd9f63c90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 3.17.0 - 2020-09-16 +- Optional `$guard` parameter may be passed to `RoleMiddleware`, `PermissionMiddleware`, and `RoleOrPermissionMiddleware`. See #1565 + ## 3.16.0 - 2020-08-18 - Added Laravel 8 support From 26e90d57cbb98aa3b28a7e770ca05f2de7af65ec Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 16 Sep 2020 13:13:34 -0400 Subject: [PATCH 0334/1013] Update new-app.md --- docs/basic-usage/new-app.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index d54e806bc..d65e71464 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -24,7 +24,7 @@ git commit -m "Fresh Laravel Install" # Environment cp -n .env.example .env sed -i '' 's/DB_CONNECTION=mysql/DB_CONNECTION=sqlite/' .env -sed -i '' 's/DB_DATABASE=laravel/#DB_DATABASE=laravel/' .env +sed -i '' 's/DB_DATABASE=/#DB_DATABASE=/' .env touch database/database.sqlite # Package @@ -52,6 +52,8 @@ git add . && git commit -m "Setup auth scaffold" ```php create([ + $user = \App\Models\User::factory()->create([ 'name' => 'Example User', 'email' => 'test@example.com', ]); $user->assignRole($role1); - $user = Factory(App\User::class)->create([ + $user = \App\Models\User::factory()->create([ 'name' => 'Example Admin User', 'email' => 'admin@example.com', ]); $user->assignRole($role2); - $user = Factory(App\User::class)->create([ + $user = \App\Models\User::factory()->create([ 'name' => 'Example Super-Admin User', 'email' => 'superadmin@example.com', ]); From feab049e489a15fcfd574491fa43fa7152c7a913 Mon Sep 17 00:00:00 2001 From: Sam Bellen Date: Thu, 24 Sep 2020 13:32:48 +0200 Subject: [PATCH 0335/1013] Update Auth0 sponsorship link Hey Freek and team Spatie We recently launched a new page specifically geared towards developers on auth0.com. Can we change the link in the sponsorship message? Thanks again for your open-source work! --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a63729359..9534fce88 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ - +
If you want to quickly add authentication and authorization to Laravel projects, feel free to check Auth0's Laravel SDK and free plan at https://auth0.com/overview.If you want to quickly add authentication and authorization to Laravel projects, feel free to check Auth0's Laravel SDK and free plan at https://auth0.com/developers.
From 194cf4c130fb550deea5e203c7748b6c3060b212 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 27 Sep 2020 16:33:32 -0400 Subject: [PATCH 0336/1013] Update and rename unit-testing.md to testing.md --- docs/advanced-usage/{unit-testing.md => testing.md} | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) rename docs/advanced-usage/{unit-testing.md => testing.md} (63%) diff --git a/docs/advanced-usage/unit-testing.md b/docs/advanced-usage/testing.md similarity index 63% rename from docs/advanced-usage/unit-testing.md rename to docs/advanced-usage/testing.md index ad12b9ea9..d814166b6 100644 --- a/docs/advanced-usage/unit-testing.md +++ b/docs/advanced-usage/testing.md @@ -1,9 +1,13 @@ --- -title: Unit testing +title: Testing weight: 1 --- -In your application's tests, if you are not seeding roles and permissions as part of your test `setUp()` then you may run into a chicken/egg situation where roles and permissions aren't registered with the gate (because your tests create them after that gate registration is done). Working around this is simple: In your tests simply add a `setUp()` instruction to re-register the permissions, like this: +## Clear Cache During Tests + +In your application's tests, if you are not seeding roles and permissions as part of your test `setUp()` then you may run into a chicken/egg situation where roles and permissions aren't registered with the gate (because your tests create them after that gate registration is done). Working around this is simple: + +In your tests simply add a `setUp()` instruction to re-register the permissions, like this: ```php public function setUp(): void @@ -11,7 +15,7 @@ In your application's tests, if you are not seeding roles and permissions as par // first include all the normal setUp operations parent::setUp(); - // now re-register all the roles and permissions + // now re-register all the roles and permissions (clears cache and reloads relations) $this->app->make(\Spatie\Permission\PermissionRegistrar::class)->registerPermissions(); } ``` From c4d3a213c62d5235c203c0df3487386f611038ad Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 27 Sep 2020 16:39:58 -0400 Subject: [PATCH 0337/1013] Add note about Laravel 8 HasFactory trait Closes #1574 --- docs/advanced-usage/testing.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/advanced-usage/testing.md b/docs/advanced-usage/testing.md index d814166b6..bd9e64c50 100644 --- a/docs/advanced-usage/testing.md +++ b/docs/advanced-usage/testing.md @@ -20,3 +20,13 @@ In your tests simply add a `setUp()` instruction to re-register the permissions, } ``` +## Factories + +Many applications do not require using factories to create fake roles/permissions for testing, because they use a Seeder to create specific roles and permissions that the application uses; thus tests are performed using the declared roles and permissions. + +However, if your application allows users to define their own roles and permissions you may wish to use Model Factories to generate roles and permissions as part of your test suite. + +With Laravel 7 you can simply create a model factory using the artisan command, and then call the `factory()` helper function to invoke it as needed. + +With Laravel 8 if you want to use the class-based Model Factory features you will need to `extend` this package's `Role` and/or `Permission` model into your app's namespace, add the `HasFactory` trait to it, and define a model factory for it. Then you can use that factory in your seeders like any other factory related to your app's models. + From 1109a196bb44cc2484db40da03c2ebce30b0377b Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 27 Sep 2020 16:42:27 -0400 Subject: [PATCH 0338/1013] Update seeding.md --- docs/advanced-usage/seeding.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/advanced-usage/seeding.md b/docs/advanced-usage/seeding.md index 0e04e40f1..782ad1308 100644 --- a/docs/advanced-usage/seeding.md +++ b/docs/advanced-usage/seeding.md @@ -12,7 +12,10 @@ You may discover that it is best to flush this package's cache before seeding, t app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions(); ``` -This can be done directly in a seeder class. +You can do this in the `SetUp()` method of your test suite (see the Testing page in the docs). + +Or it can be done directly in a seeder class: + Here is a sample seeder, which first clears the cache, creates permissions and then assigns permissions to roles (the order of these steps is intentional): ```php From dd430b78153e7adc2878e6896cf98a289bb72f48 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 27 Sep 2020 16:42:51 -0400 Subject: [PATCH 0339/1013] Update seeding.md --- docs/advanced-usage/seeding.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-usage/seeding.md b/docs/advanced-usage/seeding.md index 782ad1308..e8b1fa266 100644 --- a/docs/advanced-usage/seeding.md +++ b/docs/advanced-usage/seeding.md @@ -14,7 +14,7 @@ app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions(); You can do this in the `SetUp()` method of your test suite (see the Testing page in the docs). -Or it can be done directly in a seeder class: +Or it can be done directly in a seeder class, as shown below. Here is a sample seeder, which first clears the cache, creates permissions and then assigns permissions to roles (the order of these steps is intentional): From 311237712f8b7b62706c18ab625d3f5a49b16dc2 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Thu, 1 Oct 2020 01:26:54 +0200 Subject: [PATCH 0340/1013] Delete deploy-docs.yml --- .github/workflows/deploy-docs.yml | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 .github/workflows/deploy-docs.yml diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml deleted file mode 100644 index 056cec269..000000000 --- a/.github/workflows/deploy-docs.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: deploy-docs - -on: - push: - branches: - - master - paths: - - 'docs/**' - -jobs: - deploy: - - runs-on: ubuntu-latest - - name: Deploy docs - - steps: - - name: Netlify deploy - uses: wei/curl@v1 - with: - args: -X POST -d {} ${{ secrets.NETLIFY_DEPLOY_URL }}?trigger_title=laravel-permission From 373a32e9e7aad8182cb72eb186889a2fe9221c7e Mon Sep 17 00:00:00 2001 From: freek Date: Thu, 1 Oct 2020 01:30:55 +0200 Subject: [PATCH 0341/1013] bring package up-to-date with latest spatie package standards --- .github/FUNDING.yml | 2 +- .github/workflows/php-cs-fixer.yml | 29 +++++++++++++++++++++++ .gitignore | 2 ++ .php_cs | 37 ++++++++++++++++++++++++++++++ .scrutinizer.yml | 19 --------------- .styleci.yml | 10 -------- README.md | 1 - phpunit.xml.dist | 37 +++++++++++------------------- 8 files changed, 83 insertions(+), 54 deletions(-) create mode 100644 .github/workflows/php-cs-fixer.yml create mode 100644 .php_cs delete mode 100644 .scrutinizer.yml delete mode 100644 .styleci.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 636ef5381..5ccc87cfb 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -custom: https://spatie.be/open-source/support-us +github: spatie diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml new file mode 100644 index 000000000..5cb3a86dc --- /dev/null +++ b/.github/workflows/php-cs-fixer.yml @@ -0,0 +1,29 @@ +name: Check & fix styling + +on: [push] + +jobs: + style: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v1 + + - name: Fix style + uses: docker://oskarstark/php-cs-fixer-ga + with: + args: --config=.php_cs --allow-risky=yes + + - name: Extract branch name + shell: bash + run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" + id: extract_branch + + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v2.3.0 + with: + commit_message: Fix styling + branch: ${{ steps.extract_branch.outputs.branch }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 8ddc91365..f7129a1c7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ vendor tests/temp .idea .phpunit.result.cache +.php_cs.cache + diff --git a/.php_cs b/.php_cs new file mode 100644 index 000000000..1c4e7d562 --- /dev/null +++ b/.php_cs @@ -0,0 +1,37 @@ +notPath('bootstrap/*') + ->notPath('storage/*') + ->notPath('resources/view/mail/*') + ->in([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]) + ->name('*.php') + ->notName('*.blade.php') + ->ignoreDotFiles(true) + ->ignoreVCS(true); + +return PhpCsFixer\Config::create() + ->setRules([ + '@PSR2' => true, + 'array_syntax' => ['syntax' => 'short'], + 'ordered_imports' => ['sortAlgorithm' => 'alpha'], + 'no_unused_imports' => true, + 'not_operator_with_successor_space' => true, + 'trailing_comma_in_multiline_array' => true, + 'phpdoc_scalar' => true, + 'unary_operator_spaces' => true, + 'binary_operator_spaces' => true, + 'blank_line_before_statement' => [ + 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], + ], + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_var_without_name' => true, + 'method_argument_space' => [ + 'on_multiline' => 'ensure_fully_multiline', + 'keep_multiple_spaces_after_comma' => true, + ] + ]) + ->setFinder($finder); diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index df16b68b5..000000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,19 +0,0 @@ -filter: - excluded_paths: [tests/*] - -checks: - php: - remove_extra_empty_lines: true - remove_php_closing_tag: true - remove_trailing_whitespace: true - fix_use_statements: - remove_unused: true - preserve_multiple: false - preserve_blanklines: true - order_alphabetically: true - fix_php_opening_tag: true - fix_linefeed: true - fix_line_ending: true - fix_identation_4spaces: true - fix_doc_comments: true - diff --git a/.styleci.yml b/.styleci.yml deleted file mode 100644 index e5db67cae..000000000 --- a/.styleci.yml +++ /dev/null @@ -1,10 +0,0 @@ -preset: laravel - -enabled: -- length_ordered_imports -- long_list_syntax - -disabled: -- short_list_syntax -- alpha_ordered_imports -- single_class_element_per_statement diff --git a/README.md b/README.md index 9534fce88..5006435e1 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-permission.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-permission) ![](https://github.com/spatie/laravel-permission/workflows/Run%20Tests/badge.svg?branch=master) -[![StyleCI](https://styleci.io/repos/42480275/shield)](https://styleci.io/repos/42480275) [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-permission.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-permission) ## Documentation, Installation, and Usage Instructions diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ffd168cde..6cd82acfb 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,25 +1,16 @@ - - - - tests - - - - - src/ - - - - - + + + + src/ + + + + + tests + + + + + From 5ba8bc7e40737cd6e328d50a125b6f147b1b61f5 Mon Sep 17 00:00:00 2001 From: freek Date: Thu, 1 Oct 2020 01:31:40 +0200 Subject: [PATCH 0342/1013] update workflow step versions --- .github/workflows/php-cs-fixer.yml | 2 +- .github/workflows/run-tests-L7.yml | 4 ++-- .github/workflows/run-tests-L8.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml index 5cb3a86dc..84ab01ad2 100644 --- a/.github/workflows/php-cs-fixer.yml +++ b/.github/workflows/php-cs-fixer.yml @@ -8,7 +8,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Fix style uses: docker://oskarstark/php-cs-fixer-ga diff --git a/.github/workflows/run-tests-L7.yml b/.github/workflows/run-tests-L7.yml index 623d4775a..75baade7d 100644 --- a/.github/workflows/run-tests-L7.yml +++ b/.github/workflows/run-tests-L7.yml @@ -24,10 +24,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Cache dependencies - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ~/.composer/cache/files key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} diff --git a/.github/workflows/run-tests-L8.yml b/.github/workflows/run-tests-L8.yml index aef65b7c0..d9dd5fe72 100644 --- a/.github/workflows/run-tests-L8.yml +++ b/.github/workflows/run-tests-L8.yml @@ -20,10 +20,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Cache dependencies - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ~/.composer/cache/files key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} From 2236f8545dc67be95dc44388bf3ce85df52b8a31 Mon Sep 17 00:00:00 2001 From: freekmurze Date: Wed, 30 Sep 2020 23:32:27 +0000 Subject: [PATCH 0343/1013] Fix styling --- src/Commands/CreateRole.php | 2 +- src/Commands/Show.php | 2 +- src/Exceptions/GuardDoesNotMatch.php | 2 +- src/Models/Permission.php | 14 +- src/Models/Role.php | 10 +- src/PermissionRegistrar.php | 6 +- src/PermissionServiceProvider.php | 4 +- src/Traits/HasPermissions.php | 10 +- src/Traits/HasRoles.php | 10 +- tests/Admin.php | 6 +- tests/CacheTest.php | 7 +- tests/CommandTest.php | 2 +- tests/HasPermissionsTest.php | 2 +- tests/HasRolesTest.php | 2 +- tests/Manager.php | 6 +- tests/MiddlewareTest.php | 185 +++++++++++++++++++-------- tests/RoleTest.php | 6 +- tests/TestCase.php | 12 +- tests/User.php | 6 +- tests/WildcardHasPermissionsTest.php | 2 +- tests/WildcardMiddlewareTest.php | 46 +++++-- 21 files changed, 222 insertions(+), 120 deletions(-) diff --git a/src/Commands/CreateRole.php b/src/Commands/CreateRole.php index b43db26e0..426d6f0c7 100644 --- a/src/Commands/CreateRole.php +++ b/src/Commands/CreateRole.php @@ -3,8 +3,8 @@ namespace Spatie\Permission\Commands; use Illuminate\Console\Command; -use Spatie\Permission\Contracts\Role as RoleContract; use Spatie\Permission\Contracts\Permission as PermissionContract; +use Spatie\Permission\Contracts\Role as RoleContract; class CreateRole extends Command { diff --git a/src/Commands/Show.php b/src/Commands/Show.php index e8a53946b..c0aa0e33d 100644 --- a/src/Commands/Show.php +++ b/src/Commands/Show.php @@ -4,8 +4,8 @@ use Illuminate\Console\Command; use Illuminate\Support\Collection; -use Spatie\Permission\Contracts\Role as RoleContract; use Spatie\Permission\Contracts\Permission as PermissionContract; +use Spatie\Permission\Contracts\Role as RoleContract; class Show extends Command { diff --git a/src/Exceptions/GuardDoesNotMatch.php b/src/Exceptions/GuardDoesNotMatch.php index 1e9262821..6506bb658 100644 --- a/src/Exceptions/GuardDoesNotMatch.php +++ b/src/Exceptions/GuardDoesNotMatch.php @@ -2,8 +2,8 @@ namespace Spatie\Permission\Exceptions; -use InvalidArgumentException; use Illuminate\Support\Collection; +use InvalidArgumentException; class GuardDoesNotMatch extends InvalidArgumentException { diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 5397f8e6f..054eb2905 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -2,16 +2,16 @@ namespace Spatie\Permission\Models; -use Spatie\Permission\Guard; -use Illuminate\Support\Collection; -use Spatie\Permission\Traits\HasRoles; use Illuminate\Database\Eloquent\Model; -use Spatie\Permission\PermissionRegistrar; -use Spatie\Permission\Traits\RefreshesPermissionCache; -use Spatie\Permission\Exceptions\PermissionDoesNotExist; use Illuminate\Database\Eloquent\Relations\BelongsToMany; -use Spatie\Permission\Exceptions\PermissionAlreadyExists; +use Illuminate\Support\Collection; use Spatie\Permission\Contracts\Permission as PermissionContract; +use Spatie\Permission\Exceptions\PermissionAlreadyExists; +use Spatie\Permission\Exceptions\PermissionDoesNotExist; +use Spatie\Permission\Guard; +use Spatie\Permission\PermissionRegistrar; +use Spatie\Permission\Traits\HasRoles; +use Spatie\Permission\Traits\RefreshesPermissionCache; class Permission extends Model implements PermissionContract { diff --git a/src/Models/Role.php b/src/Models/Role.php index cf9551674..5fd3177fc 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -2,15 +2,15 @@ namespace Spatie\Permission\Models; -use Spatie\Permission\Guard; use Illuminate\Database\Eloquent\Model; -use Spatie\Permission\Traits\HasPermissions; -use Spatie\Permission\Exceptions\RoleDoesNotExist; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Spatie\Permission\Contracts\Role as RoleContract; use Spatie\Permission\Exceptions\GuardDoesNotMatch; use Spatie\Permission\Exceptions\RoleAlreadyExists; -use Spatie\Permission\Contracts\Role as RoleContract; +use Spatie\Permission\Exceptions\RoleDoesNotExist; +use Spatie\Permission\Guard; +use Spatie\Permission\Traits\HasPermissions; use Spatie\Permission\Traits\RefreshesPermissionCache; -use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Role extends Model implements RoleContract { diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index a0ee15054..acad62f26 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -3,11 +3,11 @@ namespace Spatie\Permission; use Illuminate\Cache\CacheManager; -use Illuminate\Support\Collection; -use Spatie\Permission\Contracts\Role; +use Illuminate\Contracts\Auth\Access\Authorizable; use Illuminate\Contracts\Auth\Access\Gate; +use Illuminate\Support\Collection; use Spatie\Permission\Contracts\Permission; -use Illuminate\Contracts\Auth\Access\Authorizable; +use Spatie\Permission\Contracts\Role; class PermissionRegistrar { diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 55703fbb4..e45361b18 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -2,13 +2,13 @@ namespace Spatie\Permission; +use Illuminate\Filesystem\Filesystem; use Illuminate\Routing\Route; use Illuminate\Support\Collection; -use Illuminate\Filesystem\Filesystem; use Illuminate\Support\ServiceProvider; use Illuminate\View\Compilers\BladeCompiler; -use Spatie\Permission\Contracts\Role as RoleContract; use Spatie\Permission\Contracts\Permission as PermissionContract; +use Spatie\Permission\Contracts\Role as RoleContract; class PermissionServiceProvider extends ServiceProvider { diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 804685795..532e815ce 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -2,16 +2,16 @@ namespace Spatie\Permission\Traits; -use Spatie\Permission\Guard; -use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Builder; -use Spatie\Permission\WildcardPermission; -use Spatie\Permission\PermissionRegistrar; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Support\Collection; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Exceptions\GuardDoesNotMatch; use Spatie\Permission\Exceptions\PermissionDoesNotExist; -use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Spatie\Permission\Exceptions\WildcardPermissionInvalidArgument; +use Spatie\Permission\Guard; +use Spatie\Permission\PermissionRegistrar; +use Spatie\Permission\WildcardPermission; trait HasPermissions { diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 4bc56e991..55376d4b4 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -2,11 +2,11 @@ namespace Spatie\Permission\Traits; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Support\Collection; use Spatie\Permission\Contracts\Role; -use Illuminate\Database\Eloquent\Builder; use Spatie\Permission\PermissionRegistrar; -use Illuminate\Database\Eloquent\Relations\BelongsToMany; trait HasRoles { @@ -127,7 +127,8 @@ function ($object) use ($roles, $model) { $object->roles()->sync($roles, false); $object->load('roles'); $modelLastFiredOn = $object; - }); + } + ); } $this->forgetCachedPermissions(); @@ -251,7 +252,8 @@ public function hasAllRoles($roles, string $guard = null): bool return $roles->intersect( $guard ? $this->roles->where('guard_name', $guard)->pluck('name') - : $this->getRoleNames()) == $roles; + : $this->getRoleNames() + ) == $roles; } /** diff --git a/tests/Admin.php b/tests/Admin.php index 302c37df4..ede545b9e 100644 --- a/tests/Admin.php +++ b/tests/Admin.php @@ -3,11 +3,11 @@ namespace Spatie\Permission\Test; use Illuminate\Auth\Authenticatable; -use Spatie\Permission\Traits\HasRoles; +use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; +use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Auth\Access\Authorizable; -use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; -use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; +use Spatie\Permission\Traits\HasRoles; class Admin extends Model implements AuthorizableContract, AuthenticatableContract { diff --git a/tests/CacheTest.php b/tests/CacheTest.php index 9256e6c45..1a1caa0ff 100644 --- a/tests/CacheTest.php +++ b/tests/CacheTest.php @@ -2,12 +2,12 @@ namespace Spatie\Permission\Test; -use Illuminate\Support\Facades\DB; -use Spatie\Permission\Contracts\Role; use Illuminate\Support\Facades\Artisan; -use Spatie\Permission\PermissionRegistrar; +use Illuminate\Support\Facades\DB; use Spatie\Permission\Contracts\Permission; +use Spatie\Permission\Contracts\Role; use Spatie\Permission\Exceptions\PermissionDoesNotExist; +use Spatie\Permission\PermissionRegistrar; class CacheTest extends TestCase { @@ -34,6 +34,7 @@ public function setUp(): void case $cacheStore instanceof \Illuminate\Cache\DatabaseStore: $this->cache_init_count = 1; $this->cache_load_count = 1; + // no break default: } } diff --git a/tests/CommandTest.php b/tests/CommandTest.php index bd5e911f6..b900b8162 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -2,9 +2,9 @@ namespace Spatie\Permission\Test; -use Spatie\Permission\Models\Role; use Illuminate\Support\Facades\Artisan; use Spatie\Permission\Models\Permission; +use Spatie\Permission\Models\Role; class CommandTest extends TestCase { diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index b49a9ea58..bd718d0ba 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -2,8 +2,8 @@ namespace Spatie\Permission\Test; -use Spatie\Permission\Contracts\Role; use Spatie\Permission\Contracts\Permission; +use Spatie\Permission\Contracts\Role; use Spatie\Permission\Exceptions\GuardDoesNotMatch; use Spatie\Permission\Exceptions\PermissionDoesNotExist; diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 761f46574..902d520dd 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -3,8 +3,8 @@ namespace Spatie\Permission\Test; use Spatie\Permission\Contracts\Role; -use Spatie\Permission\Exceptions\RoleDoesNotExist; use Spatie\Permission\Exceptions\GuardDoesNotMatch; +use Spatie\Permission\Exceptions\RoleDoesNotExist; class HasRolesTest extends TestCase { diff --git a/tests/Manager.php b/tests/Manager.php index e2928e18d..9bd21928d 100644 --- a/tests/Manager.php +++ b/tests/Manager.php @@ -3,11 +3,11 @@ namespace Spatie\Permission\Test; use Illuminate\Auth\Authenticatable; -use Spatie\Permission\Traits\HasRoles; +use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; +use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Auth\Access\Authorizable; -use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; -use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; +use Spatie\Permission\Traits\HasRoles; class Manager extends Model implements AuthorizableContract, AuthenticatableContract { diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php index 2ffe607d8..0787922a5 100644 --- a/tests/MiddlewareTest.php +++ b/tests/MiddlewareTest.php @@ -4,12 +4,12 @@ use Illuminate\Http\Request; use Illuminate\Http\Response; -use InvalidArgumentException; use Illuminate\Support\Facades\Auth; +use InvalidArgumentException; use Spatie\Permission\Contracts\Permission; -use Spatie\Permission\Middlewares\RoleMiddleware; use Spatie\Permission\Exceptions\UnauthorizedException; use Spatie\Permission\Middlewares\PermissionMiddleware; +use Spatie\Permission\Middlewares\RoleMiddleware; use Spatie\Permission\Middlewares\RoleOrPermissionMiddleware; class MiddlewareTest extends TestCase @@ -34,8 +34,11 @@ public function a_guest_cannot_access_a_route_protected_by_the_role_or_permissio { $this->assertEquals( $this->runMiddleware( - $this->roleOrPermissionMiddleware, 'testRole' - ), 403); + $this->roleOrPermissionMiddleware, + 'testRole' + ), + 403 + ); } /** @test */ @@ -43,8 +46,11 @@ public function a_guest_cannot_access_a_route_protected_by_rolemiddleware() { $this->assertEquals( $this->runMiddleware( - $this->roleMiddleware, 'testRole' - ), 403); + $this->roleMiddleware, + 'testRole' + ), + 403 + ); } /** @test */ @@ -56,8 +62,11 @@ public function a_user_cannot_access_a_route_protected_by_role_middleware_of_ano $this->assertEquals( $this->runMiddleware( - $this->roleMiddleware, 'testAdminRole' - ), 403); + $this->roleMiddleware, + 'testAdminRole' + ), + 403 + ); } /** @test */ @@ -69,8 +78,11 @@ public function a_user_can_access_a_route_protected_by_role_middleware_if_have_t $this->assertEquals( $this->runMiddleware( - $this->roleMiddleware, 'testRole' - ), 200); + $this->roleMiddleware, + 'testRole' + ), + 200 + ); } /** @test */ @@ -82,13 +94,19 @@ public function a_user_can_access_a_route_protected_by_this_role_middleware_if_h $this->assertEquals( $this->runMiddleware( - $this->roleMiddleware, 'testRole|testRole2' - ), 200); + $this->roleMiddleware, + 'testRole|testRole2' + ), + 200 + ); $this->assertEquals( $this->runMiddleware( - $this->roleMiddleware, ['testRole2', 'testRole'] - ), 200); + $this->roleMiddleware, + ['testRole2', 'testRole'] + ), + 200 + ); } /** @test */ @@ -100,8 +118,11 @@ public function a_user_cannot_access_a_route_protected_by_the_role_middleware_if $this->assertEquals( $this->runMiddleware( - $this->roleMiddleware, 'testRole2' - ), 403); + $this->roleMiddleware, + 'testRole2' + ), + 403 + ); } /** @test */ @@ -111,8 +132,11 @@ public function a_user_cannot_access_a_route_protected_by_role_middleware_if_hav $this->assertEquals( $this->runMiddleware( - $this->roleMiddleware, 'testRole|testRole2' - ), 403); + $this->roleMiddleware, + 'testRole|testRole2' + ), + 403 + ); } /** @test */ @@ -122,8 +146,11 @@ public function a_user_cannot_access_a_route_protected_by_role_middleware_if_rol $this->assertEquals( $this->runMiddleware( - $this->roleMiddleware, '' - ), 403); + $this->roleMiddleware, + '' + ), + 403 + ); } /** @test */ @@ -131,8 +158,11 @@ public function a_guest_cannot_access_a_route_protected_by_the_permission_middle { $this->assertEquals( $this->runMiddleware( - $this->permissionMiddleware, 'edit-articles' - ), 403); + $this->permissionMiddleware, + 'edit-articles' + ), + 403 + ); } /** @test */ @@ -150,13 +180,19 @@ public function a_user_cannot_access_a_route_protected_by_the_permission_middlew $this->assertEquals( $this->runMiddleware( - $this->permissionMiddleware, 'admin-permission2' - ), 200); + $this->permissionMiddleware, + 'admin-permission2' + ), + 200 + ); $this->assertEquals( $this->runMiddleware( - $this->permissionMiddleware, 'edit-articles2' - ), 403); + $this->permissionMiddleware, + 'edit-articles2' + ), + 403 + ); Auth::login($this->testUser); @@ -164,13 +200,19 @@ public function a_user_cannot_access_a_route_protected_by_the_permission_middlew $this->assertEquals( $this->runMiddleware( - $this->permissionMiddleware, 'edit-articles2' - ), 200); + $this->permissionMiddleware, + 'edit-articles2' + ), + 200 + ); $this->assertEquals( $this->runMiddleware( - $this->permissionMiddleware, 'admin-permission2' - ), 403); + $this->permissionMiddleware, + 'admin-permission2' + ), + 403 + ); } /** @test */ @@ -182,8 +224,11 @@ public function a_user_can_access_a_route_protected_by_permission_middleware_if_ $this->assertEquals( $this->runMiddleware( - $this->permissionMiddleware, 'edit-articles' - ), 200); + $this->permissionMiddleware, + 'edit-articles' + ), + 200 + ); } /** @test */ @@ -195,13 +240,19 @@ public function a_user_can_access_a_route_protected_by_this_permission_middlewar $this->assertEquals( $this->runMiddleware( - $this->permissionMiddleware, 'edit-news|edit-articles' - ), 200); + $this->permissionMiddleware, + 'edit-news|edit-articles' + ), + 200 + ); $this->assertEquals( $this->runMiddleware( - $this->permissionMiddleware, ['edit-news', 'edit-articles'] - ), 200); + $this->permissionMiddleware, + ['edit-news', 'edit-articles'] + ), + 200 + ); } /** @test */ @@ -213,8 +264,11 @@ public function a_user_cannot_access_a_route_protected_by_the_permission_middlew $this->assertEquals( $this->runMiddleware( - $this->permissionMiddleware, 'edit-news' - ), 403); + $this->permissionMiddleware, + 'edit-news' + ), + 403 + ); } /** @test */ @@ -224,8 +278,11 @@ public function a_user_cannot_access_a_route_protected_by_permission_middleware_ $this->assertEquals( $this->runMiddleware( - $this->permissionMiddleware, 'edit-articles|edit-news' - ), 403); + $this->permissionMiddleware, + 'edit-articles|edit-news' + ), + 403 + ); } /** @test */ @@ -371,8 +428,12 @@ public function user_can_not_access_role_with_guard_admin_while_using_default_gu $this->assertEquals( $this->runMiddleware( - $this->roleMiddleware, 'testRole', 'admin' - ), 403); + $this->roleMiddleware, + 'testRole', + 'admin' + ), + 403 + ); } /** @test */ @@ -384,8 +445,12 @@ public function user_can_access_role_with_guard_admin_while_using_default_guard( $this->assertEquals( $this->runMiddleware( - $this->roleMiddleware, 'testAdminRole', 'admin' - ), 200); + $this->roleMiddleware, + 'testAdminRole', + 'admin' + ), + 200 + ); } /** @test */ @@ -397,8 +462,12 @@ public function user_can_not_access_permission_with_guard_admin_while_using_defa $this->assertEquals( $this->runMiddleware( - $this->permissionMiddleware, 'edit-articles', 'admin' - ), 403); + $this->permissionMiddleware, + 'edit-articles', + 'admin' + ), + 403 + ); } /** @test */ @@ -410,8 +479,12 @@ public function user_can_access_permission_with_guard_admin_while_using_default_ $this->assertEquals( $this->runMiddleware( - $this->permissionMiddleware, 'admin-permission', 'admin' - ), 200); + $this->permissionMiddleware, + 'admin-permission', + 'admin' + ), + 200 + ); } /** @test */ @@ -424,8 +497,12 @@ public function user_can_not_access_permission_or_role_with_guard_admin_while_us $this->assertEquals( $this->runMiddleware( - $this->roleOrPermissionMiddleware, 'edit-articles|testRole', 'admin' - ), 403); + $this->roleOrPermissionMiddleware, + 'edit-articles|testRole', + 'admin' + ), + 403 + ); } /** @test */ @@ -438,8 +515,12 @@ public function user_can_access_permission_or_role_with_guard_admin_while_using_ $this->assertEquals( $this->runMiddleware( - $this->roleOrPermissionMiddleware, 'admin-permission|testAdminRole', 'admin' - ), 200); + $this->roleOrPermissionMiddleware, + 'admin-permission|testAdminRole', + 'admin' + ), + 200 + ); } protected function runMiddleware($middleware, $parameter, $guard = null) diff --git a/tests/RoleTest.php b/tests/RoleTest.php index 01703213c..89d51804a 100644 --- a/tests/RoleTest.php +++ b/tests/RoleTest.php @@ -3,11 +3,11 @@ namespace Spatie\Permission\Test; use Spatie\Permission\Contracts\Role; -use Spatie\Permission\Models\Permission; -use Spatie\Permission\Exceptions\RoleDoesNotExist; use Spatie\Permission\Exceptions\GuardDoesNotMatch; -use Spatie\Permission\Exceptions\RoleAlreadyExists; use Spatie\Permission\Exceptions\PermissionDoesNotExist; +use Spatie\Permission\Exceptions\RoleAlreadyExists; +use Spatie\Permission\Exceptions\RoleDoesNotExist; +use Spatie\Permission\Models\Permission; class RoleTest extends TestCase { diff --git a/tests/TestCase.php b/tests/TestCase.php index 3a19bf695..a789cc882 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,13 +2,13 @@ namespace Spatie\Permission\Test; +use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Cache; -use Spatie\Permission\Contracts\Role; use Illuminate\Support\Facades\Schema; -use Illuminate\Database\Schema\Blueprint; -use Spatie\Permission\PermissionRegistrar; -use Spatie\Permission\Contracts\Permission; use Orchestra\Testbench\TestCase as Orchestra; +use Spatie\Permission\Contracts\Permission; +use Spatie\Permission\Contracts\Role; +use Spatie\Permission\PermissionRegistrar; use Spatie\Permission\PermissionServiceProvider; abstract class TestCase extends Orchestra @@ -68,9 +68,9 @@ protected function getEnvironmentSetUp($app) { $app['config']->set('database.default', 'sqlite'); $app['config']->set('database.connections.sqlite', [ - 'driver' => 'sqlite', + 'driver' => 'sqlite', 'database' => ':memory:', - 'prefix' => '', + 'prefix' => '', ]); $app['config']->set('view.paths', [__DIR__.'/resources/views']); diff --git a/tests/User.php b/tests/User.php index e98ccb1fc..dc8e2c568 100644 --- a/tests/User.php +++ b/tests/User.php @@ -3,11 +3,11 @@ namespace Spatie\Permission\Test; use Illuminate\Auth\Authenticatable; -use Spatie\Permission\Traits\HasRoles; +use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; +use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Auth\Access\Authorizable; -use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; -use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; +use Spatie\Permission\Traits\HasRoles; class User extends Model implements AuthorizableContract, AuthenticatableContract { diff --git a/tests/WildcardHasPermissionsTest.php b/tests/WildcardHasPermissionsTest.php index 3cf12d6c6..5ae2e3c79 100644 --- a/tests/WildcardHasPermissionsTest.php +++ b/tests/WildcardHasPermissionsTest.php @@ -2,10 +2,10 @@ namespace Spatie\Permission\Test; -use Spatie\Permission\Models\Permission; use Spatie\Permission\Exceptions\PermissionDoesNotExist; use Spatie\Permission\Exceptions\WildcardPermissionInvalidArgument; use Spatie\Permission\Exceptions\WildcardPermissionNotProperlyFormatted; +use Spatie\Permission\Models\Permission; class WildcardHasPermissionsTest extends TestCase { diff --git a/tests/WildcardMiddlewareTest.php b/tests/WildcardMiddlewareTest.php index 8ee103fce..260223472 100644 --- a/tests/WildcardMiddlewareTest.php +++ b/tests/WildcardMiddlewareTest.php @@ -5,11 +5,11 @@ use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; -use Spatie\Permission\Models\Permission; -use Spatie\Permission\Middlewares\RoleMiddleware; use Spatie\Permission\Exceptions\UnauthorizedException; use Spatie\Permission\Middlewares\PermissionMiddleware; +use Spatie\Permission\Middlewares\RoleMiddleware; use Spatie\Permission\Middlewares\RoleOrPermissionMiddleware; +use Spatie\Permission\Models\Permission; class WildcardMiddlewareTest extends TestCase { @@ -35,8 +35,11 @@ public function a_guest_cannot_access_a_route_protected_by_the_permission_middle { $this->assertEquals( $this->runMiddleware( - $this->permissionMiddleware, 'articles.edit' - ), 403); + $this->permissionMiddleware, + 'articles.edit' + ), + 403 + ); } /** @test */ @@ -50,8 +53,11 @@ public function a_user_can_access_a_route_protected_by_permission_middleware_if_ $this->assertEquals( $this->runMiddleware( - $this->permissionMiddleware, 'articles.edit' - ), 200); + $this->permissionMiddleware, + 'articles.edit' + ), + 200 + ); } /** @test */ @@ -65,13 +71,19 @@ public function a_user_can_access_a_route_protected_by_this_permission_middlewar $this->assertEquals( $this->runMiddleware( - $this->permissionMiddleware, 'news.edit|articles.create.test' - ), 200); + $this->permissionMiddleware, + 'news.edit|articles.create.test' + ), + 200 + ); $this->assertEquals( $this->runMiddleware( - $this->permissionMiddleware, ['news.edit', 'articles.create.test'] - ), 200); + $this->permissionMiddleware, + ['news.edit', 'articles.create.test'] + ), + 200 + ); } /** @test */ @@ -85,8 +97,11 @@ public function a_user_cannot_access_a_route_protected_by_the_permission_middlew $this->assertEquals( $this->runMiddleware( - $this->permissionMiddleware, 'news.edit' - ), 403); + $this->permissionMiddleware, + 'news.edit' + ), + 403 + ); } /** @test */ @@ -96,8 +111,11 @@ public function a_user_cannot_access_a_route_protected_by_permission_middleware_ $this->assertEquals( $this->runMiddleware( - $this->permissionMiddleware, 'articles.edit|news.edit' - ), 403); + $this->permissionMiddleware, + 'articles.edit|news.edit' + ), + 403 + ); } /** @test */ From 85521b452cd556086c0685b4ac92f8bd4b70cd7f Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 2 Oct 2020 00:08:00 -0400 Subject: [PATCH 0344/1013] Update new-app.md --- docs/basic-usage/new-app.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index d65e71464..d17788371 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -115,7 +115,6 @@ class PermissionsDemoSeeder extends Seeder - re-migrate and seed the database: ```sh -composer dump-autoload php artisan migrate:fresh --seed --seeder=PermissionsDemoSeeder ``` From a48abce4bf521662ef83f89a50c71d3c77c6355c Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 6 Oct 2020 14:33:50 -0400 Subject: [PATCH 0345/1013] Update new-app.md --- docs/basic-usage/new-app.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index d17788371..b35d50928 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -9,7 +9,7 @@ If you want to just try out the features of this package you can get started wit The examples on this page are primarily added for assistance in creating a quick demo app for troubleshooting purposes, to post the repo on github for convenient sharing to collaborate or get support. -If you're new to Laravel or to any of the concepts mentioned here, you can learn more in the [Laravel documentation](https://laravel.com/docs/) and in the free videos at Laracasts such as in the [Laravel 6 from scratch series](https://laracasts.com/series/laravel-6-from-scratch/). +If you're new to Laravel or to any of the concepts mentioned here, you can learn more in the [Laravel documentation](https://laravel.com/docs/) and in the free videos at Laracasts such as with the [Laravel From Scratch series](https://laracasts.com/series/laravel-6-from-scratch/). ### Initial setup: From 8b8143ce8b2194e950cd7d5b64431de999c7d503 Mon Sep 17 00:00:00 2001 From: Michel LAURENT Date: Tue, 6 Oct 2020 23:30:49 +0200 Subject: [PATCH 0346/1013] Add link Add link to the example --- docs/best-practices/using-policies.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/best-practices/using-policies.md b/docs/best-practices/using-policies.md index 4d80c087f..607f90e81 100644 --- a/docs/best-practices/using-policies.md +++ b/docs/best-practices/using-policies.md @@ -9,4 +9,4 @@ Using Policies allows you to simplify things by abstracting your "control" rules Jeffrey Way explains the concept simply in the [Laravel 6 Authorization Filters](https://laracasts.com/series/laravel-6-from-scratch/episodes/51) and [policies](https://laracasts.com/series/laravel-6-from-scratch/episodes/63) videos and in other related lessons in that chapter. He also mentions how to set up a super-admin, both in a model policy and globally in your application. -You can find an example of implementing a model policy with this Laravel Permissions package in this demo app: https://github.com/drbyte/spatie-permissions-demo/blob/master/app/Policies/PostPolicy.php +You can find an example of implementing a model policy with this Laravel Permissions package in this demo app: [https://github.com/drbyte/spatie-permissions-demo/blob/master/app/Policies/PostPolicy.php](https://github.com/drbyte/spatie-permissions-demo/blob/master/app/Policies/PostPolicy.php) From 53475abe5464ac722b3794af14cc316f7a77805e Mon Sep 17 00:00:00 2001 From: Adriaan Marain Date: Wed, 7 Oct 2020 10:49:44 +0200 Subject: [PATCH 0347/1013] Update README with new "Support us" section --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 5006435e1..998e7ea15 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,7 @@ $user->can('edit articles'); ## Support us -Learn how to create a package like this one, by watching our premium video course: - -[![Laravel Package training](https://spatie.be/github/package-training.jpg)](https://laravelpackage.training) +[![Image](https://github-ads.s3.eu-central-1.amazonaws.com/laravel-permission.jpg)](https://spatie.be/github-ad-click/laravel-permission) We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). From bf22b741ad774e3ceff76957dd5c05ed08fa2c48 Mon Sep 17 00:00:00 2001 From: Adriaan Marain Date: Mon, 12 Oct 2020 10:59:46 +0200 Subject: [PATCH 0348/1013] Update README img tag --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 998e7ea15..ed92ad6f1 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ $user->can('edit articles'); ## Support us -[![Image](https://github-ads.s3.eu-central-1.amazonaws.com/laravel-permission.jpg)](https://spatie.be/github-ad-click/laravel-permission) +[](https://spatie.be/github-ad-click/laravel-permission) We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). From 85b8fb4bf48733d064b0a69fabf47f2177e4ca83 Mon Sep 17 00:00:00 2001 From: Michel LAURENT Date: Wed, 14 Oct 2020 11:23:41 +0200 Subject: [PATCH 0349/1013] Add link markup --- docs/installation-laravel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 616438c5f..4527c6a43 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -47,4 +47,4 @@ This package can be used with Laravel 5.8 or higher. You can view the default config file contents at: -https://github.com/spatie/laravel-permission/blob/master/config/permission.php +[https://github.com/spatie/laravel-permission/blob/master/config/permission.php](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) From 78e8f95eaaf0fe0d395cea4a6f6b2cc675a7a9bb Mon Sep 17 00:00:00 2001 From: Caneco Date: Thu, 22 Oct 2020 14:57:50 +0100 Subject: [PATCH 0350/1013] improvement: add brand new logo to the project (#1595) --- README.md | 4 +++ art/README.md | 58 ++++++++++++++++++++++++++++++++++++++++++++ art/logomark.png | Bin 0 -> 6866 bytes art/logomark.svg | 17 +++++++++++++ art/logomark@2x.png | Bin 0 -> 13033 bytes art/logomark@3x.png | Bin 0 -> 19518 bytes art/logomark@4x.png | Bin 0 -> 27409 bytes art/palette/100.png | Bin 0 -> 1142 bytes art/palette/200.png | Bin 0 -> 1140 bytes art/palette/300.png | Bin 0 -> 1139 bytes art/palette/400.png | Bin 0 -> 1140 bytes art/palette/500.png | Bin 0 -> 1141 bytes art/palette/600.png | Bin 0 -> 1141 bytes art/palette/700.png | Bin 0 -> 1140 bytes art/palette/800.png | Bin 0 -> 1134 bytes art/palette/900.png | Bin 0 -> 1148 bytes art/socialcard.png | Bin 0 -> 441805 bytes 17 files changed, 79 insertions(+) create mode 100644 art/README.md create mode 100644 art/logomark.png create mode 100644 art/logomark.svg create mode 100644 art/logomark@2x.png create mode 100644 art/logomark@3x.png create mode 100644 art/logomark@4x.png create mode 100644 art/palette/100.png create mode 100644 art/palette/200.png create mode 100644 art/palette/300.png create mode 100644 art/palette/400.png create mode 100644 art/palette/500.png create mode 100644 art/palette/600.png create mode 100644 art/palette/700.png create mode 100644 art/palette/800.png create mode 100644 art/palette/900.png create mode 100644 art/socialcard.png diff --git a/README.md b/README.md index ed92ad6f1..278e3e3a8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +

Social Card of Laravel Permission

+ # Associate users with permissions and roles ### Sponsor @@ -84,6 +86,8 @@ can be found [in this repo on GitHub](https://github.com/laracasts/laravel-5-rol Special thanks to [Alex Vanderbist](https://github.com/AlexVanderbist) who greatly helped with `v2`, and to [Chris Brown](https://github.com/drbyte) for his longtime support helping us maintain the package. +And a special thanks to [Caneco](https://twitter.com/caneco) for the logo ✨ + ## Alternatives - [Povilas Korop](https://twitter.com/@povilaskorop) did an excellent job listing the alternatives [in an article on Laravel News](https://laravel-news.com/two-best-roles-permissions-packages). In that same article, he compares laravel-permission to [Joseph Silber](https://github.com/JosephSilber)'s [Bouncer]((https://github.com/JosephSilber/bouncer)), which in our book is also an excellent package. diff --git a/art/README.md b/art/README.md new file mode 100644 index 000000000..bda495e53 --- /dev/null +++ b/art/README.md @@ -0,0 +1,58 @@ +

+ +

+ +# Laravel Permission Art + +The logo was inspired by the [Spatie](https://spatie.be) brand, and the well known minimal design of Laravel packages. + +## Fonts + +The logo is using the following fonts: + +- [Inter 500](https://fonts.google.com/specimen/Inter#500) +- [Inter 600](https://fonts.google.com/specimen/Inter#600) + +## Colors + +| |#hex |rgb() | +|--- |--- |--- | +|![100](/art/palette/100.png)|`#E8F1F4`|`rgb(232,241,244)`| +|![200](/art/palette/200.png)|`#C6DDE4`|`rgb(198,221,228)`| +|![300](/art/palette/300.png)|`#A3C8D4`|`rgb(163,200,212)`| +|![400](/art/palette/400.png)|`#5E9EB3`|`rgb(94,158,179)` | +|![500](/art/palette/500.png)|`#197593`|`rgb(25,117,147)` | +|![600](/art/palette/600.png)|`#176984`|`rgb(23,105,132)` | +|![700](/art/palette/700.png)|`#0F4658`|`rgb(15,70,88)` | +|![800](/art/palette/800.png)|`#0B3542`|`rgb(11,53,66)` | +|![900](/art/palette/900.png)|`#08232C`|`rgb(8,35,44)` | + +## Requirements + +- A screen or a printer + +## Installation + +- Open the file +- *Right-click* on the image +- Choose **"Save image as…"** option + +## Maintainers + +**Laravel Permission** logo is designed and maintained by [Caneco](https://twitter.com/caneco). + +## License + +All rights reserved, but with the following extra conditions: + +- It is **OK** to use the Laravel Permission logo in the following cases: + - In marketing materials for technical events, e.g. meetups, hackathons, conferences and workshops that are related to Laravel. + - In open source projects related to Laravel. + - In technical articles/videos/books/papers for educational purposes. + - To illustrate a commercial product. + +- It is **NOT OK** to use the Laravel Permission logo in the following cases without prior written consent from the copyright owners: + - Using the Laravel Permission logo in a commercial product for purposes other than illustrating its integration. + - Sell physical products that uses the Laravel Permission logo or its variants, e.g. t-shirts. + +By any means the owner reserves the right of final explanation for any use case not explicitly stated above. diff --git a/art/logomark.png b/art/logomark.png new file mode 100644 index 0000000000000000000000000000000000000000..c02bb87301cfad6c37fdd858dd83f5f5044b6bfc GIT binary patch literal 6866 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 literal 0 HcmV?d00001 diff --git a/art/logomark.svg b/art/logomark.svg new file mode 100644 index 000000000..8071f1f70 --- /dev/null +++ b/art/logomark.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/art/logomark@2x.png b/art/logomark@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1f0d440f69e4cf8d9c3decf62b1a9d99a426cc1d GIT binary patch literal 13033 zcmaKTXIPWn(yjsu0i^d{0*Q1`ij)AMNGOI5(nFC>=rvU7MTGzskkF-fLAq3>2~q^4 zNiPD@A=DG!Z}0t`A7`IjSMnrJW@gP=>zVS)-73ie|Ln9vz<4GwVl1An;h4l=5{U)M_V~ABXKPeEq5h5 z2S>F)PdmN9C;B#l&Nh#1x#S@nvi{OI0j_r52o8T&7dJ0ye>tvy+2|DpEs*0uXzegB_=z4Qaz?SyshypTSgHn@#@#{G9F zcWEV0JA^mVQy+kE@-V z_rG~<|Cd+lzw-V?gR47kWMw-~M_)TzRZpZV$3L@{cKq+Ui2ql4|Khd%@486*S6*S9 zGQxlN_W#=Jf8W9Z=}QYENA|im^&4n@%ue9 z(|{b_nZ$;!Uw%8?_$AJMEj$u%=jdh79n%}p-(!{p9GB3*;9G;3r`wVuNcOKmWGnci z@5LJ~j_86mc0|%Lie_I4l%6t2aA+?S*Szk}v28qjdbI-ybS?;SlP_n1rAuyHG>$)+ zEFJ_6km%EGW~c;WUO))H*YFvk#d78yGdyAka#0iczYCjVeCbCI>sTmdf$=Lkl2YT|YRnwmz^FNZmCA&)!nrjkXD_oNMpPlspacm%H*K zfx45lE~q!Rr%8NzDp-1~@0Q=rj3&K}ehwh`ZF4$p{wN~3NJMa~YiWUm9{4dZ4s!o{ zW&vFPmu#As{CKAhLCqD(v509ZZ*-r$7 zu$MPx%v&lu?-u^qeQ~e1H^aLmX(e8S+0j`;^IDq7ma}gjXyDj)?4bw_cVj+rFo<#& zj$5(d-PUDWqvU(3i#0sBmg>!6r$-V&*&#V~n&Qbi;KHg>8;hsISF+E4MYiigUH6IzoQk}Pfs!m!EXyrueW zTC0ekN2PVslMdMGH?QmAQxi2l)4AGCN4&fc80isM@bXr0lW@uH+q*goPHg2zK6$iDVG1F+l0bUpsO1f9t0l{`oVq23Mb3;D;D*{3rHh}U2N--UOd@wGhaAlo&|)G<}1Y!FfNk8Uas^=oo{aset%WA+=B z?764y`Ii2#d_AO*+|LD4ex&~ZBBE~#LTXR1D zG0OUYw<&o3t_PN|?;Ct<#&V>SenaCJBKmbcKdnKGl%VSmD<%jtOJ`sh-AHg(uB-5Aj75vl9IKf1ovn={?Z0VwE+qSftHq4!N& zL7UfI{~7r*IrffSYyEPdAgZJ7%J$V+H%L&{7-j9z_WkFpA8o00mg)oTs?0_8@8k=R z*UYUq$e$qr*)_)D1;2-<*!&Os6whxSJ52oEOqeV>`$+G&nDdJoQzsIn>A>zgF$|K} z0mW?Y2v*00tDLWebI=XkWQ&cR3eZ`~81#c|>P|i$O)PwXR~FXPf_6ZYV9ZpsdGHE4 zSKOyvf6jAWfqg;Y5C757P5$YyHdari<0g@R930=vnrH05T{{F zCMA0K?lBp~-=%luFLc#!0Q&u^{we+K**=d&X_MDO>8n3IAOCLa>`mZ?DHg0{=47b< zvSbJrRYWYjZS^c&T}b#w);Py}UEw7F4y}9?)1v7U`Z5~7FH6qp#l$xXtR<{m<;1&a zDamEKuRG&1vZkZI));HdEGiO&7QoCxkw^o!=l#c7DBbGEfJBrw55{HEjeoTI_$~Zk zrbIkc<~};^F;*`2;#JxyhsrUpc&2ePvUT?Zd8jfoM9K6<{V|KRR%FyUTHN?LmlIi* zJ;NsGbl3u_En=R2mwX%)rLJAV^WFV=sTwptTl`Fjg_=n&KB`v4E%F6hkNe0dTiQK7 zQ&e<6yNZN~I!BQ_n3q&H_uS+BZ1U)umJbyt13B$9i(`TBzWj4C?L(ZG^ zAsVX??`3K$KF3dnSOPgLLFpRxY&|L^WbXCU_9=&(4WkcP^+QSbhtEh*5&fXe{N1`H zNAMlpU4+pw^tMdSk>GZO_0%dbwFA(s3GGmiv_9f}|C)*P6vp=w)Z#*;p3oyc%e5&v zx)ii=rT>a>;Cu#tcI3jd1G0<0qD%I+alL1lPKWRbi0c#V`TK8LEH61;`!B|Aj>-6V4DYCtUn|r5lXZ_Ra;B{Of(&9MSeWKXFNBReLA!e zoCc}X)|t6|)Lox5{c8Bsh2Yxal$4^BP}lEbP|3*d5P#enuY)i%Rs^uYeQ;o2`L8DdYmdQ~nH z3)2`{K8@d7!T3K*olWg*sE?t5x`3viKjC~^_14^}dVsI(&-;b-u)3l{8IB{0s}T@Q zjKxsFS-i|=GF)G-;_R5SZGyO8dstB>X8K zpUVvIoS}D2!8u{c{s~`Tpl})w55+|5rP7fRNQRbXg)D248u9pfA7~3I{EFyVuW-j~ zH!W39S_dWm^R@7f3s>`)OzktrS2fV&rav|#tz+sBOIYR`f}Ce~sD@mE7@w&LjA;x2 zv(sYVM(3NhCTOct3t{iCYUyj7uNRQe?V&ooz5WnlzRoKL%ga9^dtfBg;i50pb^eoY z%5H&B3HUZXY)9(tQk^mwya$_&;tzV@4Fxca8ALG3RMBU7Ss^nYe%5-!+o3630-g5{ zFqqMD9Kl9npRS5P7DmmhTy}btUk0zD%P8*~XEphg6&f_6!u_pPJ{z-m`^4gZ|3ntX z_(Z8{jXF4LV)~`omxc%mvOjaIX-}XEfYmLinf@CIO%&HowcWVz0b-J4ukUi4z)a4M zsqh;nN+rU%d>`p(Q>D^*&q#>ccX}@_wP)*^hMwD&UoDN|lgCX~QIQ~Rm*y+&-gP?e zXhuVbqmQ30gWM!Nq%SU#%+RA77-xNOx%t)E)>Uqbj%zborAG2QfamL686P$>Dx~-8 z$!>hDeZ!o4SWDmC#NmcT;yAme)^YJ&&R=N5{pcw`fh3A5BMNUj?g3mc;I83ox_dff z?g_x)d!Gc}-^0+1c8VVY93rWdZir?B=OBC+d3Of~YDwJV)W6h{vS0|_QfPSoDJp;J zjas7eg`sNH=Mk50K~_sgdQ}hQU!T7a-FD@n()6(%A>pYVy0st20^`_rN&3A1n_O8% z+|VNPX&A|aL&{pxUsaaR4AFxxF3-Aow$%>-C4)qYCu#Of{2m#>w-zP*y5>oasU z*K}OR;wZL27`3}?LEeO!P5TtHA!ihtLiLN6N3xpQ;OMsuW$h@gVJ9x4o-DXaSH-9A zyZvJec?VKa+OFk`NWbKjN=E+F+skN$qVxPJek9CEUUGYmm3HFw_UyQser|6c+{wsrclxt4E|iOJj|N11Pfq@t zH|u-GVV1l1dvL`imGU<|rmlYLd^|06uF`JG?iU(cy6nDKjqDg)C7~f8OzB1Fw3YP3H~;Y2+!#vBhbm}D7EvJf0OFD$Cc zkD-mbwgRbNgyAZz_kU-l7xh>W zJkC)=)zpZW?rjQ1cEu)5m7 zp72vQt4ZwM&PVaeDvhn>bV@Z*ntt{o8H|gYOJ~8CcCA&JFcy>~mxqSD*wr}SBD$n$ zVrDPS&*alt0_&KRr;4SerX6`f+kCnEk5kJ`E`Zr4%pxT7CWfs7ch!{ILHAH zUKL)Z^lu_W(;EUHPc(E}W=P6T?58Ss_9s?k4!lxUE4^gt=`8hp{~&ycN!<<-I+Yna z^XcD6kcMz2SA^$0Y$vg|>3M3C-^Y;;dn4VAa`l*;Mru?ZNqcYFOBN?KfAR*@Hu0c(j6SmjNa3(!t7OC_U92 z2i`m<<)v~8w6-=y{I}1*oU>?i2S(r3s9cif{AmeAewyWR&-t;!XvfoTlAa`ZgeovC zKLGoCE);SbA7izEC1{D{3MhD$=U-n~wMt#TUqBi6Am7<}kNAw>@=Tf8_qP8bK))9> z@g#$Z>o=9ScogUM7C4_WF7(~&#wA0u+jt(EBzX0!~ulRhB5cl*hM) zb0Sf6N`AFR^d*3X63xRkesCxx{M}i;_S=)Ua0P+@uc@AW7?F#>mkVlsh~t}lyoPW( zsK+vzZ>-Z8Ve-idQhZ)bmctgP9%-JWb}2{16p1&n4{)P~h}OsZAHcvNj)(5d<0k6d zDJjr!T-bTDkPiPm2oh;QaYgynBvdVG61B)4VB<_#v$RQm z=k-0gSxPnbYZcu8vgamrr{_vU_Tp{$vG5y5j? z9bAHDAdJJ<<})Of=Tp1)+Mf-*|`sMkSveK$GG=?kL|VqntlWxKsO&@jsm7CxiKOkJxbuYD*jZxkMF7m zeg=zkdC zi@EYNtjKsX-VNd;mfv9Y{YS(iqap+Hy{LXs6g*oOQXD!}=}63hL)`Qk`Rd!4?MNIB zOo3nq{(jUF?yzvsH{w&ezJ{nHYmSEkda7VF@mV;tN~MOiVJPWe=u`UMb{Td%My37Y zOWh5D&OF^RJIkkp1;A4_?SWC|WYGQH`Xz3fA`ILcLBGt*8Z|0+B0_GKYg)VZ7ZreH zntN2MRRi6i-nuGgOqd|%$ZJYCe#|Ur9|pc}38G=e5a6^P&59H^G@s(x0~ii~xB<2mjaz$KkA;#J1C!a>ms%9}_zSa4f&w1xivYp@`ef5jRBP$^D{c z_=RW}pl>{c73pQm|HUmWKwYmbmm6b6er6jMa}}Yz*?a(V$oximS(t%T)`bG;pqum< z((@|IWiMw|EKs?ZaI;g0#_Q=p5>nY(m}=0{bbJqRlm)+wTgHwDQU|a%DFqp@U+eqt8A~Cq&CRaVP?_pU}bOsaFKy5jt=({ zsyKI6XuA~I=i3e8B=jc$m$hait%pHUK-AKFd7FRvT782cN#=VN#f5SjyTMwST`VlS5q2i+WZg}+?Ls< z&yIIHUX8x$x9E?7LwBL!snWFpyh=OaIkIn5wO3if zK8a*ehA>tk+BsNiDIi)Q=T$Hm0uk`@14;_p*$><~mp1!R@gJbsTc? z&lZ*-Iu#xkAix|LL>;pAooYTG7h-8(v~bkHDjLCdLZbP>q#fr#y*{@=0XX%k0O+PN zN7Yvr`!IOSmDtSBNL=X*>cyV6ohFVO5OzO#e82m{{5}9l-nwmAS-Cxj1=cz12ozG- z&i~`a#Ywe%*3q6_AsYLzK192r@AhX9;%7T!43|mt-{`E;S{k$JCLVV5%xGglo|xG3T05u|LurUwm6K*oEmOE(k?!v+P8Fi~WlS z-6ud)xhu7A+A{SK1@YOzUr-Ux52nE*L7H!)xc2`#ajN7!RsAb!$KW$thNg*i;V{u{ zT+|s4BxHwtSEaxi{CZTTm+u;NSuz~eG;!)=!qwLKS_v9Fl{T1AARcHB`MV+%arHMk zm1p_Ubye(`Fd0mCU%DM+F*lbYQk4o*#UHxNoa`S+_4`HO!~uY2w6GpA`HS>oqTxmJ zVH`p^=cP98_{F0fk+_^*cwDziUA2P#**tL?1_uuFNilF^BFnJOk=9FSE8F~ioYms# zI*Z04UVjA%%@br_9al$JOBvf%-(t-CUgb zJ^dhkLkUsRhYh67ovbn+RVQM*d(jN%Y_w+t{kYQ;Ippcng9`a(8;_*+P=zqox*IhcXazB( zBUKC7Rf8W-R^4`0A#ZheIF?O}Jyy`k-$j>Rz5g2kQku=*tt+RmqC>n(E1jn*mtL-Z z^pVKt*QY;0d8&why%X?2Z>s08Z=>z`-M^_u90B8cB{Ti^J=o=fW>A4UXCpE0pdeK8 z@DHQbW5nJf`h5(UDT6IHl_3yH&n7TER?ZGH6Xm?9E^J2@=U&8)|+Q~y4pF2le}ej6f(K^aGoIg6CmaL(<~y*``%c=lyl z5J^urs6&k?FUGCv;a63lV=~9#ijcA#=9k1G^+J`iCj#(8a)jdYIXOnQkYUBc#3FrJ zQXazCy#MAoE*0j2XAop*)K^UgM!kKl zU;%bR)vQkeTB{|v7WG>0R3@tCd@6_v>H>WvzbGj1(mOnG-)oWaUPcRlu=fkqe&UBc zj9Yre8gwejwNDxMTzj@l2-mBbZAY{p4)9ZkZn!iAxzM3f=9W5&4qfp))j4W&jldnC zpqC59lKHB@SL_0I+%#7A`Ypf{KBblEaR2>7)WK1=CTA%j6LN{dcSsjY_4&seZ?n9c zYt3b;7Ob+z*Qun(iZ(q-YGNb@k$4pdqxUF0@%=d0se;dD`!PaS=1_2`s-5`ZE!OkB zr{jaY+R<>|g+Q6aZ6;@BYi=sdFB=Z4S|vEUU80_JJ3c{KKOqsWEbrJXe%hfb-b%cU zU8#LfyZNI<{(~pN84qEKwC(^YO5t6&wQC4I^4k@l1!46$=PhI1u@t7y+;&f((;5EBR|A_5Ay3jpGW#e)@EU?^` z6=U|!G>qRXq33P3IGRcNppP$Y0gj|xBRir>dwF>O3odlnee|7o*W;#IJpE`>K==;6 z#d;#sHWF>m`^+B>E_8a2U6JRzmygEdv>9v1$5wzg@k_$sC+|^0V@9QvuofzkR)b^2 z>kry2T@@h*ed9}Fvf>bK2s!K9tY|h=uFG{Rwu;|21g6;EKA|PJ#p1gbo4`Jh@okL$ch^<1{a$J#UIeuy_K~-a&1G~v2Pn1n z=)`n3AkvJ4xAmv~_1kpOl3&y{S#qG=$T>+2&W{wCeX1%YgM_mk#*MqKZjYOFvlvS~ zyN`dz?kv)Ub;j(Bp+=rQ^y1O^jb86au>fYod3(cSDs-KMgTYapOOzDfhopHnDgx)* zl7_U1i+OPssTPqf^7m2Jzo_k&F=$-C*&DIm>h*M1o1{`@AWl5OGer9%RrE<)VNRf#H^-2#p0Qlu zhRF6*DACV#Ow{UrLgN69tvq4oL^nT)rE58nts`ML$JPq+;YIB<{PIxoe zw1MEh`P#495fQx&^;&}7Y;8;{iTA&5b2rfX!tZ}&=X}O~|Mm6UKy8A%TOg$0umCRg z6XRXh_`Gwrn(mv1^s_KwU2M$J1Ln&%k)RMow$YDzV!T%Pd(wQC3T;>2^BGcwQ3k~R z!}~J7J|9}*m3NBFyRN|=?vT9|v9`kIGx!zAPWEu{8Zf~@}R_~}f2be@S*eiwjG_zE62q-ocn zXGX@`B^e!E2~ z3Aacxl~1`^&606_hwD60L*;6a@C$o9b0TXC2w|xZs#Dy0-NWvz4(2r#Iqdte(ax^1 z*68Ub?^uv0yw7&%QSE7fjQ;d|vVD73mUe$)2&714BRPlHTp^FA@%r26QCe~G(>3h} zu=8BnZw+u%%F8n!R4zf0)bi3G6qw>NmnRD=pLe`b+mDY$;$F8YcjJYJ+^OK&jDzN|_ z%Kj=`Hg)t==@wky{}JCd0h7Sa^G4JT+Fjm?JF&I zw~Tt&Wd>2K5RmX|-C-`2FVhHN`IQ$eec|+U(&C&spKL`&|2nc?)G#kX@;folBVE&+ zNEyG5dGv5pbzVc~Xw-vhZD~`oIJNj`mYgN)d%}XL*{>!R<@~u)IqZRVx_%QCe43lU z@_dq#VVB3`{o)7y`63_+z3&f8(oQj;-{@K>5V!2QHMK0btMP3mO8QJe5`tw)dtE~M znpb#)8p1f;$b2$?A}ghqD5hR!-9_}g8`v`@KDfFv6%tW!mLDWP!A;1<+3O!@&6I99 z*#r2gopGrsh{aX({{2YiR#>D6kwXw*bj79NmjGo;0a-5scCGHs=b zcXE*RKOIP+!&()6X4T0`Codm66q++sm}xJq75P|2`qGRc2K41aPHVjoOYKu|5p}!@ zu}qq^Vg1QT_eq7jBVys&yj)Y3abs}o9en%)`RLLOtj0{()@Frm9|lY{4^jgQhb7tU z5Uu5nI8zfooaM{Xr_>D^25lW095B$^>w`b$kk5!E#r7x^TSEdOJ1a2yoNYW95eFfh zKSERg_+jCxbpmdk(^$RAtlaRqfC;2Z9Z4T`UgS+1)R1EtN#R^4ub%x$3Rv@;JV9SF zO)Lf5g;2KnT&uIFmzO7uZGIhI9+8`)iY(SlaztplB)E{aFVBZq)B8r9NOmQ_$Mb0C z6)_ens}KK>2M&OAes644H~^PB=;z379=pZy)-y9CGklxw25D1biatn~zcc836J}re zP4(^l6MBmBiU1ze=v9%+roriZ*>f^Gn~)ulFXFXZiBi!kc$?a)o5|&^ZtC&yWVirB z9w|l{)NTXXZab!73EUsZL$o8Dzubx?*!jjNu4)+p{~;-{h!>?pj$wWo-CDtiU??10 zvsw5Gy2s}te}4}~(jm*ox!|Ac%u?+jkG(56uQ78;8hozB zC#f$)qdC~616IiS@h!vc1rv)=A+H+P9ue$O@rCPly>rQKCN{`sG9)*;(c<0H$l?>K zpg&@M&yfN`^`BRouv?K-1es6mU!3v zHv=%;oS{D1rcK?^m%1V3$c6mv!tXVS+D+o`o@>`9g2KN4Mn=BZ|MBcYPVgpaEBsg) z81-(*R^ew5w~<-x4(+Fh4MbkpFRyQ7?o9%CUX^jI&wsM{!{)UpYXH6$JwMvLc*62t zVBT-G>ERDt6Bi0laHH8_(I0g!urp{TmduO59m`J^W9FM~vWzY*D2AO*r6KMqW1XFR z<-aAbT}#qkS)~XFi^>}4tu~h!jkB2Ga}!Onu>8Nt@WHuPh*ftT_CJfWD^B?JPxQ-)W~R1+U<%b$`G z`4BaGL*E9SaUO%WCehjeorJbUBp5EC0iA&qSV=Rw&GlOZ3Ip|PZ=*35xzDnjjNxDC&73dMm4eU zN6eLaUjSSHs~UAf1CBc4m$GtT$3<^LJKOaqQ$d$xpLToXZw0xv-Roq{6MxnaIhasv z&KXdgn99_8dh;!*>uq17W(i$Xi-?Kc;G1Ryo684h`2gO=_0xsNOYZN}zgAxnOx0Cc zJr?BLJ0@>dfNz>W;#6`aE=dEZdqDE!9!BTC8<`!MPE5%$K=FhvMbtJhk+qRFJ#TRG z>6Ebr-yaw$qe@1V20$@NM@N; zfvb}ru1|{1-af4w5EU%j=XGl7yHEoYkB@T%P5km#hpcrtJ#kClM6Iy z6Vt^+8%FjZIc00cABERYt&BXAR{OV!`k#$W(xBfDV4{MuN(t=wr{ua|asIa%LuY8W z*e_F+j~fEx;3l3W0|KdAKjDLw(=qJ2^hcJ-+7JSd=XqPLjqQ~?pePgmHoiQMlCF`&NkihX2P$&VRNqkA$r zyhS;6@#j;E~6>;nz#tN7v0O zqJ>V3CnWEDxkR^Xa7$-!OW$D1ZiO0@qxpSC1_u7~{EbcsWC+X)DHaqYeXlx?D@h73 z;o|R{%(Tj<_jlxvtJYkrv( zwz_{f!dqF>*py4#=aiaklGv1qbm96PS<%Ax5JV$ZQv7Rymq)UichJi~eC*eFhV81GH3wA6pgak3Un5XmfeO9b) zx6@td#2l)mHIpRgO-2Q!`ezng)%gH;bvkpjNxm!NVQbJH>6TVg=gF_pUOC_$fnkBoLc`bz} zI#VDY9iiPuwm8EomL0 zsNRIWY*(z*j65hgqLZUnw?gc(LBHDp%%eKDEhj!$W~;1gmr4yHKw+t2Gzl;w4#;&3F^ zMQ9{|KC4!Wo18sNqaS-`G}c78njEEKWA*rVj^&NecVZ`&6H^n87Mh2ClS*LPCmQ1c z!RUUt-lIDuw@Iw3*~h5tq9QDyx5;zd2|v5E6^ZHA?UE%{PBa|oZHx=kxY~U&3#xtJ zNOn=K$>BTS4F@|6`sZAqu4EP3bL=+d)|j`eE+n4fx|>*LJf<9X_V|Wr)Lg#kY~PGO zN-)>?fTT|xZ_Z550MNrhoiN{t;whl#b*l6#z`M_#TGA$hXvsPM$b85`dhPA83y z59gwSl&7foG``)$X6X1J(1B8w-3Cbe#y~y02`syGquy}km zG|GRe*=V$ZJR1SY3fTvo?Z=&;Dx^#_OFo*^B7}rnM49^>0F3uwa00P>)M!9)>Kgf& zq;T%cfusIYR!gKVuJ6W-DdU-zk)}UGNT(9AV!Kh>O5Grb4v-x6SBgW}`msYMwsU5) zw`lttYPhrF044VFyA-Lc%IXKSm&YQtjfwFZHG>mmvr(ZAF`2y=7lK(|+9u1&whS~9 z%WJLGu98G~zU^;z*o>5ZW)fOoQzt@ZJ!K$=Inv@V;Vz1YZ;>=hzpXqSZ2)avUqJd6s3O0HcvX0$6}x)DB!;jJNbBP-v;=Er~T>; z?iZc;Q4NnUxIPO$|0~2YHNJVK2HTZ;CZt8?UG=Q!c5d$V+9c;L*_QQA)SO0O{C7i6KaAAks3BZb4c}0jW^}qjZ2YA}u8j zDG8B|5%YQD@ALWoowev_mflvHjBt8fJuz71)d7C1fynXGw;6!SU2zxk>uDjh6xCz|O(ciNlu1G|5h2O=@ z%G=5SBJY537qhz@Bj)Fh1ZEQvDXRD(?HpX;-aPj3CoUdJyxXl$FiaJP2-{q0JCnhN?Eg>Z0pXaTEA6#B(nPKyXfc zmybfqYk0x!yb)ez2!z{zZ}EXM!W;3-8G+;hONvSI2pZTqxOiNCa_8R^0|R+ok7wR? z9u9C_O(kAn5iu7RM|rR$SoW^8w6uh}ww9!%mbyAvPF77@O->E0rL7^WDe>RAnh1yI z?r;z9|IT&%zjI~&*SVM4;En{ItO@sW@qs&Pdm-F;{+(Lh<$vD`_x$EQccaCFiZOtE* z+ZL;X@zL?|E7yPhO$z) zQDp%aYZ2Rskfxr{AjZYxy5+yDNcW+7&cE2IV$PqmD$0?=QJ%B!&hLdzRQ=Hs2vnrh z5lHZQXg!5k;!zm!6uJ5Blx{ae479-BEBo?w!qV-}Cjrb)I>{}OCBnN>mDJcIco?hc zABDrOPFyh3$GS$m={^s)bG7O)vmGpyM!}Pbj4y>p=68M!fqa%>!{Kwcm9@34ZML=GxDMMO|6k z&j+osTa1lYbpwU`KSiscMD*+VRS0VeZ~UEOLW^5c+#gP{Oa+57V(#p!O?`3<)lR)# z@K6@XrU}(UlI^W@zut*nGIV>tGi-W(FFFrlxSV+o&bPA8sgQFJ!7GV%@MCWXWfRySp%lSMCsj`6!2~{Jp81r-9TOp8k6K+S?aMyz`()r+w^|&Pd32o65i}osTBZ z!-T^i%vFJNjEPH+j(1*@|7}qUo!GkDPdo^g*dK&+o}vsvxY zkA}8mI9kPzk`FG>AxNRkm-_cX@&_2R&Kc?^nq#L5Y`3`Ju%XSSLmioI((USd6X|Q$ zIkT}V1(U=g50PZ4n^9$0e!91@YlvM1r$=F1*3-G6Oy4>BTZqWP{hIDOC0!y#TaRv` z^dEu78r=fxCr=mTz@PYChI+{x_o>{vc&ss5?&>#0(Fe1=k`jL}{~1+i2E9D-Ju7BIZ(#>R6+5x@S~+ zi6cLtKrwyaK0+plEWGB;a@(yPdAo4~A2`BGu+MKyopB%+t3UhtkscC#y|>(lW90bq z1GmwDousW&y~=*m-dA92vj6ws$pIOigj-(c5l^Eej#i!3j{g?J)#WMa76`L=@$GpY zKU^`Q>xj00pL4WK_f-r#fz>4;=M>7?sDr8}rKGl5 z7ot6ghbXD92ks~5?kB76fJHVif}AqJ8|KS<>9wh?Q-Gi{?7bPZnhK z1ts!xZpZswJZgys6*o-Z&_$d^YH9EyO}H4_&E%a&A(?MW>M52(i;5$HUO}$BS>;Q5 z^bu>$jEeJW+zDCQeAlxedUu!d6T#K(FU8#bT2ZqhT={6{zR3x+xAx-=9Dy!~z`8t4 zca@OdAf2~(3iW7J^Fs~BK$y4Og6)E73H>Wedbin#m0y}(uqZ#GEhiOOAqS_$cwC_0 z>`I?`1mPdv9bK=#T2O*b6Jq*^U4h%HU5L(@KcyhxUuwe7WgCYV=D0h5sq~iRmZnGX zD?E-~s7qeu+rb#xpV_*JcOQj_JD7q{73?m`WJefHj&SS@MEKM^=q&E!{K*MOJks|Q zj4S?2{&Ih>9RPtg*Qr zLY5`iT2ibDm0S2*-6I#wkTQ_`Dc&GvEQtA9hPZ>qG3L7_^k7FmEA~Cj3!^bVt#7{e zfkt~RQ`aoy6WIF7#6Je#&Yi+Fvok83pKkBbi<95{D(dmrZU4jMv9BrRk>$!z!_@wk| zT1p&pdnvH?LcFgzPE+uGojGa~y0a3sTMejW0hRDiu!%T%JhNxNnAAX~23y13BTOAE z5724@l<;S&uinb=qUftB&yH#Po6oN~i`!=A-A)a=n!2EecMe=O*u5!0{x?Jldt34t z%*h~xgr~>6!a_)Cvvf!@de}LHDA=3gjY>SS`lJm_oV;_LUKPe41tzqc6J-hdz1a_Y zl74=r%Rj!_@T-4RyZyu!UEnYdix)RPwNdUqj5?cjcvwP3BH5HW$n$V((wC{B1I~=@ zI~N^#owJMxHAM|RttWev+~ROrYr)hpnTH_&nLu7zsg=; z5Tu4eM0?g>FU#-xCqI?0Nk&w@lt}##R0RovHH_;7O6OR3jA_I2TYWtWCwoivme$ug zuEu#!`0&0G=Um=WT-5Hqh-+DZ+XOBd8Au_6LD=pJ4I#6gsp( zX?=~>_sEh;qn^9rC?RFc^8E#$GS8ZL-O1OGn!l`ZaZj=iaY9aP9CPq&j1$S4HQSrR zN%PWbkz=&TA9B=eF zIX}dFYH)w}O^*G|nH^_Fhr$haY4(wJ|8MwbVOrHwNO{6>z~D2^Irgti*u=c<$uZoG z6lZ!4mc1!4cy+&~rHM#bKW=ni2-VX=JQ=jgFtF5N>5k-a^H-WC+@k#Zn?Undqfza} ziz^nj-NTzTEsey&L%(FsFI-(~I@)csjW!~>Giv*Z#0Y+pji`i#Uv$sj+0IazKj8I$ z7ICyxA-A}X>clq21jp22+DKqzLJl8TYM6a*)Cs0oTfdE3!QZpWeG^fPw+>RG7ry%J zTB%?C#AcK;laH2QJeQqp5?|sc&aj43s*WB$Rnk4LoX^4@`iC-Zl3(sl+>j#55Ff8B zYyzDZ&f~XhKHi*DXnOfsmM^hpOnfisRV4RI!6?DIMXzGQoh#rN*6R3#T8Q_ZiWuUA#dPe97CBAW!J>;Xt zTx)dC%CE5vb`h^=bk*n1r%F%bPHe*@yUCb?vYcAM$tr%T2ak9RXU^`qVXwNsv-j0g zZ}jgt0|lKLME7cs?_zWo3QwWdj)j1!^O*DXts-{&n;_84?C9C^gWDp8Mz=DZb|1T{ zx^Rc%hUMRh8X(D194%)&nHL3Gecj@0M%2MvANK^^xPHoD>!_~^pEq-9IclZEeB8s3 zY*b59T&I6U5a`191Ucp##VA&8B3%%le`ORuV&mu6ukV=r0G6F4q{o3}VRb_W@$QZO zZr@GIe{1~QE*{Y2ZE8$>cMzuQf0JxWPUGZOgX`JjiNrti4fpOG53HtJ+BFt?_FI1g z50Tw*;0xTz-KST7>v$31#$-V6$`h^I)3C(VP?YRQc(RM3f!nv`HlB-i9%_u^7VA8M zy>1prw@{U?;H^tf`JM-6hv<*vs7Wbamj`*D)&96Jn0tB!={)?kR7T}zu9a~6ssyj= zfUYY%JHfG#9o9UcNyWkJI-nB3(v=p4VxQaIrQ969yCh|1$upx}yNH+pJj9iZ6%U4l zgy-R7cfTaUBCp)>pVCy@lB4uZL5*S#?rii$!q@Hg#v&*!pclE)1>?gbDVhVT;88S|e>1Rn31Mv5 zQ_#Ww9f>rWXquFX8DW0|4X?!)uT>Rspenw*C^s}-(f4J(Zh#tufmW9gyL~Qudl98W z2}vZb5-g#L^`(T!L`xl=XI?9ab)T@2w3xyrmN4=7w#dS@O~doYPX7Earvy7)n?G52 zqT+H_O_`Wv1hObq+n#Ur;1)S$d&P63YOnr!>=}^K2cj2Ust_GdjcFlR47aE;bQGs5 zdy-BceDY8UFqrUs?&ovsDVo#jOmgUb2_2bxi2k+Il>2VAO9*szReM*lfZchge`dFi zp3RdUjlmrv17CV`u;d%;ir7$ZHdgD%tuejfNw2IM#m{ei6n0K7WRXu;A3J*7Dt8-2 z5G)cr-lost>=S4+Th5u)H(=hrN&Mu$SY=7*MJYxYb&=MyrbaDlA^zD(S5d-SV_*Lw zv~7(PdqV0URiw=Z1K5&^+?N0V~Jl&c4-VokTV}X z9W(&>+&uf%ss=EHG0@^e5Z(3+n@ldRH_OW6O4&UtofbW_NnE+HWo;9|0t0d6*i2sL zQ2J|?$~Z2Y6GG2rL)XF5w{%I~&&sXSJs2iF#9J2M^+a;NZF71wp?0`;pcv27?k9d$ zj)%%9Ef)7{7BYVAz%3r;M{+B0Xqj@jK9{_uSig6*rlru`lQmSfi4q<(kg}?g5yMEi zICmCh-|yEu>bkJ;g#?!Q)*U-&<%Oz9XB{7{R?SywlfSa*qWt0AXUcN%`#)0KY8s*} zhDj>TkZE1G4Yll~7>L_LoXw5SgsI;V95o5U;vHI~QxlzDF6W|lM#l!%1-ow`w-9jM zeHUrj$BT0XR@qtKO53*!CdZwHyK&Txqx!clL@%5Nr*Yua81%2zhCmS<7eSTyogXau*FOJ3EP-#B&`nlOA)Yr1b<`yp5RIB(JS)B1 z3dNc;RX;#D+P}wk|CHW4vA&^n%pzH;SRbp0US>$~c46q#e25x+k9Y3tTILJ(-JM9j zf?OO?_MBWRtc1X7l;^*ppBK?wdyR+jvqr_lX!?uGYnVR7oH!gGxg-B3 z+%J6FEoCWCs3!OJ3>qt)$6jPRRQitfeeaZ6R-dVJ)!Pfkcx6J`-GidH-??m7LRe)q zZtY@7^vZbhR$sSbySb-+bG@u?Jv=QH?s~l><#8dZm2G6;?Kpa0l(lyXyfZ}eD6z2I zAW!dC!EkG? zZLj8lw)n1$)nH=yw;vyP^8S8*uQEzkq3Tmo{sYwFV}s!*Nie6>iMti73bP=3;OAIt zfP9sBmCIeGxNv!48%A`cTd@o$o+*4GVHh(pX2h_9?^LxxCEWRX-ncIce+rs0T-;8s zH0r&tG52(*J?7m3_gi^3&ywXc=u*7I$`0dAX0dh_M}=mkdcB{ICCQq2SYYdUF-AI) zjaI@;AxD`-&CBA?$EjfG2aBFo-!nj$4lk+sZ^ zi*1#PIP|KHF&^EohE?g&)Ech)efHkS=2tObWIxeQO^|E{9H9O-pqG)5qCVLk%FR+y zCKss)F!yW&kQXwybJi92c#66zeJ+{?`7(&JU-*i%*F@<7*KIH?mHf0>f!d!ors3&MU zOQA2}DB)Ch!3+W`DVNO!u_#FKPV5GPrIjNbaM}$V;axa;>khaelg7+sB#6o20^P1r z(K%utPbtQ83N2zX!*Y-wKtMiG*)#)7i0Aha3p6nF>+lU8Yfks9Gbophc@!()!$c^u z`e+;rFFY7ljxniE^`~&WUzmIz$+X{qIe|wtD#rF+xoAR3QG@Q;^qZ~H+zwfS-|0Qz z_L-6LfeVFPuYh%q>oY-3-mPjxCmswwj)hLyNl29@VoSkh%{w((uVdofRQ;UIscZ`8 zE6K!m=6Qgt$|aE#f8miCiDM*i)o97A2PIAhoUke>f!Lpb*c`hQzfOAK(nrdyu zexA&Q984ObPoUabbiW6?YW5dyp`%4ngBlg$1uI{0Bv2P+c!f_kDm$s_xJZDB<*;V9 znmK#a!s2pB_J}`#$YI((Nz7RSGW7fZi;jn(4Ec}cQ*(=cW{cdo>goHHJxu9%y*txY zu;g#*u`F`Gy1p(z^HuWGcuCyxP#Qp(^cSM;$lBioi_Dgp4}F>?$zy8;<`11EiKEL` z5!W2;o5zAw=Wt+(j87d51E&q_{hDjaev0*{95!V<8?{)nY(lz(*t(}2<*F*Ejs~2} zp4OoEgwr`zy4LRq1x412KlV9C%NE}R41?Wf*mZPzYHhBs8GD1tDST972uD`1_(ZDa4wKg6EK7-DOF`CFn&+TK>S#IUv zp><5%8WrawPZfws7g7c^Ddu^oR}t%)*Sq!ilU3&n0HvkU-Ng_=EXOn|>h@~3NFRg^ z`i6y=6yX_eiVFZ*-@G@I5(5ZLZp40!sl66!zAT*wE6>Re9n=KPl3X_jMe>GC@HYXM zL~O&8_TYi^Kc3L9sZDZy9#^OhEpTIyiu5d`KQ{^Y_88{YZVY}Pk_uXD}-$59VjF$4uTn_stiWqV@a zJ!Lh~U}Xt9mCg$q4?1WQByLLWtYfhn`|Y;M01LgugR5g#v21wgi`>;K>;#Kk7v)oY z#oT&*-dRatb@$Vz zM+>Rzvo}Au5bZ1DAYGoKEtGdruFvt{fE64J0%`Jv0&}>t zW|k#*^`&|m+r|$ZAaGE9Ca=xZ>lo#s_dwbM!U2J*~ zK|eA^k-?HREsUx#aZ&z42?J2HM!`r&Wfq9?x3~dVVu@t^z|ZtTE?V6}A?^Kq^Xr&f z@Lp&}%$r_Jlm)zizpSF(f2c$g`dDD7Hxlyu0Q22K0p7h$ehZzW_4=q~fm&^WK7^_% zgs$q+s+J2#U>0zDC`0N)VAI^YzHwQ0!?;3X;X@w2Qz%XSCFX9Z3%gSuN+Z`#pjEAE zloz7K5XXxtTJ`>TzyxQ#Z<+RK|I-cg0{$A;EG^WammFmQw!7{O3J}V>I{UZC)gu?c z#~AgoSP4(u)n`Ce#pZ%Yl!)g(be3RotyRtJ_E6M4vW#Xd%SL5%GQ{lk(tCxoZnhLk zxg(o5$c+!Ax&7ODlG7Sn)z%JhDWQXfSeB>N(fnC!d}!qmiwzbfpa^rQSgn(7lvSRi zo(q=TSQoQqoX6RJYX<;E$yrkG1%E7rv=7Y@ZnUq9dklHyn-K>IA7boZH0FDYhnAbp zX;hfxrqjTx9C<%jZ`J3!DD&)Kyy1l&a+J*utA?P+t!riXn!a4#N4dIw_F-NJZqG&8 z!?7?pn_lOtmGKqMc@Kv5l2&!l>>&VtLR6ya&5RWU*6CeLP&U2jkiZN1MS~_9LVB0? zlZ7$dUb01=sy_I8Dh%}W6xui*l;1VaW7Puj9;M}-M?L$6h=_$d9%D33DJ~t6MYjE8 z_67UI3$&i40@?ATVFbfhUM4C)x2&UIWc2}7dwYyA>2-!N2%H5G0n%sY6*(ljpjp7) zHq=YO$?H|&Q5KI>ZWx^fHQ7 za4`MuE=CYsTkfLF`RI2eN4Q6b#y-X|=|TPY^L99h>B=Q{6aJXnq;LsyUPqXC^0a&a z-B!ZOE7A;5ao=4gG%CO&8;Z zAk7``TTE}Z1p`{f+kv_x07pzVh1vR*6oho6eaVcNLckIbPOKN`Kh5fTL`8d;Oc^9V z428#1eig0vX#!BM19H;YPp%5%12~+jaSfj+9txmUaX}hL0Nkg^gsKp7Q7)t01j2L@ z*+jrW*Sa}m^`(xGTwZ|wH+`F4+*opqxxxM&dpXLIt77_K$rn84R{>W<0G`8alVUq2 zPhfra2QdmSG}>Rc^)*R#GzLZ1-C0`z$20<~=xJO%i0wZ7f)g^#rcad~y00*+XH8rV zG0Vufg@=}0da?46!eZad{;u*8{6RSfsp!ysPItC2eGO1#5slmXOE8xyD1M#MLp@1b z_|`?)JIi?$QT9&M0qJrAEm&x=Qv+>Kn*Y=Gw2Tryrizr+D``6W*B}No9*_(4t7r~^ z%Qc=C6#qu*yo!l;9I;7{xmSdj5CoXeSo*LK6PdA(;CQIv1t2Ts@bp|r{ZE98Z&=M1 zLaq5iG)?6WODs+08ML6Sug&2fo61i^D~j&}_eQLa7!Rt~vAfMQ&R6(=mGRf=B`yCk z&KnvBFk5->C=k27j#+O{?(2#Ngk9V06wR@yhu5i?9T`Y{;JN^4Xx`iLpxdOT-;KW9 zX3}_&>1utsi{Oa#V0E-B3aBRoSf}RQQyQN?s0oA`6edG!2 YUezx2XcP6g5#Qb} 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 literal 0 HcmV?d00001 diff --git a/art/palette/500.png b/art/palette/500.png new file mode 100644 index 0000000000000000000000000000000000000000..a6b8053454f9569a99de69421ce3b0c047f76b4b GIT binary patch literal 1141 zcmaJ>Ye*DP6duha%pOui531Yrj&@$U>+a-OyUto`joG%K)Q=ixt~%+=oo24>=#P+| z6g?ma`jJ6G{fZvH3WTx;LSbQ1QBhO`Q6UvnL{QcpbFCk319R^^hwuB&Irp5IeKpl9 z#}!O3peSlwVwIdEJLkV+^2u78`)x1Trr~%UUXL1ZT6G{5)loeJ2~%x^NvP^=TW`WL ziW)WFNY&vwWsRsIlTm#PyL4{N73fN?< zYIorJ_Ue?@-mFD*dU-i0^F$(GLaYMMY_VL?ljtE`k(_-uOM@W@ZkFiZN!2MepaMA% z1es7k<4eMzG{W$q60SJ36bKw2VYx8N7Y8_AhS5)g!E0$=I>F*Lu zdqmc9hp&lT80)Dv%QKu`QU<6f|A(692-?L-_$S|g3cIN`8?s60qE<&Ejcb_ehqA>A z2dWr3DTG=ww^-AJFmjua4FsOy!6HS~49oW{9-=6Un6O-|S{h8q5=}%H!_dW8sUU~q z@lq}t6L>xz3s(xkaFkfG5D!IT@eEf+TB`{yoZ;$!xZ$i^e;Q1iM3$jrY=e5GgG?|~ zwP<9|C74xjn5$>cC6txRl4MxFw}16I<01p(A4eva7$dU}Ei&*9ne5JWpBIv^;&nof zro1Nu_oeYU>!$90xq4S`{*5W~wileqnZb4DpX#kD>C5fQf3$myu<_W;zR9PZLr;yC zo*zd(%|{jPiA2Me&L3C5HFpIzbj{k*eV$plxT?SC==TdRe-3nwoqowaJORJkcyjcu zcY`T}TvB6rRnfFs&pg`+=sBQmLIkb#+Ha-Q8Jr%Q4xOlpdsU=DIuS%$;Vg?rM~q zWFb+|Q!l+lKNL|3LXbg!QW8N3{X9fbQPfL6&{Jhlcg(dO+6Ly{dk*LO&UfxPGb61n zjrqB&a|wdTk2Fb9ytD4RXaQc^=6ySiw^bQZ51*rTOp9p zn+9yKeV`?#4s@%2jjF39Yf=IxFaT1>l+j~4LQ14&cm;fR-3&#}grIJb`YozmZY4v| z24n^8^Qv5V689? zAxmJGWHL!7%V=oF8P4y=96ZDGUL4_d`c0&yyrxr>VUU2M+PZ~wXp%0Y(g}N!NMWVF zOE9cCTGN@mCVXK`O0gJ@X5Er9fwKJnP{Wvuc2E@j(fdzgC)RHPCJG$bYpb|%T}5sx zO9qDittzQ zE+PeDsb?P^%S&#z77oAOdT`|00dLcfywL?c_3@4SyH+R#$TRNgTR(&rzX()M9e3XL zb>@6JagymTZhrXa%%$GGh3~J7TuqD&od0-deC^qiKx6F7J7VqB1vtE- zHyKE3$n(KVC%$EFvt YM8_nVe{Z7tuKOd1gj=M`!CgbY0Q#YG7ytkO literal 0 HcmV?d00001 diff --git a/art/palette/700.png b/art/palette/700.png new file mode 100644 index 0000000000000000000000000000000000000000..9baf5680bf8e4f97af118a59539108cfcef58ae9 GIT binary patch literal 1140 zcmaJ>YiQF@6wcN<=c6Kn!F*xJP!w!lTe~(ax^_vmXy)vS76o0F+^u2D!{p9(Q}BU9 z_CrNfP*8L#=no(NhWMNyqJnOU$e6!;q4?S##V5m?*-n3K26FE`hwuB&Irp5TyS{F@ zZ|dx+7>4=cD})5vY4v9W709ozWd=FG( zm}h~OOu>}2ic?ITlwAy&)h)!vum|m*6bTgef{iF%^EA;piY2sKRp(fuh-JGnWuzxmd{Ji8O>3 zM^TwfhRjrurrkm@;V{x*DVFskgx~2fpq%v^PI+EI0FGj77Sv1wcNOI(vmNpT%JfeO zx-}+iIJs*=7e-}ei(*LHEh!I_r2j*8eGKiu1o)fpxWZ1d!va(SIA*)8pvE(?WAe8yYn-rR7ip~wQTwJku6*`|(< zR?TVSNvUNxZ;Fd3ME6iz5S=xc^4WW_c%7W$QYY_V4#7w(PZBb z+~Ltz@j5OQ>dU&GuiV{zyRa?tqGd;ESz`fg-qrJZVA9*7!LH)RemMDK z&GLr_o^|^!Z?bQ^I!=dIAMAa4^un>53x`ez-u3iOdv6~)<(;?Y-uyWu{p6LCJEwj9 zR&(}-Z~dwBpH6Ih`*QpK4VTV|eGdn}z59_~S@Y_{MxpO6-I%)WSv&l5#*w83-kCMn z#LGy*BHwWTl*@C9&UlacJSBxqpDRNnU3K1Tj|%zed*66YI9tDz-XHMJ8}OAD>?3~# fjk!M`d@Aq5N)FHOUFaFw>;4DgVx4d?+PLdC%OrTF literal 0 HcmV?d00001 diff --git a/art/palette/800.png b/art/palette/800.png new file mode 100644 index 0000000000000000000000000000000000000000..8cf3d3287a3b6cc637cf80f0dadacb78ad054726 GIT binary patch literal 1134 zcmaJ>TSyd97#`cS3n^k{<%OoP=wdrJb={fVa^2a@g+^puav?&;nWHEEm`xB(TQZiSrT`*)K%Z^~s1LX9P^7K}sLlqNm8}p+ z>aAHDbY$D&YBr?`8ntCJ*_08nfB}#~W{iH*5iuqI-RD|bu_g57)}td2G8)k7bCpRpox@>*K}%f3KDQsTepx7P103Vdf)&GP&m`y zB^cJMtm({L6TUDeqgV__vu;T_pe+9%Y8bO<2gSgjeE%uz#0M?F#DD__Y!x@Ix5f=+ zi6I*(2-*T)4#_;T!xxY)O3B7i}W8 z$myy@J%28tym~WSEq^XjUM_=^Vcg#S)$5!K50HDDom_0p&OR{lz}tATMgU1-x#6pqgNGadS%R7K}C{{J+8bL|>dx3+0r)r^`|SKrK%yEUxIjme$u28swG zBI3(l94d$)4nY)D6jA38{~z3gpuRW}WcZ7|sK~&ZxlSLff!uq~;rqUG&OIkNoNnG! zRk^T|qNu7=lcbQH^WN!G$=W*k$04#Uz=S36oX%jX@)i)GvRqKQpw80L;iqM~L+@^t{N9fjCnUn1}%o$BR z4s7XZ&S*WIT3DwWHiG)1NCZrXRZukZmMaz`^q8(l&Yqj4!59R0M(E#3W#u%8BL{+7 zCK%B84Rs(CX87O+t|qu12pk_~xjL4w32?l~1;yGB7=LII&C%OMMM{pxB36XXVQh;m zTPPHmLJfnQ4wesxiH5)mLVzFwZnuTiV!(2jl@uiCYK~!J16jaRRNGJ&j?g62-zAv# zgskO`UlX}7wy4@H&v0HzC7>+-A8MKtXcsH+Prm;Yb~D{JWEJS5E=MDcYhUJtvc@C*-D%c^Ero@dP%MV7^sP6HNzqL4 z<%gG-W(=n1-21X+uC{bK9ldz@O8S1<&z;+vii#K*~ z(_YH5k|3pNieoYylFZuEZ~YRo^GArCz(gpkp_ z`$ly--G6m(MfKY@B#xZD-MDfq~9*SDyu zLhyZEY+BL|Ol6-IZ&W}nI(`E>4RpK{AT%og6Sn|tO^ b^}Z?dslBaW=VRrt_gA4(iDv0SY}bKb?f-X? literal 0 HcmV?d00001 diff --git a/art/socialcard.png b/art/socialcard.png new file mode 100644 index 0000000000000000000000000000000000000000..e2d9cda013248662ed818d8dca2590476e773482 GIT binary patch literal 441805 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%0Djj+4&T~sh6T4AEN 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
!8Y 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 X>x$K5XL|7&00000NkvXXu0mjfmWaL1 literal 0 HcmV?d00001 From 22c35f680510adde91060feeba38587ecb123b90 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 4 Nov 2020 12:21:39 -0500 Subject: [PATCH 0351/1013] Update basic-usage.md --- docs/basic-usage/basic-usage.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/basic-usage/basic-usage.md b/docs/basic-usage/basic-usage.md index 5302e1686..f1508ea5a 100644 --- a/docs/basic-usage/basic-usage.md +++ b/docs/basic-usage/basic-usage.md @@ -93,5 +93,6 @@ $all_users_with_all_their_roles = User::with('roles')->get(); $all_users_with_all_direct_permissions = User::with('permissions')->get(); $all_roles_in_database = Role::all()->pluck('name'); $users_without_any_roles = User::doesntHave('roles')->get(); +$all_roles_except_a_and_b = Role::whereNotIn('name', ['role A', 'role B'])->get(); ``` From e61ba292250c1ca3cf6ae6f91fbca763f7a60794 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 5 Nov 2020 12:17:45 -0500 Subject: [PATCH 0352/1013] Update extending.md --- docs/advanced-usage/extending.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/advanced-usage/extending.md b/docs/advanced-usage/extending.md index 673ce7f40..0396fb5b2 100644 --- a/docs/advanced-usage/extending.md +++ b/docs/advanced-usage/extending.md @@ -34,12 +34,14 @@ If you need to EXTEND the existing `Role` or `Permission` models note that: - Your `Role` model needs to extend the `Spatie\Permission\Models\Role` model - Your `Permission` model needs to extend the `Spatie\Permission\Models\Permission` model +- You need to update `config/permisison.php` to specify your namespaced model ### Replacing If you need to REPLACE the existing `Role` or `Permission` models you need to keep the following things in mind: - Your `Role` model needs to implement the `Spatie\Permission\Contracts\Role` contract - Your `Permission` model needs to implement the `Spatie\Permission\Contracts\Permission` contract +- You need to update `config/permisison.php` to specify your namespaced model ## Migrations - Adding fields to your models From 1c51a5fa12131565fe3860705163e53d7a26258a Mon Sep 17 00:00:00 2001 From: Niels Date: Mon, 9 Nov 2020 15:02:11 +0100 Subject: [PATCH 0353/1013] run test suite with PHP8 --- .github/workflows/run-tests-L7.yml | 2 +- .github/workflows/run-tests-L8.yml | 4 ++-- composer.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/run-tests-L7.yml b/.github/workflows/run-tests-L7.yml index 75baade7d..9e07c69e0 100644 --- a/.github/workflows/run-tests-L7.yml +++ b/.github/workflows/run-tests-L7.yml @@ -42,7 +42,7 @@ jobs: - name: Install dependencies run: | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:>=4.3.4" --no-interaction --no-update - composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest + composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction - name: Execute tests run: vendor/bin/phpunit diff --git a/.github/workflows/run-tests-L8.yml b/.github/workflows/run-tests-L8.yml index d9dd5fe72..d8844b560 100644 --- a/.github/workflows/run-tests-L8.yml +++ b/.github/workflows/run-tests-L8.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: true matrix: - php: [7.4, 7.3] + php: [8.0, 7.4, 7.3] laravel: [8.*] dependency-version: [prefer-lowest, prefer-stable] include: @@ -38,7 +38,7 @@ jobs: - name: Install dependencies run: | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:>=4.3.4" --no-interaction --no-update - composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest + composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction - name: Execute tests run: vendor/bin/phpunit diff --git a/composer.json b/composer.json index 4ccee4679..f4ed271e8 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ } ], "require": { - "php" : "^7.2.5", + "php" : "^7.2.5|^8.0", "illuminate/auth": "^5.8|^6.0|^7.0|^8.0", "illuminate/container": "^5.8|^6.0|^7.0|^8.0", "illuminate/contracts": "^5.8|^6.0|^7.0|^8.0", From 65ee670baa8e7cf49c281dad12f74a221045f23b Mon Sep 17 00:00:00 2001 From: Niels Date: Mon, 9 Nov 2020 15:12:06 +0100 Subject: [PATCH 0354/1013] drop support on L 5.8 --- .github/workflows/run-tests-L7.yml | 6 ++---- composer.json | 8 ++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/run-tests-L7.yml b/.github/workflows/run-tests-L7.yml index 9e07c69e0..f601ce491 100644 --- a/.github/workflows/run-tests-L7.yml +++ b/.github/workflows/run-tests-L7.yml @@ -9,16 +9,14 @@ jobs: strategy: fail-fast: true matrix: - php: [7.4, 7.3, 7.2] - laravel: [7.*, 6.*, 5.8.*] + php: [8.0, 7.4, 7.3, 7.2] + laravel: [7.*, 6.*] dependency-version: [prefer-lowest, prefer-stable] include: - laravel: 7.* testbench: 5.* - laravel: 6.* testbench: 4.* - - laravel: 5.8.* - testbench: 3.8.* name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} diff --git a/composer.json b/composer.json index f4ed271e8..cc3107051 100644 --- a/composer.json +++ b/composer.json @@ -23,10 +23,10 @@ ], "require": { "php" : "^7.2.5|^8.0", - "illuminate/auth": "^5.8|^6.0|^7.0|^8.0", - "illuminate/container": "^5.8|^6.0|^7.0|^8.0", - "illuminate/contracts": "^5.8|^6.0|^7.0|^8.0", - "illuminate/database": "^5.8|^6.0|^7.0|^8.0" + "illuminate/auth": "^6.0|^7.0|^8.0", + "illuminate/container": "^6.0|^7.0|^8.0", + "illuminate/contracts": "^6.0|^7.0|^8.0", + "illuminate/database": "^6.0|^7.0|^8.0" }, "require-dev": { "orchestra/testbench": "^3.8|^4.0|^5.0|^6.0", From 1a7dde9563217f253ef55a2ea6ce2a3305fff111 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 9 Nov 2020 11:40:01 -0500 Subject: [PATCH 0355/1013] Update composer.json Removed testbench 3.8 as well since we're also removing L5.8 --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index cc3107051..2de4b582e 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "spatie/laravel-permission", - "description": "Permission handling for Laravel 5.8 and up", + "description": "Permission handling for Laravel 6.0 and up", "keywords": [ "spatie", "laravel", @@ -29,7 +29,7 @@ "illuminate/database": "^6.0|^7.0|^8.0" }, "require-dev": { - "orchestra/testbench": "^3.8|^4.0|^5.0|^6.0", + "orchestra/testbench": "^4.0|^5.0|^6.0", "phpunit/phpunit": "^8.0|^9.0", "predis/predis": "^1.1" }, From 3892454f6457e6f2b71e2cbb02bbacb9d542278b Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 9 Nov 2020 11:44:50 -0500 Subject: [PATCH 0356/1013] Update installation-laravel.md --- docs/installation-laravel.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 616438c5f..f1e4f8b3e 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -3,9 +3,13 @@ title: Installation in Laravel weight: 4 --- -This package can be used with Laravel 5.8 or higher. +This package can be used with Laravel 6.0 or higher. -1. Consult the Prerequisites page for important considerations regarding your User models! +(For Laravel 5.8, use v3.17.0) + +## Installing + +1. Consult the **Prerequisites** page for important considerations regarding your **User** models! 2. This package publishes a `config/permission.php` file. If you already have a file by that name, you must rename or remove it. From a67214893a04c9fa8af1076116dab0bad8ec604d Mon Sep 17 00:00:00 2001 From: MrReeds <16443716+MrReeds@users.noreply.github.com> Date: Sat, 21 Nov 2020 02:13:28 +0200 Subject: [PATCH 0357/1013] Use Eloquent\Collection The extended Eloquent\Model will use Eloquent\Collection: for consistency, collection merging, etc --- src/Models/Permission.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 054eb2905..8e712f4d7 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -4,7 +4,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; -use Illuminate\Support\Collection; +use Illuminate\Database\Eloquent\Collection; use Spatie\Permission\Contracts\Permission as PermissionContract; use Spatie\Permission\Exceptions\PermissionAlreadyExists; use Spatie\Permission\Exceptions\PermissionDoesNotExist; From 00f7cc5dd7d990e198417fc8183ef063cf33064f Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 26 Nov 2020 15:46:41 -0500 Subject: [PATCH 0358/1013] Update test due to phpunit deprecations --- tests/CommandTest.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tests/CommandTest.php b/tests/CommandTest.php index b900b8162..2071e45a9 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -94,11 +94,16 @@ public function it_can_show_permission_tables() $this->assertTrue(strpos($output, 'Guard: web') !== false); $this->assertTrue(strpos($output, 'Guard: admin') !== false); - // | | testRole | testRole2 | - $this->assertRegExp('/\|\s+\|\s+testRole\s+\|\s+testRole2\s+\|/', $output); + if (method_exists($this, 'assertMatchesRegularExpression')) { + // | | testRole | testRole2 | + $this->assertMatchesRegularExpression('/\|\s+\|\s+testRole\s+\|\s+testRole2\s+\|/', $output); - // | edit-articles | · | · | - $this->assertRegExp('/\|\s+edit-articles\s+\|\s+·\s+\|\s+·\s+\|/', $output); + // | edit-articles | · | · | + $this->assertMatchesRegularExpression('/\|\s+edit-articles\s+\|\s+·\s+\|\s+·\s+\|/', $output); + } else { // phpUnit 9/8 + $this->assertRegExp('/\|\s+\|\s+testRole\s+\|\s+testRole2\s+\|/', $output); + $this->assertRegExp('/\|\s+edit-articles\s+\|\s+·\s+\|\s+·\s+\|/', $output); + } Role::findByName('testRole')->givePermissionTo('edit-articles'); $this->reloadPermissions(); @@ -108,7 +113,11 @@ public function it_can_show_permission_tables() $output = Artisan::output(); // | edit-articles | · | · | - $this->assertRegExp('/\|\s+edit-articles\s+\|\s+✔\s+\|\s+·\s+\|/', $output); + if (method_exists($this, 'assertMatchesRegularExpression')) { + $this->assertMatchesRegularExpression('/\|\s+edit-articles\s+\|\s+✔\s+\|\s+·\s+\|/', $output); + } else { + $this->assertRegExp('/\|\s+edit-articles\s+\|\s+✔\s+\|\s+·\s+\|/', $output); + } } /** @test */ From 2cbd00e18619e5c104fa7c85ef3b0972f4d07fd7 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 26 Nov 2020 15:58:33 -0500 Subject: [PATCH 0359/1013] Convert from \Support\Collection to Eloquent\Collection for $permissions --- src/Models/Permission.php | 2 +- src/PermissionRegistrar.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 8e712f4d7..4de94c966 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -2,9 +2,9 @@ namespace Spatie\Permission\Models; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; -use Illuminate\Database\Eloquent\Collection; use Spatie\Permission\Contracts\Permission as PermissionContract; use Spatie\Permission\Exceptions\PermissionAlreadyExists; use Spatie\Permission\Exceptions\PermissionDoesNotExist; diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index acad62f26..6bfba05d5 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -5,7 +5,7 @@ use Illuminate\Cache\CacheManager; use Illuminate\Contracts\Auth\Access\Authorizable; use Illuminate\Contracts\Auth\Access\Gate; -use Illuminate\Support\Collection; +use Illuminate\Database\Eloquent\Collection; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Contracts\Role; @@ -23,7 +23,7 @@ class PermissionRegistrar /** @var string */ protected $roleClass; - /** @var \Illuminate\Support\Collection */ + /** @var \Illuminate\Database\Eloquent\Collection */ protected $permissions; /** @var \DateInterval|int */ @@ -119,7 +119,7 @@ public function clearClassPermissions() * * @param array $params * - * @return \Illuminate\Support\Collection + * @return \Illuminate\Database\Eloquent\Collection */ public function getPermissions(array $params = []): Collection { From 66f6e4a376fed2a3be11605daa4bd02c001c504e Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 26 Nov 2020 20:00:47 -0500 Subject: [PATCH 0360/1013] Update migration stub publishing Credits to josepostiga --- src/PermissionServiceProvider.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index e45361b18..e9e20e660 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -20,7 +20,7 @@ public function boot(PermissionRegistrar $permissionLoader, Filesystem $filesyst ], 'config'); $this->publishes([ - __DIR__.'/../database/migrations/create_permission_tables.php.stub' => $this->getMigrationFileName($filesystem), + __DIR__.'/../database/migrations/create_permission_tables.php.stub' => $this->getMigrationFileName($filesystem, 'create_permission_tables.php'), ], 'migrations'); } @@ -157,7 +157,7 @@ protected function registerMacroHelpers() * @param Filesystem $filesystem * @return string */ - protected function getMigrationFileName(Filesystem $filesystem): string + protected function getMigrationFileName(Filesystem $filesystem, $migrationFileName): string { $timestamp = date('Y_m_d_His'); @@ -165,6 +165,10 @@ protected function getMigrationFileName(Filesystem $filesystem): string ->flatMap(function ($path) use ($filesystem) { return $filesystem->glob($path.'*_create_permission_tables.php'); })->push($this->app->databasePath()."/migrations/{$timestamp}_create_permission_tables.php") + ->flatMap(function ($path) use ($filesystem, $migrationFileName) { + return $filesystem->glob($path.'*_'.$migrationFileName); + }) + ->push($this->app->databasePath()."/migrations/{$timestamp}_{$migrationFileName}") ->first(); } } From 1a67964ce45e5077655aeb65ad5c7cc9a6be8e2e Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 27 Nov 2020 00:03:42 -0500 Subject: [PATCH 0361/1013] Tests - flip expected/actual parameters to proper order --- tests/HasPermissionsTest.php | 22 +-- tests/HasRolesTest.php | 24 ++-- tests/MiddlewareTest.php | 236 ++++++++----------------------- tests/WildcardMiddlewareTest.php | 68 +++------ 4 files changed, 105 insertions(+), 245 deletions(-) diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index bd718d0ba..8ef5c4714 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -61,8 +61,8 @@ public function it_can_scope_users_using_a_string() $scopedUsers1 = User::permission('edit-articles')->get(); $scopedUsers2 = User::permission(['edit-news'])->get(); - $this->assertEquals($scopedUsers1->count(), 2); - $this->assertEquals($scopedUsers2->count(), 1); + $this->assertEquals(2, $scopedUsers1->count()); + $this->assertEquals(1, $scopedUsers2->count()); } /** @test */ @@ -77,8 +77,8 @@ public function it_can_scope_users_using_an_array() $scopedUsers1 = User::permission(['edit-articles', 'edit-news'])->get(); $scopedUsers2 = User::permission(['edit-news'])->get(); - $this->assertEquals($scopedUsers1->count(), 2); - $this->assertEquals($scopedUsers2->count(), 1); + $this->assertEquals(2, $scopedUsers1->count()); + $this->assertEquals(1, $scopedUsers2->count()); } /** @test */ @@ -93,8 +93,8 @@ public function it_can_scope_users_using_a_collection() $scopedUsers1 = User::permission(collect(['edit-articles', 'edit-news']))->get(); $scopedUsers2 = User::permission(collect(['edit-news']))->get(); - $this->assertEquals($scopedUsers1->count(), 2); - $this->assertEquals($scopedUsers2->count(), 1); + $this->assertEquals(2, $scopedUsers1->count()); + $this->assertEquals(1, $scopedUsers2->count()); } /** @test */ @@ -107,9 +107,9 @@ public function it_can_scope_users_using_an_object() $scopedUsers2 = User::permission([$this->testUserPermission])->get(); $scopedUsers3 = User::permission(collect([$this->testUserPermission]))->get(); - $this->assertEquals($scopedUsers1->count(), 1); - $this->assertEquals($scopedUsers2->count(), 1); - $this->assertEquals($scopedUsers3->count(), 1); + $this->assertEquals(1, $scopedUsers1->count()); + $this->assertEquals(1, $scopedUsers2->count()); + $this->assertEquals(1, $scopedUsers3->count()); } /** @test */ @@ -123,7 +123,7 @@ public function it_can_scope_users_without_permissions_only_role() $scopedUsers = User::permission('edit-articles')->get(); - $this->assertEquals($scopedUsers->count(), 2); + $this->assertEquals(2, $scopedUsers->count()); } /** @test */ @@ -136,7 +136,7 @@ public function it_can_scope_users_without_permissions_only_permission() $scopedUsers = User::permission('edit-news')->get(); - $this->assertEquals($scopedUsers->count(), 2); + $this->assertEquals(2, $scopedUsers->count()); } /** @test */ diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 902d520dd..bd4c58395 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -305,7 +305,7 @@ public function it_can_scope_users_using_a_string() $scopedUsers = User::role('testRole')->get(); - $this->assertEquals($scopedUsers->count(), 1); + $this->assertEquals(1, $scopedUsers->count()); } /** @test */ @@ -320,8 +320,8 @@ public function it_can_scope_users_using_an_array() $scopedUsers2 = User::role(['testRole', 'testRole2'])->get(); - $this->assertEquals($scopedUsers1->count(), 1); - $this->assertEquals($scopedUsers2->count(), 2); + $this->assertEquals(1, $scopedUsers1->count()); + $this->assertEquals(2, $scopedUsers2->count()); } /** @test */ @@ -340,7 +340,7 @@ public function it_can_scope_users_using_an_array_of_ids_and_names() $scopedUsers = User::role([$roleName, $otherRoleId])->get(); - $this->assertEquals($scopedUsers->count(), 2); + $this->assertEquals(2, $scopedUsers->count()); } /** @test */ @@ -354,8 +354,8 @@ public function it_can_scope_users_using_a_collection() $scopedUsers1 = User::role([$this->testUserRole])->get(); $scopedUsers2 = User::role(collect(['testRole', 'testRole2']))->get(); - $this->assertEquals($scopedUsers1->count(), 1); - $this->assertEquals($scopedUsers2->count(), 2); + $this->assertEquals(1, $scopedUsers1->count()); + $this->assertEquals(2, $scopedUsers2->count()); } /** @test */ @@ -370,9 +370,9 @@ public function it_can_scope_users_using_an_object() $scopedUsers2 = User::role([$this->testUserRole])->get(); $scopedUsers3 = User::role(collect([$this->testUserRole]))->get(); - $this->assertEquals($scopedUsers1->count(), 1); - $this->assertEquals($scopedUsers2->count(), 1); - $this->assertEquals($scopedUsers3->count(), 1); + $this->assertEquals(1, $scopedUsers1->count()); + $this->assertEquals(1, $scopedUsers2->count()); + $this->assertEquals(1, $scopedUsers3->count()); } /** @test */ @@ -385,7 +385,7 @@ public function it_can_scope_against_a_specific_guard() $scopedUsers1 = User::role('testRole', 'web')->get(); - $this->assertEquals($scopedUsers1->count(), 1); + $this->assertEquals(1, $scopedUsers1->count()); $user3 = Admin::create(['email' => 'user1@test.com']); $user4 = Admin::create(['email' => 'user1@test.com']); @@ -397,8 +397,8 @@ public function it_can_scope_against_a_specific_guard() $scopedUsers2 = Admin::role('testAdminRole', 'admin')->get(); $scopedUsers3 = Admin::role('testAdminRole2', 'admin')->get(); - $this->assertEquals($scopedUsers2->count(), 2); - $this->assertEquals($scopedUsers3->count(), 1); + $this->assertEquals(2, $scopedUsers2->count()); + $this->assertEquals(1, $scopedUsers3->count()); } /** @test */ diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php index 0787922a5..98b11ac05 100644 --- a/tests/MiddlewareTest.php +++ b/tests/MiddlewareTest.php @@ -32,24 +32,16 @@ public function setUp(): void /** @test */ public function a_guest_cannot_access_a_route_protected_by_the_role_or_permission_middleware() { - $this->assertEquals( - $this->runMiddleware( - $this->roleOrPermissionMiddleware, - 'testRole' - ), - 403 + $this->assertEquals(403, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole') ); } /** @test */ public function a_guest_cannot_access_a_route_protected_by_rolemiddleware() { - $this->assertEquals( - $this->runMiddleware( - $this->roleMiddleware, - 'testRole' - ), - 403 + $this->assertEquals(403, + $this->runMiddleware($this->roleMiddleware, 'testRole') ); } @@ -60,12 +52,8 @@ public function a_user_cannot_access_a_route_protected_by_role_middleware_of_ano $this->testUser->assignRole('testRole'); - $this->assertEquals( - $this->runMiddleware( - $this->roleMiddleware, - 'testAdminRole' - ), - 403 + $this->assertEquals(403, + $this->runMiddleware($this->roleMiddleware, 'testAdminRole') ); } @@ -76,12 +64,8 @@ public function a_user_can_access_a_route_protected_by_role_middleware_if_have_t $this->testUser->assignRole('testRole'); - $this->assertEquals( - $this->runMiddleware( - $this->roleMiddleware, - 'testRole' - ), - 200 + $this->assertEquals(200, + $this->runMiddleware($this->roleMiddleware, 'testRole') ); } @@ -92,20 +76,12 @@ public function a_user_can_access_a_route_protected_by_this_role_middleware_if_h $this->testUser->assignRole('testRole'); - $this->assertEquals( - $this->runMiddleware( - $this->roleMiddleware, - 'testRole|testRole2' - ), - 200 + $this->assertEquals(200, + $this->runMiddleware($this->roleMiddleware, 'testRole|testRole2') ); - $this->assertEquals( - $this->runMiddleware( - $this->roleMiddleware, - ['testRole2', 'testRole'] - ), - 200 + $this->assertEquals(200, + $this->runMiddleware($this->roleMiddleware, ['testRole2', 'testRole']) ); } @@ -116,12 +92,8 @@ public function a_user_cannot_access_a_route_protected_by_the_role_middleware_if $this->testUser->assignRole(['testRole']); - $this->assertEquals( - $this->runMiddleware( - $this->roleMiddleware, - 'testRole2' - ), - 403 + $this->assertEquals(403, + $this->runMiddleware($this->roleMiddleware, 'testRole2') ); } @@ -130,12 +102,8 @@ public function a_user_cannot_access_a_route_protected_by_role_middleware_if_hav { Auth::login($this->testUser); - $this->assertEquals( - $this->runMiddleware( - $this->roleMiddleware, - 'testRole|testRole2' - ), - 403 + $this->assertEquals(403, + $this->runMiddleware($this->roleMiddleware, 'testRole|testRole2') ); } @@ -144,24 +112,16 @@ public function a_user_cannot_access_a_route_protected_by_role_middleware_if_rol { Auth::login($this->testUser); - $this->assertEquals( - $this->runMiddleware( - $this->roleMiddleware, - '' - ), - 403 + $this->assertEquals(403, + $this->runMiddleware($this->roleMiddleware, '') ); } /** @test */ public function a_guest_cannot_access_a_route_protected_by_the_permission_middleware() { - $this->assertEquals( - $this->runMiddleware( - $this->permissionMiddleware, - 'edit-articles' - ), - 403 + $this->assertEquals(403, + $this->runMiddleware($this->permissionMiddleware, 'edit-articles') ); } @@ -178,40 +138,24 @@ public function a_user_cannot_access_a_route_protected_by_the_permission_middlew $this->testAdmin->givePermissionTo('admin-permission2'); - $this->assertEquals( - $this->runMiddleware( - $this->permissionMiddleware, - 'admin-permission2' - ), - 200 + $this->assertEquals(200, + $this->runMiddleware($this->permissionMiddleware, 'admin-permission2') ); - $this->assertEquals( - $this->runMiddleware( - $this->permissionMiddleware, - 'edit-articles2' - ), - 403 + $this->assertEquals(403, + $this->runMiddleware($this->permissionMiddleware, 'edit-articles2') ); Auth::login($this->testUser); $this->testUser->givePermissionTo('edit-articles2'); - $this->assertEquals( - $this->runMiddleware( - $this->permissionMiddleware, - 'edit-articles2' - ), - 200 + $this->assertEquals(200, + $this->runMiddleware($this->permissionMiddleware, 'edit-articles2') ); - $this->assertEquals( - $this->runMiddleware( - $this->permissionMiddleware, - 'admin-permission2' - ), - 403 + $this->assertEquals(403, + $this->runMiddleware($this->permissionMiddleware, 'admin-permission2') ); } @@ -222,12 +166,8 @@ public function a_user_can_access_a_route_protected_by_permission_middleware_if_ $this->testUser->givePermissionTo('edit-articles'); - $this->assertEquals( - $this->runMiddleware( - $this->permissionMiddleware, - 'edit-articles' - ), - 200 + $this->assertEquals(200, + $this->runMiddleware($this->permissionMiddleware, 'edit-articles') ); } @@ -238,20 +178,12 @@ public function a_user_can_access_a_route_protected_by_this_permission_middlewar $this->testUser->givePermissionTo('edit-articles'); - $this->assertEquals( - $this->runMiddleware( - $this->permissionMiddleware, - 'edit-news|edit-articles' - ), - 200 + $this->assertEquals(200, + $this->runMiddleware($this->permissionMiddleware, 'edit-news|edit-articles') ); - $this->assertEquals( - $this->runMiddleware( - $this->permissionMiddleware, - ['edit-news', 'edit-articles'] - ), - 200 + $this->assertEquals(200, + $this->runMiddleware($this->permissionMiddleware, ['edit-news', 'edit-articles']) ); } @@ -262,12 +194,8 @@ public function a_user_cannot_access_a_route_protected_by_the_permission_middlew $this->testUser->givePermissionTo('edit-articles'); - $this->assertEquals( - $this->runMiddleware( - $this->permissionMiddleware, - 'edit-news' - ), - 403 + $this->assertEquals(403, + $this->runMiddleware($this->permissionMiddleware, 'edit-news') ); } @@ -276,12 +204,8 @@ public function a_user_cannot_access_a_route_protected_by_permission_middleware_ { Auth::login($this->testUser); - $this->assertEquals( - $this->runMiddleware( - $this->permissionMiddleware, - 'edit-articles|edit-news' - ), - 403 + $this->assertEquals(403, + $this->runMiddleware($this->permissionMiddleware, 'edit-articles|edit-news') ); } @@ -293,29 +217,25 @@ public function a_user_can_access_a_route_protected_by_permission_or_role_middle $this->testUser->assignRole('testRole'); $this->testUser->givePermissionTo('edit-articles'); - $this->assertEquals( - $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-news|edit-articles'), - 200 + $this->assertEquals(200, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-news|edit-articles') ); $this->testUser->removeRole('testRole'); - $this->assertEquals( - $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles'), - 200 + $this->assertEquals(200, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles') ); $this->testUser->revokePermissionTo('edit-articles'); $this->testUser->assignRole('testRole'); - $this->assertEquals( - $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles'), - 200 + $this->assertEquals(200, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles') ); - $this->assertEquals( - $this->runMiddleware($this->roleOrPermissionMiddleware, ['testRole', 'edit-articles']), - 200 + $this->assertEquals(200, + $this->runMiddleware($this->roleOrPermissionMiddleware, ['testRole', 'edit-articles']) ); } @@ -324,14 +244,12 @@ public function a_user_can_not_access_a_route_protected_by_permission_or_role_mi { Auth::login($this->testUser); - $this->assertEquals( - $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles'), - 403 + $this->assertEquals(403, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles') ); - $this->assertEquals( - $this->runMiddleware($this->roleOrPermissionMiddleware, 'missingRole|missingPermission'), - 403 + $this->assertEquals(403, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'missingRole|missingPermission') ); } @@ -426,13 +344,8 @@ public function user_can_not_access_role_with_guard_admin_while_using_default_gu $this->testUser->assignRole('testRole'); - $this->assertEquals( - $this->runMiddleware( - $this->roleMiddleware, - 'testRole', - 'admin' - ), - 403 + $this->assertEquals(403, + $this->runMiddleware($this->roleMiddleware, 'testRole', 'admin') ); } @@ -443,13 +356,8 @@ public function user_can_access_role_with_guard_admin_while_using_default_guard( $this->testAdmin->assignRole('testAdminRole'); - $this->assertEquals( - $this->runMiddleware( - $this->roleMiddleware, - 'testAdminRole', - 'admin' - ), - 200 + $this->assertEquals(200, + $this->runMiddleware($this->roleMiddleware, 'testAdminRole', 'admin') ); } @@ -460,13 +368,8 @@ public function user_can_not_access_permission_with_guard_admin_while_using_defa $this->testUser->givePermissionTo('edit-articles'); - $this->assertEquals( - $this->runMiddleware( - $this->permissionMiddleware, - 'edit-articles', - 'admin' - ), - 403 + $this->assertEquals(403, + $this->runMiddleware($this->permissionMiddleware, 'edit-articles', 'admin') ); } @@ -477,13 +380,8 @@ public function user_can_access_permission_with_guard_admin_while_using_default_ $this->testAdmin->givePermissionTo('admin-permission'); - $this->assertEquals( - $this->runMiddleware( - $this->permissionMiddleware, - 'admin-permission', - 'admin' - ), - 200 + $this->assertEquals(200, + $this->runMiddleware($this->permissionMiddleware, 'admin-permission', 'admin') ); } @@ -495,13 +393,8 @@ public function user_can_not_access_permission_or_role_with_guard_admin_while_us $this->testUser->assignRole('testRole'); $this->testUser->givePermissionTo('edit-articles'); - $this->assertEquals( - $this->runMiddleware( - $this->roleOrPermissionMiddleware, - 'edit-articles|testRole', - 'admin' - ), - 403 + $this->assertEquals(403, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'edit-articles|testRole', 'admin') ); } @@ -513,13 +406,8 @@ public function user_can_access_permission_or_role_with_guard_admin_while_using_ $this->testAdmin->assignRole('testAdminRole'); $this->testAdmin->givePermissionTo('admin-permission'); - $this->assertEquals( - $this->runMiddleware( - $this->roleOrPermissionMiddleware, - 'admin-permission|testAdminRole', - 'admin' - ), - 200 + $this->assertEquals(200, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'admin-permission|testAdminRole', 'admin') ); } diff --git a/tests/WildcardMiddlewareTest.php b/tests/WildcardMiddlewareTest.php index 260223472..b3b2b5e08 100644 --- a/tests/WildcardMiddlewareTest.php +++ b/tests/WildcardMiddlewareTest.php @@ -33,12 +33,8 @@ public function setUp(): void /** @test */ public function a_guest_cannot_access_a_route_protected_by_the_permission_middleware() { - $this->assertEquals( - $this->runMiddleware( - $this->permissionMiddleware, - 'articles.edit' - ), - 403 + $this->assertEquals(403, + $this->runMiddleware($this->permissionMiddleware, 'articles.edit') ); } @@ -51,12 +47,8 @@ public function a_user_can_access_a_route_protected_by_permission_middleware_if_ $this->testUser->givePermissionTo('articles'); - $this->assertEquals( - $this->runMiddleware( - $this->permissionMiddleware, - 'articles.edit' - ), - 200 + $this->assertEquals(200, + $this->runMiddleware($this->permissionMiddleware, 'articles.edit') ); } @@ -69,20 +61,12 @@ public function a_user_can_access_a_route_protected_by_this_permission_middlewar $this->testUser->givePermissionTo('articles.*.test'); - $this->assertEquals( - $this->runMiddleware( - $this->permissionMiddleware, - 'news.edit|articles.create.test' - ), - 200 + $this->assertEquals(200, + $this->runMiddleware($this->permissionMiddleware, 'news.edit|articles.create.test') ); - $this->assertEquals( - $this->runMiddleware( - $this->permissionMiddleware, - ['news.edit', 'articles.create.test'] - ), - 200 + $this->assertEquals(200, + $this->runMiddleware($this->permissionMiddleware, ['news.edit', 'articles.create.test']) ); } @@ -95,12 +79,8 @@ public function a_user_cannot_access_a_route_protected_by_the_permission_middlew $this->testUser->givePermissionTo('articles.*'); - $this->assertEquals( - $this->runMiddleware( - $this->permissionMiddleware, - 'news.edit' - ), - 403 + $this->assertEquals(403, + $this->runMiddleware($this->permissionMiddleware, 'news.edit') ); } @@ -109,12 +89,8 @@ public function a_user_cannot_access_a_route_protected_by_permission_middleware_ { Auth::login($this->testUser); - $this->assertEquals( - $this->runMiddleware( - $this->permissionMiddleware, - 'articles.edit|news.edit' - ), - 403 + $this->assertEquals(403, + $this->runMiddleware($this->permissionMiddleware, 'articles.edit|news.edit') ); } @@ -128,29 +104,25 @@ public function a_user_can_access_a_route_protected_by_permission_or_role_middle $this->testUser->assignRole('testRole'); $this->testUser->givePermissionTo('articles.*'); - $this->assertEquals( - $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|news.edit|articles.create'), - 200 + $this->assertEquals(200, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|news.edit|articles.create') ); $this->testUser->removeRole('testRole'); - $this->assertEquals( - $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|articles.edit'), - 200 + $this->assertEquals(200, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|articles.edit') ); $this->testUser->revokePermissionTo('articles.*'); $this->testUser->assignRole('testRole'); - $this->assertEquals( - $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|articles.edit'), - 200 + $this->assertEquals(200, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|articles.edit') ); - $this->assertEquals( - $this->runMiddleware($this->roleOrPermissionMiddleware, ['testRole', 'articles.edit']), - 200 + $this->assertEquals(200, + $this->runMiddleware($this->roleOrPermissionMiddleware, ['testRole', 'articles.edit']) ); } From c3de063cc579bb287cc185779255a6cb41ac28f6 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 27 Nov 2020 00:37:19 -0500 Subject: [PATCH 0362/1013] Refactor Middleware tests to separate files --- tests/MiddlewareTest.php | 424 ----------------------- tests/PermissionMiddlewareTest.php | 184 ++++++++++ tests/RoleMiddlewareTest.php | 171 +++++++++ tests/RoleOrPermissionMiddlewareTest.php | 127 +++++++ 4 files changed, 482 insertions(+), 424 deletions(-) delete mode 100644 tests/MiddlewareTest.php create mode 100644 tests/PermissionMiddlewareTest.php create mode 100644 tests/RoleMiddlewareTest.php create mode 100644 tests/RoleOrPermissionMiddlewareTest.php diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php deleted file mode 100644 index 98b11ac05..000000000 --- a/tests/MiddlewareTest.php +++ /dev/null @@ -1,424 +0,0 @@ -roleMiddleware = new RoleMiddleware(); - - $this->permissionMiddleware = new PermissionMiddleware(); - - $this->roleOrPermissionMiddleware = new RoleOrPermissionMiddleware(); - } - - /** @test */ - public function a_guest_cannot_access_a_route_protected_by_the_role_or_permission_middleware() - { - $this->assertEquals(403, - $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole') - ); - } - - /** @test */ - public function a_guest_cannot_access_a_route_protected_by_rolemiddleware() - { - $this->assertEquals(403, - $this->runMiddleware($this->roleMiddleware, 'testRole') - ); - } - - /** @test */ - public function a_user_cannot_access_a_route_protected_by_role_middleware_of_another_guard() - { - Auth::login($this->testUser); - - $this->testUser->assignRole('testRole'); - - $this->assertEquals(403, - $this->runMiddleware($this->roleMiddleware, 'testAdminRole') - ); - } - - /** @test */ - public function a_user_can_access_a_route_protected_by_role_middleware_if_have_this_role() - { - Auth::login($this->testUser); - - $this->testUser->assignRole('testRole'); - - $this->assertEquals(200, - $this->runMiddleware($this->roleMiddleware, 'testRole') - ); - } - - /** @test */ - public function a_user_can_access_a_route_protected_by_this_role_middleware_if_have_one_of_the_roles() - { - Auth::login($this->testUser); - - $this->testUser->assignRole('testRole'); - - $this->assertEquals(200, - $this->runMiddleware($this->roleMiddleware, 'testRole|testRole2') - ); - - $this->assertEquals(200, - $this->runMiddleware($this->roleMiddleware, ['testRole2', 'testRole']) - ); - } - - /** @test */ - public function a_user_cannot_access_a_route_protected_by_the_role_middleware_if_have_a_different_role() - { - Auth::login($this->testUser); - - $this->testUser->assignRole(['testRole']); - - $this->assertEquals(403, - $this->runMiddleware($this->roleMiddleware, 'testRole2') - ); - } - - /** @test */ - public function a_user_cannot_access_a_route_protected_by_role_middleware_if_have_not_roles() - { - Auth::login($this->testUser); - - $this->assertEquals(403, - $this->runMiddleware($this->roleMiddleware, 'testRole|testRole2') - ); - } - - /** @test */ - public function a_user_cannot_access_a_route_protected_by_role_middleware_if_role_is_undefined() - { - Auth::login($this->testUser); - - $this->assertEquals(403, - $this->runMiddleware($this->roleMiddleware, '') - ); - } - - /** @test */ - public function a_guest_cannot_access_a_route_protected_by_the_permission_middleware() - { - $this->assertEquals(403, - $this->runMiddleware($this->permissionMiddleware, 'edit-articles') - ); - } - - /** @test */ - public function a_user_cannot_access_a_route_protected_by_the_permission_middleware_of_a_different_guard() - { - // These permissions are created fresh here in reverse order of guard being applied, so they are not "found first" in the db lookup when matching - app(Permission::class)->create(['name' => 'admin-permission2', 'guard_name' => 'web']); - app(Permission::class)->create(['name' => 'admin-permission2', 'guard_name' => 'admin']); - app(Permission::class)->create(['name' => 'edit-articles2', 'guard_name' => 'admin']); - app(Permission::class)->create(['name' => 'edit-articles2', 'guard_name' => 'web']); - - Auth::login($this->testAdmin); - - $this->testAdmin->givePermissionTo('admin-permission2'); - - $this->assertEquals(200, - $this->runMiddleware($this->permissionMiddleware, 'admin-permission2') - ); - - $this->assertEquals(403, - $this->runMiddleware($this->permissionMiddleware, 'edit-articles2') - ); - - Auth::login($this->testUser); - - $this->testUser->givePermissionTo('edit-articles2'); - - $this->assertEquals(200, - $this->runMiddleware($this->permissionMiddleware, 'edit-articles2') - ); - - $this->assertEquals(403, - $this->runMiddleware($this->permissionMiddleware, 'admin-permission2') - ); - } - - /** @test */ - public function a_user_can_access_a_route_protected_by_permission_middleware_if_have_this_permission() - { - Auth::login($this->testUser); - - $this->testUser->givePermissionTo('edit-articles'); - - $this->assertEquals(200, - $this->runMiddleware($this->permissionMiddleware, 'edit-articles') - ); - } - - /** @test */ - public function a_user_can_access_a_route_protected_by_this_permission_middleware_if_have_one_of_the_permissions() - { - Auth::login($this->testUser); - - $this->testUser->givePermissionTo('edit-articles'); - - $this->assertEquals(200, - $this->runMiddleware($this->permissionMiddleware, 'edit-news|edit-articles') - ); - - $this->assertEquals(200, - $this->runMiddleware($this->permissionMiddleware, ['edit-news', 'edit-articles']) - ); - } - - /** @test */ - public function a_user_cannot_access_a_route_protected_by_the_permission_middleware_if_have_a_different_permission() - { - Auth::login($this->testUser); - - $this->testUser->givePermissionTo('edit-articles'); - - $this->assertEquals(403, - $this->runMiddleware($this->permissionMiddleware, 'edit-news') - ); - } - - /** @test */ - public function a_user_cannot_access_a_route_protected_by_permission_middleware_if_have_not_permissions() - { - Auth::login($this->testUser); - - $this->assertEquals(403, - $this->runMiddleware($this->permissionMiddleware, 'edit-articles|edit-news') - ); - } - - /** @test */ - public function a_user_can_access_a_route_protected_by_permission_or_role_middleware_if_has_this_permission_or_role() - { - Auth::login($this->testUser); - - $this->testUser->assignRole('testRole'); - $this->testUser->givePermissionTo('edit-articles'); - - $this->assertEquals(200, - $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-news|edit-articles') - ); - - $this->testUser->removeRole('testRole'); - - $this->assertEquals(200, - $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles') - ); - - $this->testUser->revokePermissionTo('edit-articles'); - $this->testUser->assignRole('testRole'); - - $this->assertEquals(200, - $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles') - ); - - $this->assertEquals(200, - $this->runMiddleware($this->roleOrPermissionMiddleware, ['testRole', 'edit-articles']) - ); - } - - /** @test */ - public function a_user_can_not_access_a_route_protected_by_permission_or_role_middleware_if_have_not_this_permission_and_role() - { - Auth::login($this->testUser); - - $this->assertEquals(403, - $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles') - ); - - $this->assertEquals(403, - $this->runMiddleware($this->roleOrPermissionMiddleware, 'missingRole|missingPermission') - ); - } - - /** @test */ - public function the_required_roles_can_be_fetched_from_the_exception() - { - Auth::login($this->testUser); - - $requiredRoles = []; - - try { - $this->roleMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); - }, 'some-role'); - } catch (UnauthorizedException $e) { - $requiredRoles = $e->getRequiredRoles(); - } - - $this->assertEquals(['some-role'], $requiredRoles); - } - - /** @test */ - public function the_required_permissions_can_be_fetched_from_the_exception() - { - Auth::login($this->testUser); - - $requiredPermissions = []; - - try { - $this->permissionMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); - }, 'some-permission'); - } catch (UnauthorizedException $e) { - $requiredPermissions = $e->getRequiredPermissions(); - } - - $this->assertEquals(['some-permission'], $requiredPermissions); - } - - /** @test */ - public function use_not_existing_custom_guard_in_role_or_permission() - { - $class = null; - - try { - $this->roleOrPermissionMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); - }, 'testRole', 'xxx'); - } catch (InvalidArgumentException $e) { - $class = get_class($e); - } - - $this->assertEquals(InvalidArgumentException::class, $class); - } - - /** @test */ - public function use_not_existing_custom_guard_in_permission() - { - $class = null; - - try { - $this->permissionMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); - }, 'edit-articles', 'xxx'); - } catch (InvalidArgumentException $e) { - $class = get_class($e); - } - - $this->assertEquals(InvalidArgumentException::class, $class); - } - - /** @test */ - public function use_not_existing_custom_guard_in_role() - { - $class = null; - - try { - $this->roleMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); - }, 'testRole', 'xxx'); - } catch (InvalidArgumentException $e) { - $class = get_class($e); - } - - $this->assertEquals(InvalidArgumentException::class, $class); - } - - /** @test */ - public function user_can_not_access_role_with_guard_admin_while_using_default_guard() - { - Auth::login($this->testUser); - - $this->testUser->assignRole('testRole'); - - $this->assertEquals(403, - $this->runMiddleware($this->roleMiddleware, 'testRole', 'admin') - ); - } - - /** @test */ - public function user_can_access_role_with_guard_admin_while_using_default_guard() - { - Auth::guard('admin')->login($this->testAdmin); - - $this->testAdmin->assignRole('testAdminRole'); - - $this->assertEquals(200, - $this->runMiddleware($this->roleMiddleware, 'testAdminRole', 'admin') - ); - } - - /** @test */ - public function user_can_not_access_permission_with_guard_admin_while_using_default_guard() - { - Auth::login($this->testUser); - - $this->testUser->givePermissionTo('edit-articles'); - - $this->assertEquals(403, - $this->runMiddleware($this->permissionMiddleware, 'edit-articles', 'admin') - ); - } - - /** @test */ - public function user_can_access_permission_with_guard_admin_while_using_default_guard() - { - Auth::guard('admin')->login($this->testAdmin); - - $this->testAdmin->givePermissionTo('admin-permission'); - - $this->assertEquals(200, - $this->runMiddleware($this->permissionMiddleware, 'admin-permission', 'admin') - ); - } - - /** @test */ - public function user_can_not_access_permission_or_role_with_guard_admin_while_using_default_guard() - { - Auth::login($this->testUser); - - $this->testUser->assignRole('testRole'); - $this->testUser->givePermissionTo('edit-articles'); - - $this->assertEquals(403, - $this->runMiddleware($this->roleOrPermissionMiddleware, 'edit-articles|testRole', 'admin') - ); - } - - /** @test */ - public function user_can_access_permission_or_role_with_guard_admin_while_using_default_guard() - { - Auth::guard('admin')->login($this->testAdmin); - - $this->testAdmin->assignRole('testAdminRole'); - $this->testAdmin->givePermissionTo('admin-permission'); - - $this->assertEquals(200, - $this->runMiddleware($this->roleOrPermissionMiddleware, 'admin-permission|testAdminRole', 'admin') - ); - } - - protected function runMiddleware($middleware, $parameter, $guard = null) - { - try { - return $middleware->handle(new Request(), function () { - return (new Response())->setContent(''); - }, $parameter, $guard)->status(); - } catch (UnauthorizedException $e) { - return $e->getStatusCode(); - } - } -} diff --git a/tests/PermissionMiddlewareTest.php b/tests/PermissionMiddlewareTest.php new file mode 100644 index 000000000..026524595 --- /dev/null +++ b/tests/PermissionMiddlewareTest.php @@ -0,0 +1,184 @@ +permissionMiddleware = new PermissionMiddleware(); + } + + /** @test */ + public function a_guest_cannot_access_a_route_protected_by_the_permission_middleware() + { + $this->assertEquals(403, + $this->runMiddleware($this->permissionMiddleware, 'edit-articles') + ); + } + + /** @test */ + public function a_user_cannot_access_a_route_protected_by_the_permission_middleware_of_a_different_guard() + { + // These permissions are created fresh here in reverse order of guard being applied, so they are not "found first" in the db lookup when matching + app(Permission::class)->create(['name' => 'admin-permission2', 'guard_name' => 'web']); + app(Permission::class)->create(['name' => 'admin-permission2', 'guard_name' => 'admin']); + app(Permission::class)->create(['name' => 'edit-articles2', 'guard_name' => 'admin']); + app(Permission::class)->create(['name' => 'edit-articles2', 'guard_name' => 'web']); + + Auth::login($this->testAdmin); + + $this->testAdmin->givePermissionTo('admin-permission2'); + + $this->assertEquals(200, + $this->runMiddleware($this->permissionMiddleware, 'admin-permission2') + ); + + $this->assertEquals(403, + $this->runMiddleware($this->permissionMiddleware, 'edit-articles2') + ); + + Auth::login($this->testUser); + + $this->testUser->givePermissionTo('edit-articles2'); + + $this->assertEquals(200, + $this->runMiddleware($this->permissionMiddleware, 'edit-articles2') + ); + + $this->assertEquals(403, + $this->runMiddleware($this->permissionMiddleware, 'admin-permission2') + ); + } + + /** @test */ + public function a_user_can_access_a_route_protected_by_permission_middleware_if_have_this_permission() + { + Auth::login($this->testUser); + + $this->testUser->givePermissionTo('edit-articles'); + + $this->assertEquals(200, + $this->runMiddleware($this->permissionMiddleware, 'edit-articles') + ); + } + + /** @test */ + public function a_user_can_access_a_route_protected_by_this_permission_middleware_if_have_one_of_the_permissions() + { + Auth::login($this->testUser); + + $this->testUser->givePermissionTo('edit-articles'); + + $this->assertEquals(200, + $this->runMiddleware($this->permissionMiddleware, 'edit-news|edit-articles') + ); + + $this->assertEquals(200, + $this->runMiddleware($this->permissionMiddleware, ['edit-news', 'edit-articles']) + ); + } + + /** @test */ + public function a_user_cannot_access_a_route_protected_by_the_permission_middleware_if_have_a_different_permission() + { + Auth::login($this->testUser); + + $this->testUser->givePermissionTo('edit-articles'); + + $this->assertEquals(403, + $this->runMiddleware($this->permissionMiddleware, 'edit-news') + ); + } + + /** @test */ + public function a_user_cannot_access_a_route_protected_by_permission_middleware_if_have_not_permissions() + { + Auth::login($this->testUser); + + $this->assertEquals(403, + $this->runMiddleware($this->permissionMiddleware, 'edit-articles|edit-news') + ); + } + + /** @test */ + public function the_required_permissions_can_be_fetched_from_the_exception() + { + Auth::login($this->testUser); + + $requiredPermissions = []; + + try { + $this->permissionMiddleware->handle(new Request(), function () { + return (new Response())->setContent(''); + }, 'some-permission'); + } catch (UnauthorizedException $e) { + $requiredPermissions = $e->getRequiredPermissions(); + } + + $this->assertEquals(['some-permission'], $requiredPermissions); + } + + /** @test */ + public function use_not_existing_custom_guard_in_permission() + { + $class = null; + + try { + $this->permissionMiddleware->handle(new Request(), function () { + return (new Response())->setContent(''); + }, 'edit-articles', 'xxx'); + } catch (InvalidArgumentException $e) { + $class = get_class($e); + } + + $this->assertEquals(InvalidArgumentException::class, $class); + } + + /** @test */ + public function user_can_not_access_permission_with_guard_admin_while_using_default_guard() + { + Auth::login($this->testUser); + + $this->testUser->givePermissionTo('edit-articles'); + + $this->assertEquals(403, + $this->runMiddleware($this->permissionMiddleware, 'edit-articles', 'admin') + ); + } + + /** @test */ + public function user_can_access_permission_with_guard_admin_while_using_default_guard() + { + Auth::guard('admin')->login($this->testAdmin); + + $this->testAdmin->givePermissionTo('admin-permission'); + + $this->assertEquals(200, + $this->runMiddleware($this->permissionMiddleware, 'admin-permission', 'admin') + ); + } + + protected function runMiddleware($middleware, $permission, $guard = null) + { + try { + return $middleware->handle(new Request(), function () { + return (new Response())->setContent(''); + }, $permission, $guard)->status(); + } catch (UnauthorizedException $e) { + return $e->getStatusCode(); + } + } +} diff --git a/tests/RoleMiddlewareTest.php b/tests/RoleMiddlewareTest.php new file mode 100644 index 000000000..0266570e9 --- /dev/null +++ b/tests/RoleMiddlewareTest.php @@ -0,0 +1,171 @@ +roleMiddleware = new RoleMiddleware(); + } + + /** @test */ + public function a_guest_cannot_access_a_route_protected_by_rolemiddleware() + { + $this->assertEquals(403, + $this->runMiddleware($this->roleMiddleware, 'testRole') + ); + } + + /** @test */ + public function a_user_cannot_access_a_route_protected_by_role_middleware_of_another_guard() + { + Auth::login($this->testUser); + + $this->testUser->assignRole('testRole'); + + $this->assertEquals(403, + $this->runMiddleware($this->roleMiddleware, 'testAdminRole') + ); + } + + /** @test */ + public function a_user_can_access_a_route_protected_by_role_middleware_if_have_this_role() + { + Auth::login($this->testUser); + + $this->testUser->assignRole('testRole'); + + $this->assertEquals(200, + $this->runMiddleware($this->roleMiddleware, 'testRole') + ); + } + + /** @test */ + public function a_user_can_access_a_route_protected_by_this_role_middleware_if_have_one_of_the_roles() + { + Auth::login($this->testUser); + + $this->testUser->assignRole('testRole'); + + $this->assertEquals(200, + $this->runMiddleware($this->roleMiddleware, 'testRole|testRole2') + ); + + $this->assertEquals(200, + $this->runMiddleware($this->roleMiddleware, ['testRole2', 'testRole']) + ); + } + + /** @test */ + public function a_user_cannot_access_a_route_protected_by_the_role_middleware_if_have_a_different_role() + { + Auth::login($this->testUser); + + $this->testUser->assignRole(['testRole']); + + $this->assertEquals(403, + $this->runMiddleware($this->roleMiddleware, 'testRole2') + ); + } + + /** @test */ + public function a_user_cannot_access_a_route_protected_by_role_middleware_if_have_not_roles() + { + Auth::login($this->testUser); + + $this->assertEquals(403, + $this->runMiddleware($this->roleMiddleware, 'testRole|testRole2') + ); + } + + /** @test */ + public function a_user_cannot_access_a_route_protected_by_role_middleware_if_role_is_undefined() + { + Auth::login($this->testUser); + + $this->assertEquals(403, + $this->runMiddleware($this->roleMiddleware, '') + ); + } + + /** @test */ + public function the_required_roles_can_be_fetched_from_the_exception() + { + Auth::login($this->testUser); + + $requiredRoles = []; + + try { + $this->roleMiddleware->handle(new Request(), function () { + return (new Response())->setContent(''); + }, 'some-role'); + } catch (UnauthorizedException $e) { + $requiredRoles = $e->getRequiredRoles(); + } + + $this->assertEquals(['some-role'], $requiredRoles); + } + + /** @test */ + public function use_not_existing_custom_guard_in_role() + { + $class = null; + + try { + $this->roleMiddleware->handle(new Request(), function () { + return (new Response())->setContent(''); + }, 'testRole', 'xxx'); + } catch (InvalidArgumentException $e) { + $class = get_class($e); + } + + $this->assertEquals(InvalidArgumentException::class, $class); + } + + /** @test */ + public function user_can_not_access_role_with_guard_admin_while_using_default_guard() + { + Auth::login($this->testUser); + + $this->testUser->assignRole('testRole'); + + $this->assertEquals(403, + $this->runMiddleware($this->roleMiddleware, 'testRole', 'admin') + ); + } + + /** @test */ + public function user_can_access_role_with_guard_admin_while_using_default_guard() + { + Auth::guard('admin')->login($this->testAdmin); + + $this->testAdmin->assignRole('testAdminRole'); + + $this->assertEquals(200, + $this->runMiddleware($this->roleMiddleware, 'testAdminRole', 'admin') + ); + } + + protected function runMiddleware($middleware, $roleName, $guard = null) + { + try { + return $middleware->handle(new Request(), function () { + return (new Response())->setContent(''); + }, $roleName, $guard)->status(); + } catch (UnauthorizedException $e) { + return $e->getStatusCode(); + } + } +} diff --git a/tests/RoleOrPermissionMiddlewareTest.php b/tests/RoleOrPermissionMiddlewareTest.php new file mode 100644 index 000000000..38fb558e1 --- /dev/null +++ b/tests/RoleOrPermissionMiddlewareTest.php @@ -0,0 +1,127 @@ +roleOrPermissionMiddleware = new RoleOrPermissionMiddleware(); + } + + /** @test */ + public function a_guest_cannot_access_a_route_protected_by_the_role_or_permission_middleware() + { + $this->assertEquals(403, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole') + ); + } + + /** @test */ + public function a_user_can_access_a_route_protected_by_permission_or_role_middleware_if_has_this_permission_or_role() + { + Auth::login($this->testUser); + + $this->testUser->assignRole('testRole'); + $this->testUser->givePermissionTo('edit-articles'); + + $this->assertEquals(200, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-news|edit-articles') + ); + + $this->testUser->removeRole('testRole'); + + $this->assertEquals(200, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles') + ); + + $this->testUser->revokePermissionTo('edit-articles'); + $this->testUser->assignRole('testRole'); + + $this->assertEquals(200, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles') + ); + + $this->assertEquals(200, + $this->runMiddleware($this->roleOrPermissionMiddleware, ['testRole', 'edit-articles']) + ); + } + + /** @test */ + public function a_user_can_not_access_a_route_protected_by_permission_or_role_middleware_if_have_not_this_permission_and_role() + { + Auth::login($this->testUser); + + $this->assertEquals(403, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles') + ); + + $this->assertEquals(403, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'missingRole|missingPermission') + ); + } + + /** @test */ + public function use_not_existing_custom_guard_in_role_or_permission() + { + $class = null; + + try { + $this->roleOrPermissionMiddleware->handle(new Request(), function () { + return (new Response())->setContent(''); + }, 'testRole', 'xxx'); + } catch (InvalidArgumentException $e) { + $class = get_class($e); + } + + $this->assertEquals(InvalidArgumentException::class, $class); + } + + /** @test */ + public function user_can_not_access_permission_or_role_with_guard_admin_while_using_default_guard() + { + Auth::login($this->testUser); + + $this->testUser->assignRole('testRole'); + $this->testUser->givePermissionTo('edit-articles'); + + $this->assertEquals(403, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'edit-articles|testRole', 'admin') + ); + } + + /** @test */ + public function user_can_access_permission_or_role_with_guard_admin_while_using_default_guard() + { + Auth::guard('admin')->login($this->testAdmin); + + $this->testAdmin->assignRole('testAdminRole'); + $this->testAdmin->givePermissionTo('admin-permission'); + + $this->assertEquals(200, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'admin-permission|testAdminRole', 'admin') + ); + } + + protected function runMiddleware($middleware, $name, $guard = null) + { + try { + return $middleware->handle(new Request(), function () { + return (new Response())->setContent(''); + }, $name, $guard)->status(); + } catch (UnauthorizedException $e) { + return $e->getStatusCode(); + } + } +} From 4d0813e83bb2a7d3caed3643fa7e7069943b383e Mon Sep 17 00:00:00 2001 From: drbyte Date: Fri, 27 Nov 2020 05:39:01 +0000 Subject: [PATCH 0363/1013] Fix styling --- tests/PermissionMiddlewareTest.php | 36 ++++++++++++++++-------- tests/RoleMiddlewareTest.php | 30 +++++++++++++------- tests/RoleOrPermissionMiddlewareTest.php | 27 ++++++++++++------ tests/WildcardMiddlewareTest.php | 30 +++++++++++++------- 4 files changed, 82 insertions(+), 41 deletions(-) diff --git a/tests/PermissionMiddlewareTest.php b/tests/PermissionMiddlewareTest.php index 026524595..dafb1eec1 100644 --- a/tests/PermissionMiddlewareTest.php +++ b/tests/PermissionMiddlewareTest.php @@ -24,7 +24,8 @@ public function setUp(): void /** @test */ public function a_guest_cannot_access_a_route_protected_by_the_permission_middleware() { - $this->assertEquals(403, + $this->assertEquals( + 403, $this->runMiddleware($this->permissionMiddleware, 'edit-articles') ); } @@ -42,11 +43,13 @@ public function a_user_cannot_access_a_route_protected_by_the_permission_middlew $this->testAdmin->givePermissionTo('admin-permission2'); - $this->assertEquals(200, + $this->assertEquals( + 200, $this->runMiddleware($this->permissionMiddleware, 'admin-permission2') ); - $this->assertEquals(403, + $this->assertEquals( + 403, $this->runMiddleware($this->permissionMiddleware, 'edit-articles2') ); @@ -54,11 +57,13 @@ public function a_user_cannot_access_a_route_protected_by_the_permission_middlew $this->testUser->givePermissionTo('edit-articles2'); - $this->assertEquals(200, + $this->assertEquals( + 200, $this->runMiddleware($this->permissionMiddleware, 'edit-articles2') ); - $this->assertEquals(403, + $this->assertEquals( + 403, $this->runMiddleware($this->permissionMiddleware, 'admin-permission2') ); } @@ -70,7 +75,8 @@ public function a_user_can_access_a_route_protected_by_permission_middleware_if_ $this->testUser->givePermissionTo('edit-articles'); - $this->assertEquals(200, + $this->assertEquals( + 200, $this->runMiddleware($this->permissionMiddleware, 'edit-articles') ); } @@ -82,11 +88,13 @@ public function a_user_can_access_a_route_protected_by_this_permission_middlewar $this->testUser->givePermissionTo('edit-articles'); - $this->assertEquals(200, + $this->assertEquals( + 200, $this->runMiddleware($this->permissionMiddleware, 'edit-news|edit-articles') ); - $this->assertEquals(200, + $this->assertEquals( + 200, $this->runMiddleware($this->permissionMiddleware, ['edit-news', 'edit-articles']) ); } @@ -98,7 +106,8 @@ public function a_user_cannot_access_a_route_protected_by_the_permission_middlew $this->testUser->givePermissionTo('edit-articles'); - $this->assertEquals(403, + $this->assertEquals( + 403, $this->runMiddleware($this->permissionMiddleware, 'edit-news') ); } @@ -108,7 +117,8 @@ public function a_user_cannot_access_a_route_protected_by_permission_middleware_ { Auth::login($this->testUser); - $this->assertEquals(403, + $this->assertEquals( + 403, $this->runMiddleware($this->permissionMiddleware, 'edit-articles|edit-news') ); } @@ -154,7 +164,8 @@ public function user_can_not_access_permission_with_guard_admin_while_using_defa $this->testUser->givePermissionTo('edit-articles'); - $this->assertEquals(403, + $this->assertEquals( + 403, $this->runMiddleware($this->permissionMiddleware, 'edit-articles', 'admin') ); } @@ -166,7 +177,8 @@ public function user_can_access_permission_with_guard_admin_while_using_default_ $this->testAdmin->givePermissionTo('admin-permission'); - $this->assertEquals(200, + $this->assertEquals( + 200, $this->runMiddleware($this->permissionMiddleware, 'admin-permission', 'admin') ); } diff --git a/tests/RoleMiddlewareTest.php b/tests/RoleMiddlewareTest.php index 0266570e9..75a576bdd 100644 --- a/tests/RoleMiddlewareTest.php +++ b/tests/RoleMiddlewareTest.php @@ -23,7 +23,8 @@ public function setUp(): void /** @test */ public function a_guest_cannot_access_a_route_protected_by_rolemiddleware() { - $this->assertEquals(403, + $this->assertEquals( + 403, $this->runMiddleware($this->roleMiddleware, 'testRole') ); } @@ -35,7 +36,8 @@ public function a_user_cannot_access_a_route_protected_by_role_middleware_of_ano $this->testUser->assignRole('testRole'); - $this->assertEquals(403, + $this->assertEquals( + 403, $this->runMiddleware($this->roleMiddleware, 'testAdminRole') ); } @@ -47,7 +49,8 @@ public function a_user_can_access_a_route_protected_by_role_middleware_if_have_t $this->testUser->assignRole('testRole'); - $this->assertEquals(200, + $this->assertEquals( + 200, $this->runMiddleware($this->roleMiddleware, 'testRole') ); } @@ -59,11 +62,13 @@ public function a_user_can_access_a_route_protected_by_this_role_middleware_if_h $this->testUser->assignRole('testRole'); - $this->assertEquals(200, + $this->assertEquals( + 200, $this->runMiddleware($this->roleMiddleware, 'testRole|testRole2') ); - $this->assertEquals(200, + $this->assertEquals( + 200, $this->runMiddleware($this->roleMiddleware, ['testRole2', 'testRole']) ); } @@ -75,7 +80,8 @@ public function a_user_cannot_access_a_route_protected_by_the_role_middleware_if $this->testUser->assignRole(['testRole']); - $this->assertEquals(403, + $this->assertEquals( + 403, $this->runMiddleware($this->roleMiddleware, 'testRole2') ); } @@ -85,7 +91,8 @@ public function a_user_cannot_access_a_route_protected_by_role_middleware_if_hav { Auth::login($this->testUser); - $this->assertEquals(403, + $this->assertEquals( + 403, $this->runMiddleware($this->roleMiddleware, 'testRole|testRole2') ); } @@ -95,7 +102,8 @@ public function a_user_cannot_access_a_route_protected_by_role_middleware_if_rol { Auth::login($this->testUser); - $this->assertEquals(403, + $this->assertEquals( + 403, $this->runMiddleware($this->roleMiddleware, '') ); } @@ -141,7 +149,8 @@ public function user_can_not_access_role_with_guard_admin_while_using_default_gu $this->testUser->assignRole('testRole'); - $this->assertEquals(403, + $this->assertEquals( + 403, $this->runMiddleware($this->roleMiddleware, 'testRole', 'admin') ); } @@ -153,7 +162,8 @@ public function user_can_access_role_with_guard_admin_while_using_default_guard( $this->testAdmin->assignRole('testAdminRole'); - $this->assertEquals(200, + $this->assertEquals( + 200, $this->runMiddleware($this->roleMiddleware, 'testAdminRole', 'admin') ); } diff --git a/tests/RoleOrPermissionMiddlewareTest.php b/tests/RoleOrPermissionMiddlewareTest.php index 38fb558e1..da41c13fd 100644 --- a/tests/RoleOrPermissionMiddlewareTest.php +++ b/tests/RoleOrPermissionMiddlewareTest.php @@ -23,7 +23,8 @@ public function setUp(): void /** @test */ public function a_guest_cannot_access_a_route_protected_by_the_role_or_permission_middleware() { - $this->assertEquals(403, + $this->assertEquals( + 403, $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole') ); } @@ -36,24 +37,28 @@ public function a_user_can_access_a_route_protected_by_permission_or_role_middle $this->testUser->assignRole('testRole'); $this->testUser->givePermissionTo('edit-articles'); - $this->assertEquals(200, + $this->assertEquals( + 200, $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-news|edit-articles') ); $this->testUser->removeRole('testRole'); - $this->assertEquals(200, + $this->assertEquals( + 200, $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles') ); $this->testUser->revokePermissionTo('edit-articles'); $this->testUser->assignRole('testRole'); - $this->assertEquals(200, + $this->assertEquals( + 200, $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles') ); - $this->assertEquals(200, + $this->assertEquals( + 200, $this->runMiddleware($this->roleOrPermissionMiddleware, ['testRole', 'edit-articles']) ); } @@ -63,11 +68,13 @@ public function a_user_can_not_access_a_route_protected_by_permission_or_role_mi { Auth::login($this->testUser); - $this->assertEquals(403, + $this->assertEquals( + 403, $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles') ); - $this->assertEquals(403, + $this->assertEquals( + 403, $this->runMiddleware($this->roleOrPermissionMiddleware, 'missingRole|missingPermission') ); } @@ -96,7 +103,8 @@ public function user_can_not_access_permission_or_role_with_guard_admin_while_us $this->testUser->assignRole('testRole'); $this->testUser->givePermissionTo('edit-articles'); - $this->assertEquals(403, + $this->assertEquals( + 403, $this->runMiddleware($this->roleOrPermissionMiddleware, 'edit-articles|testRole', 'admin') ); } @@ -109,7 +117,8 @@ public function user_can_access_permission_or_role_with_guard_admin_while_using_ $this->testAdmin->assignRole('testAdminRole'); $this->testAdmin->givePermissionTo('admin-permission'); - $this->assertEquals(200, + $this->assertEquals( + 200, $this->runMiddleware($this->roleOrPermissionMiddleware, 'admin-permission|testAdminRole', 'admin') ); } diff --git a/tests/WildcardMiddlewareTest.php b/tests/WildcardMiddlewareTest.php index b3b2b5e08..9fe52796d 100644 --- a/tests/WildcardMiddlewareTest.php +++ b/tests/WildcardMiddlewareTest.php @@ -33,7 +33,8 @@ public function setUp(): void /** @test */ public function a_guest_cannot_access_a_route_protected_by_the_permission_middleware() { - $this->assertEquals(403, + $this->assertEquals( + 403, $this->runMiddleware($this->permissionMiddleware, 'articles.edit') ); } @@ -47,7 +48,8 @@ public function a_user_can_access_a_route_protected_by_permission_middleware_if_ $this->testUser->givePermissionTo('articles'); - $this->assertEquals(200, + $this->assertEquals( + 200, $this->runMiddleware($this->permissionMiddleware, 'articles.edit') ); } @@ -61,11 +63,13 @@ public function a_user_can_access_a_route_protected_by_this_permission_middlewar $this->testUser->givePermissionTo('articles.*.test'); - $this->assertEquals(200, + $this->assertEquals( + 200, $this->runMiddleware($this->permissionMiddleware, 'news.edit|articles.create.test') ); - $this->assertEquals(200, + $this->assertEquals( + 200, $this->runMiddleware($this->permissionMiddleware, ['news.edit', 'articles.create.test']) ); } @@ -79,7 +83,8 @@ public function a_user_cannot_access_a_route_protected_by_the_permission_middlew $this->testUser->givePermissionTo('articles.*'); - $this->assertEquals(403, + $this->assertEquals( + 403, $this->runMiddleware($this->permissionMiddleware, 'news.edit') ); } @@ -89,7 +94,8 @@ public function a_user_cannot_access_a_route_protected_by_permission_middleware_ { Auth::login($this->testUser); - $this->assertEquals(403, + $this->assertEquals( + 403, $this->runMiddleware($this->permissionMiddleware, 'articles.edit|news.edit') ); } @@ -104,24 +110,28 @@ public function a_user_can_access_a_route_protected_by_permission_or_role_middle $this->testUser->assignRole('testRole'); $this->testUser->givePermissionTo('articles.*'); - $this->assertEquals(200, + $this->assertEquals( + 200, $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|news.edit|articles.create') ); $this->testUser->removeRole('testRole'); - $this->assertEquals(200, + $this->assertEquals( + 200, $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|articles.edit') ); $this->testUser->revokePermissionTo('articles.*'); $this->testUser->assignRole('testRole'); - $this->assertEquals(200, + $this->assertEquals( + 200, $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|articles.edit') ); - $this->assertEquals(200, + $this->assertEquals( + 200, $this->runMiddleware($this->roleOrPermissionMiddleware, ['testRole', 'articles.edit']) ); } From f2892b126e0102bf1bafe583890f46e8df356040 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 27 Nov 2020 00:43:03 -0500 Subject: [PATCH 0364/1013] Refactor Middleware tests to separate files --- tests/RoleMiddlewareTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/RoleMiddlewareTest.php b/tests/RoleMiddlewareTest.php index 75a576bdd..991e73597 100644 --- a/tests/RoleMiddlewareTest.php +++ b/tests/RoleMiddlewareTest.php @@ -9,7 +9,7 @@ use Spatie\Permission\Exceptions\UnauthorizedException; use Spatie\Permission\Middlewares\RoleMiddleware; -class MiddlewareTest extends TestCase +class RoleMiddlewareTest extends TestCase { protected $roleMiddleware; From ea1ccc0128d53554b76034f2c1dc00aba2f52b9a Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 27 Nov 2020 00:55:27 -0500 Subject: [PATCH 0365/1013] Tidying --- src/Guard.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Guard.php b/src/Guard.php index df63b432e..9875aca3e 100644 --- a/src/Guard.php +++ b/src/Guard.php @@ -2,6 +2,7 @@ namespace Spatie\Permission; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; class Guard @@ -9,7 +10,8 @@ class Guard /** * return collection of (guard_name) property if exist on class or object * otherwise will return collection of guards names that exists in config/auth.php. - * @param $model + * + * @param string|Model $model model class object or name * @return Collection */ public static function getNames($model): Collection @@ -35,7 +37,7 @@ public static function getNames($model): Collection return collect(config('auth.guards')) ->map(function ($guard) { if (! isset($guard['provider'])) { - return; + return null; } return config("auth.providers.{$guard['provider']}.model"); @@ -46,6 +48,12 @@ public static function getNames($model): Collection ->keys(); } + /** + * Lookup a guard name relevant for the $class model and the current user. + * + * @param string|Model $class model class object or name + * @return string guard name + */ public static function getDefaultName($class): string { $default = config('auth.defaults.guard'); From db4a4dbc5f1a3c394a67564f490e67f2a04135e1 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 27 Nov 2020 01:03:37 -0500 Subject: [PATCH 0366/1013] Refactor MultipleGuardsTest --- tests/Manager.php | 2 +- tests/MultipleGuardsTest.php | 64 +++++++++++++++++++++++------------- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/tests/Manager.php b/tests/Manager.php index 9bd21928d..b1be89b7c 100644 --- a/tests/Manager.php +++ b/tests/Manager.php @@ -23,7 +23,7 @@ class Manager extends Model implements AuthorizableContract, AuthenticatableCont // When present, it takes precedence over the $guard_name property. public function guardName() { - return 'api'; + return 'jwt'; } // intentionally different property value for the sake of unit tests diff --git a/tests/MultipleGuardsTest.php b/tests/MultipleGuardsTest.php index 1bcc0de2d..066d0acf0 100644 --- a/tests/MultipleGuardsTest.php +++ b/tests/MultipleGuardsTest.php @@ -2,52 +2,70 @@ namespace Spatie\Permission\Test; -use Spatie\Permission\Models\Permission; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Route; +use Spatie\Permission\Contracts\Permission; class MultipleGuardsTest extends TestCase { + protected function getEnvironmentSetUp($app) + { + parent::getEnvironmentSetUp($app); + + $app['config']->set('auth.guards', [ + 'web' => ['driver' => 'session', 'provider' => 'users'], + 'api' => ['driver' => 'token', 'provider' => 'users'], + 'jwt' => ['driver' => 'token', 'provider' => 'users'], + 'abc' => ['driver' => 'abc'], + ]); + + $this->setUpRoutes(); + } + + /** + * Create routes to test authentication with guards. + */ + public function setUpRoutes(): void + { + Route::middleware('auth:api')->get('/check-api-guard-permission', function (Request $request) { + return [ + 'status' => $request->user()->checkPermissionTo('use_api_guard'), + ]; + }); + } + /** @test */ public function it_can_give_a_permission_to_a_model_that_is_used_by_multiple_guards() { - $this->testUser->givePermissionTo(Permission::create([ + $this->testUser->givePermissionTo(app(Permission::class)::create([ 'name' => 'do_this', 'guard_name' => 'web', ])); - $this->testUser->givePermissionTo(Permission::create([ + $this->testUser->givePermissionTo(app(Permission::class)::create([ 'name' => 'do_that', 'guard_name' => 'api', ])); - $this->assertTrue($this->testUser->hasPermissionTo('do_this', 'web')); - $this->assertTrue($this->testUser->hasPermissionTo('do_that', 'api')); - } - - protected function getEnvironmentSetUp($app) - { - parent::getEnvironmentSetUp($app); - - $app['config']->set('auth.guards', [ - 'web' => ['driver' => 'session', 'provider' => 'users'], - 'api' => ['driver' => 'jwt', 'provider' => 'users'], - 'abc' => ['driver' => 'abc'], - ]); + $this->assertTrue($this->testUser->checkPermissionTo('do_this', 'web')); + $this->assertTrue($this->testUser->checkPermissionTo('do_that', 'api')); + $this->assertFalse($this->testUser->checkPermissionTo('do_that', 'web')); } /** @test */ public function it_can_honour_guardName_function_on_model_for_overriding_guard_name_property() { $user = Manager::create(['email' => 'manager@test.com']); - $user->givePermissionTo(Permission::create([ - 'name' => 'do_that', - 'guard_name' => 'api', + $user->givePermissionTo(app(Permission::class)::create([ + 'name' => 'do_jwt', + 'guard_name' => 'jwt', ])); - // Manager test user has the guardName override method, which returns 'api' - $this->assertTrue($user->checkPermissionTo('do_that', 'api')); - $this->assertTrue($user->hasPermissionTo('do_that', 'api')); + // Manager test user has the guardName override method, which returns 'jwt' + $this->assertTrue($user->checkPermissionTo('do_jwt', 'jwt')); + $this->assertTrue($user->hasPermissionTo('do_jwt', 'jwt')); // Manager test user has the $guard_name property set to 'web' - $this->assertFalse($user->checkPermissionTo('do_that', 'web')); + $this->assertFalse($user->checkPermissionTo('do_jwt', 'web')); } } From 93063cd85028e99adb9cd4278c89c85441455147 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 30 Nov 2020 19:11:17 -0500 Subject: [PATCH 0367/1013] Update extending.md --- docs/advanced-usage/extending.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/advanced-usage/extending.md b/docs/advanced-usage/extending.md index 0396fb5b2..ddd40e4f4 100644 --- a/docs/advanced-usage/extending.md +++ b/docs/advanced-usage/extending.md @@ -44,6 +44,12 @@ If you need to REPLACE the existing `Role` or `Permission` models you need to ke - You need to update `config/permisison.php` to specify your namespaced model -## Migrations - Adding fields to your models +## Adding fields to your models You can add your own migrations to make changes to the role/permission tables, as you would for adding/changing fields in any other tables in your Laravel project. + Following that, you can add any necessary logic for interacting with those fields into your custom/extended Models. + +Related article: [Adding Extra Fields To Pivot Table](https://quickadminpanel.com/blog/laravel-belongstomany-add-extra-fields-to-pivot-table/) (video) + + + From 089f51dd77def7202de929e3cb1bfa23c48e1432 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 30 Nov 2020 19:11:54 -0500 Subject: [PATCH 0368/1013] Update phpstorm.md --- docs/advanced-usage/phpstorm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-usage/phpstorm.md b/docs/advanced-usage/phpstorm.md index e851b0820..e9ba8be5a 100644 --- a/docs/advanced-usage/phpstorm.md +++ b/docs/advanced-usage/phpstorm.md @@ -1,5 +1,5 @@ --- -title: Extending PhpStorm +title: PhpStorm Interaction weight: 7 --- From a3ff2c9116037953df4510c171701d9d080f79d5 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 30 Nov 2020 19:17:37 -0500 Subject: [PATCH 0369/1013] Update extending.md --- docs/advanced-usage/extending.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/advanced-usage/extending.md b/docs/advanced-usage/extending.md index ddd40e4f4..dc3e12314 100644 --- a/docs/advanced-usage/extending.md +++ b/docs/advanced-usage/extending.md @@ -34,14 +34,14 @@ If you need to EXTEND the existing `Role` or `Permission` models note that: - Your `Role` model needs to extend the `Spatie\Permission\Models\Role` model - Your `Permission` model needs to extend the `Spatie\Permission\Models\Permission` model -- You need to update `config/permisison.php` to specify your namespaced model +- You need to update `config/permisssion.php` to specify your namespaced model ### Replacing If you need to REPLACE the existing `Role` or `Permission` models you need to keep the following things in mind: - Your `Role` model needs to implement the `Spatie\Permission\Contracts\Role` contract - Your `Permission` model needs to implement the `Spatie\Permission\Contracts\Permission` contract -- You need to update `config/permisison.php` to specify your namespaced model +- You need to update `config/permission.php` to specify your namespaced model ## Adding fields to your models From acdfc6f66d880b1cd6a309750cdd7143095689bd Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 30 Nov 2020 19:37:34 -0500 Subject: [PATCH 0370/1013] Update extending.md --- docs/advanced-usage/extending.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-usage/extending.md b/docs/advanced-usage/extending.md index dc3e12314..6f1068a5e 100644 --- a/docs/advanced-usage/extending.md +++ b/docs/advanced-usage/extending.md @@ -34,7 +34,7 @@ If you need to EXTEND the existing `Role` or `Permission` models note that: - Your `Role` model needs to extend the `Spatie\Permission\Models\Role` model - Your `Permission` model needs to extend the `Spatie\Permission\Models\Permission` model -- You need to update `config/permisssion.php` to specify your namespaced model +- You need to update `config/permission.php` to specify your namespaced model ### Replacing If you need to REPLACE the existing `Role` or `Permission` models you need to keep the following things in mind: From f84823fd2fe2dd9f4cb9c211624045ed0de80456 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 30 Nov 2020 19:38:54 -0500 Subject: [PATCH 0371/1013] Update phpstorm.md --- docs/advanced-usage/phpstorm.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/advanced-usage/phpstorm.md b/docs/advanced-usage/phpstorm.md index e9ba8be5a..435680b72 100644 --- a/docs/advanced-usage/phpstorm.md +++ b/docs/advanced-usage/phpstorm.md @@ -3,7 +3,9 @@ title: PhpStorm Interaction weight: 7 --- -# Extending PhpStorm to support Blade Directives of this package +# Extending PhpStorm + +You may wish to extend PhpStorm to support Blade Directives of this package. 1. In PhpStorm, open Preferences, and navigate to **Languages and Frameworks -> PHP -> Blade** (File | Settings | Languages & Frameworks | PHP | Blade) From 87dcf4a020e2fd077df85b8b8908a468cc3c7bc8 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 8 Dec 2020 12:24:55 -0500 Subject: [PATCH 0372/1013] Update role-permissions.md --- docs/basic-usage/role-permissions.md | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/basic-usage/role-permissions.md b/docs/basic-usage/role-permissions.md index 8486a0d65..e4d0875cd 100644 --- a/docs/basic-usage/role-permissions.md +++ b/docs/basic-usage/role-permissions.md @@ -1,8 +1,10 @@ --- -title: Using permissions via roles +title: Using Permissions via Roles weight: 3 --- +## Assigning Roles + A role can be assigned to any user: ```php @@ -27,6 +29,8 @@ Roles can also be synced: $user->syncRoles(['writer', 'admin']); ``` +## Checking Roles + You can determine if a user has a certain role: ```php @@ -53,6 +57,9 @@ $user->hasAllRoles(Role::all()); The `assignRole`, `hasRole`, `hasAnyRole`, `hasAllRoles` and `removeRole` functions can accept a string, a `\Spatie\Permission\Models\Role` object or an `\Illuminate\Support\Collection` object. + +## Assigning Permissions to Roles + A permission can be given to a role: ```php @@ -75,7 +82,11 @@ The `givePermissionTo` and `revokePermissionTo` functions can accept a string or a `Spatie\Permission\Models\Permission` object. -Permissions are inherited from roles automatically. +**Permissions are inherited from roles automatically.** + + +## Assigning Direct Permissions To A User + Additionally, individual permissions can be assigned to the user too. For instance: @@ -95,9 +106,13 @@ but `false` for `$user->hasDirectPermission('edit articles')`. This method is useful if one builds a form for setting permissions for roles and users in an application and wants to restrict or change inherited permissions of roles of the user, i.e. allowing to change only direct permissions of the user. -You can check if the user has All or Any of a set of permissions directly assigned: + +You can check if the user has a Specific or All or Any of a set of permissions directly assigned: ```php +// Check if the user has Direct permission +$user->hasDirectPermission('edit articles') + // Check if the user has All direct permissions $user->hasAllDirectPermissions(['edit articles', 'delete articles']); @@ -110,7 +125,7 @@ When we call `$user->hasAnyDirectPermission('edit articles')`, it returns `true` because the user has one of the provided permissions. -You can list all of these permissions: +You can examine all of these permissions: ```php // Direct permissions From 3951ef5c362914a18323b6d9507faf859392186a Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 16 Dec 2020 11:32:34 -0500 Subject: [PATCH 0373/1013] Add test showing that permission middleware tests for non-direct permissions as well It already had tests for direct permissions. This just adds a test for non-direct (eg: via roles) --- tests/PermissionMiddlewareTest.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/PermissionMiddlewareTest.php b/tests/PermissionMiddlewareTest.php index dafb1eec1..fa187f78f 100644 --- a/tests/PermissionMiddlewareTest.php +++ b/tests/PermissionMiddlewareTest.php @@ -123,6 +123,25 @@ public function a_user_cannot_access_a_route_protected_by_permission_middleware_ ); } + /** @test */ + public function a_user_can_access_a_route_protected_by_permission_middleware_if_has_permission_via_role() + { + Auth::login($this->testUser); + + $this->assertEquals( + 403, + $this->runMiddleware($this->permissionMiddleware, 'edit-articles') + ); + + $this->testUserRole->givePermissionTo('edit-articles'); + $this->testUser->assignRole('testRole'); + + $this->assertEquals( + 200, + $this->runMiddleware($this->permissionMiddleware, 'edit-articles') + ); + } + /** @test */ public function the_required_permissions_can_be_fetched_from_the_exception() { From c3937f0bf3a3f7fa3d92c9b6b01608e7eadb4fe1 Mon Sep 17 00:00:00 2001 From: Gaurav Makhecha Date: Thu, 31 Dec 2020 13:04:56 +0530 Subject: [PATCH 0374/1013] update: Text --- docs/introduction.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/introduction.md b/docs/introduction.md index c1bed6dc2..f3dc122d3 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -17,7 +17,7 @@ $user->assignRole('writer'); $role->givePermissionTo('edit articles'); ``` -If you're using multiple guards we've got you covered as well. Every guard will have its own set of permissions and roles that can be assigned to the guard's users. Read about it in the [using multiple guards](https://docs.spatie.be/laravel-permission/v3/basic-usage/multiple-guards/) section of the readme. +If you're using multiple guards we've got you covered as well. Every guard will have its own set of permissions and roles that can be assigned to the guard's users. Read about it in the [using multiple guards](https://docs.spatie.be/laravel-permission/v3/basic-usage/multiple-guards/) section. Because all permissions will be registered on [Laravel's gate](https://laravel.com/docs/authorization), you can check if a user has a permission with Laravel's default `can` function: @@ -31,4 +31,4 @@ and Blade directives: @can('edit articles') ... @endcan -``` \ No newline at end of file +``` From 1ba2a28f55c4cd8e891773248877fca1cbdfebbf Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 13 Jan 2021 10:31:30 +0000 Subject: [PATCH 0375/1013] Fix bug when adding roles to a model that doesn't yet exist --- src/Traits/HasRoles.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 55376d4b4..9abf8906d 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -124,9 +124,9 @@ function ($object) use ($roles, $model) { if ($modelLastFiredOn !== null && $modelLastFiredOn === $model) { return; } - $object->roles()->sync($roles, false); - $object->load('roles'); - $modelLastFiredOn = $object; + $model->roles()->sync($roles, false); + $model->load('roles'); + $modelLastFiredOn = $model; } ); } From bd8ec48e5f9eb24df5441c371e13a5b9a4965c63 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 20 Jan 2021 08:39:17 +0000 Subject: [PATCH 0376/1013] Apply the same fix to the HasPermissions trait --- src/Traits/HasPermissions.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 532e815ce..fe6bc3c7e 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -352,9 +352,9 @@ function ($object) use ($permissions, $model) { if ($modelLastFiredOn !== null && $modelLastFiredOn === $model) { return; } - $object->permissions()->sync($permissions, false); - $object->load('permissions'); - $modelLastFiredOn = $object; + $model->permissions()->sync($permissions, false); + $model->load('permissions'); + $modelLastFiredOn = $model; } ); } From 33dedec04265a6be22fdea70a3b3b8bc5efa5934 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 20 Jan 2021 08:52:38 +0000 Subject: [PATCH 0377/1013] Update the HasRolesTest to cover the bug --- tests/HasRolesTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index bd4c58395..be20b73e1 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -247,6 +247,9 @@ public function calling_syncRoles_before_saving_object_doesnt_interfere_with_oth $user2->syncRoles('testRole2'); $user2->save(); + $this->assertTrue($user->fresh()->hasRole('testRole')); + $this->assertFalse($user->fresh()->hasRole('testRole2')); + $this->assertTrue($user2->fresh()->hasRole('testRole2')); $this->assertFalse($user2->fresh()->hasRole('testRole')); } @@ -262,6 +265,9 @@ public function calling_assignRole_before_saving_object_doesnt_interfere_with_ot $admin_user->assignRole('testRole2'); $admin_user->save(); + $this->assertTrue($user->fresh()->hasRole('testRole')); + $this->assertFalse($user->fresh()->hasRole('testRole2')); + $this->assertTrue($admin_user->fresh()->hasRole('testRole2')); $this->assertFalse($admin_user->fresh()->hasRole('testRole')); } From 8f02175c7a974c1ee21fd2edc9a249b934d8d599 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 20 Jan 2021 08:55:07 +0000 Subject: [PATCH 0378/1013] Update the HasPermissionsTest to cover the bug --- tests/HasPermissionsTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index 8ef5c4714..c772bda47 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -476,6 +476,9 @@ public function calling_givePermissionTo_before_saving_object_doesnt_interfere_w $user2->givePermissionTo('edit-articles'); $user2->save(); + $this->assertTrue($user->fresh()->hasPermissionTo('edit-news')); + $this->assertFalse($user->fresh()->hasPermissionTo('edit-articles')); + $this->assertTrue($user2->fresh()->hasPermissionTo('edit-articles')); $this->assertFalse($user2->fresh()->hasPermissionTo('edit-news')); } @@ -491,6 +494,9 @@ public function calling_syncPermissions_before_saving_object_doesnt_interfere_wi $user2->syncPermissions('edit-articles'); $user2->save(); + $this->assertTrue($user->fresh()->hasPermissionTo('edit-news')); + $this->assertFalse($user->fresh()->hasPermissionTo('edit-articles')); + $this->assertTrue($user2->fresh()->hasPermissionTo('edit-articles')); $this->assertFalse($user2->fresh()->hasPermissionTo('edit-news')); } From df4e9c66fc2cc0bfc74c0261ffe0101c9988b81d Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 21 Jan 2021 10:35:35 -0500 Subject: [PATCH 0379/1013] Updates #975 to avoid static var and fix #1663 By avoiding the temporary/static variable here, and using $model directly, the detection of persisted object is more accurate. --- src/Traits/HasPermissions.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index fe6bc3c7e..4b7d6a3a5 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -348,13 +348,8 @@ public function givePermissionTo(...$permissions) $class::saved( function ($object) use ($permissions, $model) { - static $modelLastFiredOn; - if ($modelLastFiredOn !== null && $modelLastFiredOn === $model) { - return; - } $model->permissions()->sync($permissions, false); $model->load('permissions'); - $modelLastFiredOn = $model; } ); } From 224fac226c9b404546c360a829fc0ee990c30be1 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 21 Jan 2021 10:36:13 -0500 Subject: [PATCH 0380/1013] Updates #975 to avoid static var and fix #1663 By avoiding the temporary/static variable here, and using $model directly, the detection of persisted object is more accurate. --- src/Traits/HasRoles.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 9abf8906d..cb1086523 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -120,13 +120,8 @@ public function assignRole(...$roles) $class::saved( function ($object) use ($roles, $model) { - static $modelLastFiredOn; - if ($modelLastFiredOn !== null && $modelLastFiredOn === $model) { - return; - } $model->roles()->sync($roles, false); $model->load('roles'); - $modelLastFiredOn = $model; } ); } From 4b9f076be3f1239fd96834e820aa76989f707c67 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 27 Jan 2021 17:45:03 -0500 Subject: [PATCH 0381/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd9f63c90..e954f0f7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 3.18.0 - 2020-11-27 +- Allow PHP 8.0 + ## 3.17.0 - 2020-09-16 - Optional `$guard` parameter may be passed to `RoleMiddleware`, `PermissionMiddleware`, and `RoleOrPermissionMiddleware`. See #1565 From 7936ea9ebbd0d91877109dfeda1659fc4570a6cc Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 27 Jan 2021 18:03:34 -0500 Subject: [PATCH 0382/1013] Update CHANGELOG.md --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e954f0f7a..82e3b4a18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to `laravel-permission` will be documented in this file +## 4.0.0 - 2021-01-27 +- Drop support on Laravel 5.8 #1615 +- Fix bug when adding roles to a model that doesn't yet exist #1663 +- Enforce unique constraints on database level #1261 +- Changed PermissionRegistrar::initializeCache() public to allow reinitializing cache in custom situations. #1521 +- Use Eloquent\Collection instead of Support\Collection for consistency, collection merging, etc #1630 + +This package now requires PHP 7.2.5 and Laravel 6.0 or higher. +If you are on a PHP version below 7.2.5 or a Laravel version below 6.0 you can use an older version of this package. + ## 3.18.0 - 2020-11-27 - Allow PHP 8.0 From 099ec8e6584669308e6cddeadafacf61eae2c8b0 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Thu, 28 Jan 2021 08:34:48 +0100 Subject: [PATCH 0383/1013] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 278e3e3a8..0aa6e4479 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ We publish all received postcards [on our company website](https://spatie.be/en/ ## Credits +- [Chris Brown](https://github.com/drbyte) - [Freek Van der Herten](https://github.com/freekmurze) - [All Contributors](../../contributors) From 8dca397be04ad7a2b489034be42a1f8cc312df24 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Thu, 28 Jan 2021 08:39:47 +0100 Subject: [PATCH 0384/1013] Update _index.md --- docs/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_index.md b/docs/_index.md index f6aefce38..390f0bb4e 100644 --- a/docs/_index.md +++ b/docs/_index.md @@ -1,5 +1,5 @@ --- -title: v3 +title: v4 slogan: Associate users with roles and permissions githubUrl: https://github.com/spatie/laravel-permission branch: master From f9518f6efc810bd1825fad6cb95258605646e257 Mon Sep 17 00:00:00 2001 From: freek Date: Thu, 28 Jan 2021 08:44:29 +0100 Subject: [PATCH 0385/1013] wip --- .github/ISSUE_TEMPLATE/bug_report.md | 34 ++++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 11 +++++++++ 2 files changed, 45 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..3084e278a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,34 @@ +--- +name: Bug report +about: Report or reproducable bug +title: '' +labels: '' +assignees: '' + +--- + +**Before creating a new bug report** +Please check if there isn't a similar issue on [the issue tracker](https://github.com/spatie/laravel-persmission/issues) or in [the discussions](https://github.com/spatie/laravel-permission/discussions). + +**Describe the bug** +A clear and concise description of what the bug is. + +**Versions** +You can use `composer show` to get the version numbers of: +- spatie/laravel-permission package version: +- illuminate/framework package + +PHP version: + +**To Reproduce** +Steps to reproduce the behavior: + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Desktop (please complete the following information):** + - OS: [e.g. macOS] + - Version [e.g. 22] + + **Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..eb06ccd7f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Ask a Question + url: https://github.com/spatie/laravel-permission/discussions/new?category=q-a + about: Ask the community for help + - name: Feature Request + url: https://github.com/spatie/laravel-permission/discussions/new?category=ideas + about: Share ideas for new features + - name: Bug Report + url: https://github.com/spatie/laravel-permission/issues/new + about: Report a reproducable bug From d3568dad67c826765f9fed80d2a1ab51e95bf0c4 Mon Sep 17 00:00:00 2001 From: freek Date: Thu, 28 Jan 2021 08:45:15 +0100 Subject: [PATCH 0386/1013] wip --- .github/ISSUE_TEMPLATE/config.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index eb06ccd7f..5940c1979 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -6,6 +6,3 @@ contact_links: - name: Feature Request url: https://github.com/spatie/laravel-permission/discussions/new?category=ideas about: Share ideas for new features - - name: Bug Report - url: https://github.com/spatie/laravel-permission/issues/new - about: Report a reproducable bug From 7f4b19557d4646320f1218720dc49b91eba7cc52 Mon Sep 17 00:00:00 2001 From: freek Date: Thu, 28 Jan 2021 08:49:56 +0100 Subject: [PATCH 0387/1013] wip --- .github/SECURITY.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/SECURITY.md diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 000000000..ca9134343 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,3 @@ +# Security Policy + +If you discover any security related issues, please email freek@spatie.be instead of using the issue tracker. From d110964cb7d481becd90b3465d77b316cd81be6c Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 2 Feb 2021 12:47:24 -0500 Subject: [PATCH 0388/1013] Note about filesystem ownership on cache files --- docs/advanced-usage/cache.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/advanced-usage/cache.md b/docs/advanced-usage/cache.md index 492bd11b5..1272b7ea6 100644 --- a/docs/advanced-usage/cache.md +++ b/docs/advanced-usage/cache.md @@ -71,3 +71,12 @@ In `config/permission.php` set `cache.store` to the name of any one of the `conf Setting `'cache.store' => 'array'` in `config/permission.php` will effectively disable caching by this package between requests (it will only cache in-memory until the current request is completed processing, never persisting it). Alternatively, in development mode you can bypass ALL of Laravel's caching between visits by setting `CACHE_DRIVER=array` in `.env`. You can see an example of this in the default `phpunit.xml` file that comes with a new Laravel install. Of course, don't do this in production though! + + +## File cache driver + +This situation is not specific to this package, but is mentioned here due to the common question being asked. + +If you are using the `File` cache driver and run into problems clearing the cache, it is most likely because your filesystem's permissions are preventing the PHP CLI from altering the cache files because the PHP-FPM process is running as a different user. + +Work with your server administrator to fix filesystem ownership on your cache files. From d5200af82334e300188bf64a6502c528d75c242c Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 23 Feb 2021 16:02:54 -0500 Subject: [PATCH 0389/1013] Update bug_report.md --- .github/ISSUE_TEMPLATE/bug_report.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 3084e278a..f4c3a68a3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -20,6 +20,9 @@ You can use `composer show` to get the version numbers of: PHP version: +Database version: + + **To Reproduce** Steps to reproduce the behavior: From 47f3706a10300239454c0ea995929404469eec89 Mon Sep 17 00:00:00 2001 From: Tiago Lucas Flach Date: Tue, 23 Feb 2021 20:39:31 -0300 Subject: [PATCH 0390/1013] Update role-permission.md Removed a duplicate paragraph --- docs/basic-usage/role-permissions.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/basic-usage/role-permissions.md b/docs/basic-usage/role-permissions.md index e4d0875cd..bbfd85d4d 100644 --- a/docs/basic-usage/role-permissions.md +++ b/docs/basic-usage/role-permissions.md @@ -140,11 +140,6 @@ $user->getAllPermissions(); All these responses are collections of `Spatie\Permission\Models\Permission` objects. - - -If we follow the previous example, the first response will be a collection with the `delete article` permission and -the second will be a collection with the `edit article` permission and the third will contain both. - If we follow the previous example, the first response will be a collection with the `delete article` permission and the second will be a collection with the `edit article` permission and the third will contain both. From 6a66dea9af1712e9e457f1a38a98f2013d9f012d Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 24 Feb 2021 13:39:56 -0500 Subject: [PATCH 0391/1013] Update extending.md --- docs/advanced-usage/extending.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/advanced-usage/extending.md b/docs/advanced-usage/extending.md index 6f1068a5e..5fd067925 100644 --- a/docs/advanced-usage/extending.md +++ b/docs/advanced-usage/extending.md @@ -32,13 +32,28 @@ Note the following requirements when extending/replacing the models: ### Extending If you need to EXTEND the existing `Role` or `Permission` models note that: -- Your `Role` model needs to extend the `Spatie\Permission\Models\Role` model -- Your `Permission` model needs to extend the `Spatie\Permission\Models\Permission` model +- Your `Role` model needs to `extend` the `Spatie\Permission\Models\Role` model +- Your `Permission` model needs to `extend` the `Spatie\Permission\Models\Permission` model - You need to update `config/permission.php` to specify your namespaced model +eg: +```php + Date: Thu, 4 Mar 2021 12:32:03 -0300 Subject: [PATCH 0392/1013] hasExactRoles method added --- docs/basic-usage/role-permissions.md | 8 +++++++- src/Traits/HasRoles.php | 28 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/docs/basic-usage/role-permissions.md b/docs/basic-usage/role-permissions.md index bbfd85d4d..8d1cb4350 100644 --- a/docs/basic-usage/role-permissions.md +++ b/docs/basic-usage/role-permissions.md @@ -54,7 +54,13 @@ You can also determine if a user has all of a given list of roles: $user->hasAllRoles(Role::all()); ``` -The `assignRole`, `hasRole`, `hasAnyRole`, `hasAllRoles` and `removeRole` functions can accept a +You can also determine if a user has exactly all of a given list of roles: + +```php +$user->hasExactRoles(Role::all()); +``` + +The `assignRole`, `hasRole`, `hasAnyRole`, `hasAllRoles`, `hasExactRoles` and `removeRole` functions can accept a string, a `\Spatie\Permission\Models\Role` object or an `\Illuminate\Support\Collection` object. diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index cb1086523..f109fb0b1 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -251,6 +251,34 @@ public function hasAllRoles($roles, string $guard = null): bool ) == $roles; } + /** + * Determine if the model has exactly all of the given role(s). + * + * @param string|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles + * @param string|null $guard + * @return bool + */ + public function hasExactRoles($roles, string $guard = null): bool + { + if (is_string($roles) && false !== strpos($roles, '|')) { + $roles = $this->convertPipeToArray($roles); + } + + if (is_string($roles)) { + $roles = [$roles]; + } + + if ($roles instanceof Role) { + $roles = [$roles->name]; + } + + $roles = collect()->make($roles)->map(function ($role) { + return $role instanceof Role ? $role->name : $role; + }); + + return $this->roles->count() == $roles->count() && $this->hasAllRoles($roles, $guard); + } + /** * Return all permissions directly coupled to the model. */ From 3e42044ab6e8cec95e9b1a0bcb3ecfb470ed21cb Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 8 Mar 2021 00:55:39 -0500 Subject: [PATCH 0393/1013] Laravel 6 --- docs/prerequisites.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/prerequisites.md b/docs/prerequisites.md index 57d133439..377b91845 100644 --- a/docs/prerequisites.md +++ b/docs/prerequisites.md @@ -3,7 +3,7 @@ title: Prerequisites weight: 3 --- -This package can be used in Laravel 5.8 or higher. +This package can be used in Laravel 6 or higher. This package uses Laravel's Gate layer to provide Authorization capabilities. The Gate/authorization layer requires that your `User` model implement the `Illuminate\Contracts\Auth\Access\Authorizable` contract. From 835daa4a35971474ff5ccef0e3b20a4562f6acec Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 8 Mar 2021 01:01:14 -0500 Subject: [PATCH 0394/1013] Schema::defaultStringLength(125) for MySQL 8.0 --- docs/prerequisites.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/prerequisites.md b/docs/prerequisites.md index 377b91845..ddcafbdb6 100644 --- a/docs/prerequisites.md +++ b/docs/prerequisites.md @@ -3,8 +3,12 @@ title: Prerequisites weight: 3 --- +## Laravel Version + This package can be used in Laravel 6 or higher. +## User Model / Contract/Interface + This package uses Laravel's Gate layer to provide Authorization capabilities. The Gate/authorization layer requires that your `User` model implement the `Illuminate\Contracts\Auth\Access\Authorizable` contract. Otherwise the `can()` and `authorize()` methods will not work in your controllers, policies, templates, etc. @@ -25,9 +29,21 @@ class User extends Authenticatable } ``` +## Must not have a `role` or `roles` property, nor a `roles()` method + Additionally, your `User` model/object MUST NOT have a `role` or `roles` property (or field in the database), nor a `roles()` method on it. Those will interfere with the properties and methods added by the `HasRoles` trait provided by this package, thus causing unexpected outcomes when this package's methods are used to inspect roles and permissions. +## Must not have a `permission` or `permissions` property, nor a `permissions()` method + Similarly, your `User` model/object MUST NOT have a `permission` or `permissions` property (or field in the database), nor a `permissions()` method on it. Those will interfere with the properties and methods added by the `HasPermissions` trait provided by this package (which is invoked via the `HasRoles` trait). +## Config file + This package publishes a `config/permission.php` file. If you already have a file by that name, you must rename or remove it, as it will conflict with this package. You could optionally merge your own values with those required by this package, as long as the keys that this package expects are present. See the source file for more details. +## Schema Limitation in MySQL + +MySQL 8.0 limits index keys to 1000 characters. This package publishes a migration which combines multiple columns in single index. With `utf8mb4` the 4-bytes-per-character requirement of `mb4` means the max length of the columns in the hybrid index can only be `125` characters. + +Thus in your AppServiceProvider you will need to set `Schema::defaultStringLength(125)`. [See the Laravel Docs for instructions](https://laravel.com/docs/master/migrations#index-lengths-mysql-mariadb). + From b84ae203f68c203b0fac2a0828967ce01d8dc069 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 8 Mar 2021 01:05:15 -0500 Subject: [PATCH 0395/1013] Add comments for optional change required by MySQL 8.0 limitations --- database/migrations/create_permission_tables.php.stub | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/database/migrations/create_permission_tables.php.stub b/database/migrations/create_permission_tables.php.stub index 504f861d1..edf92e7a1 100644 --- a/database/migrations/create_permission_tables.php.stub +++ b/database/migrations/create_permission_tables.php.stub @@ -22,8 +22,8 @@ class CreatePermissionTables extends Migration Schema::create($tableNames['permissions'], function (Blueprint $table) { $table->bigIncrements('id'); - $table->string('name'); - $table->string('guard_name'); + $table->string('name'); // For MySQL 8.0 use string('name', 125); + $table->string('guard_name'); // For MySQL 8.0 use string('guard_name', 125); $table->timestamps(); $table->unique(['name', 'guard_name']); @@ -31,8 +31,8 @@ class CreatePermissionTables extends Migration Schema::create($tableNames['roles'], function (Blueprint $table) { $table->bigIncrements('id'); - $table->string('name'); - $table->string('guard_name'); + $table->string('name'); // For MySQL 8.0 use string('name', 125); + $table->string('guard_name'); // For MySQL 8.0 use string('guard_name', 125); $table->timestamps(); $table->unique(['name', 'guard_name']); From 6adaab5be0b671a80b1d647da1ed7ff0c7d0b01e Mon Sep 17 00:00:00 2001 From: kidii <2613451+kidii@users.noreply.github.com> Date: Sun, 21 Mar 2021 16:16:40 +0100 Subject: [PATCH 0396/1013] Update the documention link to v4 instead of v3 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0aa6e4479..c06c55a16 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ ## Documentation, Installation, and Usage Instructions -See the [DOCUMENTATION](https://docs.spatie.be/laravel-permission/v3/introduction/) for detailed installation and usage instructions. +See the [DOCUMENTATION](https://docs.spatie.be/laravel-permission/v4/introduction/) for detailed installation and usage instructions. ## What It Does This package allows you to manage user permissions and roles in a database. From 29c05324c170c0be108ccb86dd29f6a719c0a617 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 22 Mar 2021 14:38:29 -0400 Subject: [PATCH 0397/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82e3b4a18..3aeb1a822 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 4.0.1 - 2021-03-22 +- Added note in migration for field lengths on MySQL 8. (either shorten the columns to 125 or use InnoDB) + ## 4.0.0 - 2021-01-27 - Drop support on Laravel 5.8 #1615 - Fix bug when adding roles to a model that doesn't yet exist #1663 From 062ae10a79ebb5c4ea4bd25d9ed303c407fcb8f7 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 23 Mar 2021 17:25:17 -0400 Subject: [PATCH 0398/1013] Refactor to extract methods --- src/PermissionServiceProvider.php | 43 ++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index e9e20e660..724b42894 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -14,24 +14,11 @@ class PermissionServiceProvider extends ServiceProvider { public function boot(PermissionRegistrar $permissionLoader, Filesystem $filesystem) { - if (function_exists('config_path')) { // function not available and 'publish' not relevant in Lumen - $this->publishes([ - __DIR__.'/../config/permission.php' => config_path('permission.php'), - ], 'config'); - - $this->publishes([ - __DIR__.'/../database/migrations/create_permission_tables.php.stub' => $this->getMigrationFileName($filesystem, 'create_permission_tables.php'), - ], 'migrations'); - } + $this->offerPublishing(); $this->registerMacroHelpers(); - $this->commands([ - Commands\CacheReset::class, - Commands\CreateRole::class, - Commands\CreatePermission::class, - Commands\Show::class, - ]); + $this->registerCommands(); $this->registerModelBindings(); @@ -53,6 +40,32 @@ public function register() $this->registerBladeExtensions(); } + protected function offerPublishing() + { + if (!function_exists('config_path')) { + // function not available and 'publish' not relevant in Lumen + return; + } + + $this->publishes([ + __DIR__.'/../config/permission.php' => config_path('permission.php'), + ], 'config'); + + $this->publishes([ + __DIR__.'/../database/migrations/create_permission_tables.php.stub' => $this->getMigrationFileName($filesystem, 'create_permission_tables.php'), + ], 'migrations'); + } + + protected function registerCommands() + { + $this->commands([ + Commands\CacheReset::class, + Commands\CreateRole::class, + Commands\CreatePermission::class, + Commands\Show::class, + ]); + } + protected function registerModelBindings() { $config = $this->app->config['permission.models']; From 491be445ffc99d76b92a298011bc27431c5697bb Mon Sep 17 00:00:00 2001 From: drbyte Date: Tue, 23 Mar 2021 21:25:53 +0000 Subject: [PATCH 0399/1013] Fix styling --- src/PermissionServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 724b42894..b3abe94d9 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -42,7 +42,7 @@ public function register() protected function offerPublishing() { - if (!function_exists('config_path')) { + if (! function_exists('config_path')) { // function not available and 'publish' not relevant in Lumen return; } From b12159e61f94680be0a95ee5b50d9f1099040714 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 23 Mar 2021 22:28:59 -0400 Subject: [PATCH 0400/1013] Re-refactor to fix resolver --- src/PermissionServiceProvider.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index b3abe94d9..b9f17398a 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -12,7 +12,7 @@ class PermissionServiceProvider extends ServiceProvider { - public function boot(PermissionRegistrar $permissionLoader, Filesystem $filesystem) + public function boot(PermissionRegistrar $permissionLoader) { $this->offerPublishing(); @@ -52,7 +52,7 @@ protected function offerPublishing() ], 'config'); $this->publishes([ - __DIR__.'/../database/migrations/create_permission_tables.php.stub' => $this->getMigrationFileName($filesystem, 'create_permission_tables.php'), + __DIR__.'/../database/migrations/create_permission_tables.php.stub' => $this->getMigrationFileName('create_permission_tables.php'), ], 'migrations'); } @@ -167,13 +167,14 @@ protected function registerMacroHelpers() /** * Returns existing migration file if found, else uses the current timestamp. * - * @param Filesystem $filesystem * @return string */ - protected function getMigrationFileName(Filesystem $filesystem, $migrationFileName): string + protected function getMigrationFileName($migrationFileName): string { $timestamp = date('Y_m_d_His'); + $filesystem = $this->app->make(Filesystem::class); + return Collection::make($this->app->databasePath().DIRECTORY_SEPARATOR.'migrations'.DIRECTORY_SEPARATOR) ->flatMap(function ($path) use ($filesystem) { return $filesystem->glob($path.'*_create_permission_tables.php'); From 5cf582f57f7d2c1b965825d96498817eb3d34ffb Mon Sep 17 00:00:00 2001 From: abenerd Date: Sun, 28 Mar 2021 19:25:06 +0200 Subject: [PATCH 0401/1013] Retrieve guard only once in middlewares --- src/Middlewares/PermissionMiddleware.php | 6 ++++-- src/Middlewares/RoleMiddleware.php | 6 ++++-- src/Middlewares/RoleOrPermissionMiddleware.php | 5 +++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Middlewares/PermissionMiddleware.php b/src/Middlewares/PermissionMiddleware.php index f1eca5a9a..73dec2ef8 100644 --- a/src/Middlewares/PermissionMiddleware.php +++ b/src/Middlewares/PermissionMiddleware.php @@ -9,7 +9,9 @@ class PermissionMiddleware { public function handle($request, Closure $next, $permission, $guard = null) { - if (app('auth')->guard($guard)->guest()) { + $authGuard = app('auth')->guard($guard); + + if ($authGuard->guest()) { throw UnauthorizedException::notLoggedIn(); } @@ -18,7 +20,7 @@ public function handle($request, Closure $next, $permission, $guard = null) : explode('|', $permission); foreach ($permissions as $permission) { - if (app('auth')->guard($guard)->user()->can($permission)) { + if ($authGuard->user()->can($permission)) { return $next($request); } } diff --git a/src/Middlewares/RoleMiddleware.php b/src/Middlewares/RoleMiddleware.php index 2c679d5c9..34e91e241 100644 --- a/src/Middlewares/RoleMiddleware.php +++ b/src/Middlewares/RoleMiddleware.php @@ -10,7 +10,9 @@ class RoleMiddleware { public function handle($request, Closure $next, $role, $guard = null) { - if (Auth::guard($guard)->guest()) { + $authGuard = Auth::guard($guard); + + if ($authGuard->guest()) { throw UnauthorizedException::notLoggedIn(); } @@ -18,7 +20,7 @@ public function handle($request, Closure $next, $role, $guard = null) ? $role : explode('|', $role); - if (! Auth::guard($guard)->user()->hasAnyRole($roles)) { + if (! $authGuard->user()->hasAnyRole($roles)) { throw UnauthorizedException::forRoles($roles); } diff --git a/src/Middlewares/RoleOrPermissionMiddleware.php b/src/Middlewares/RoleOrPermissionMiddleware.php index f8d5fa3b8..b9149f2f6 100644 --- a/src/Middlewares/RoleOrPermissionMiddleware.php +++ b/src/Middlewares/RoleOrPermissionMiddleware.php @@ -10,7 +10,8 @@ class RoleOrPermissionMiddleware { public function handle($request, Closure $next, $roleOrPermission, $guard = null) { - if (Auth::guard($guard)->guest()) { + $authGuard = Auth::guard($guard); + if ($authGuard->guest()) { throw UnauthorizedException::notLoggedIn(); } @@ -18,7 +19,7 @@ public function handle($request, Closure $next, $roleOrPermission, $guard = null ? $roleOrPermission : explode('|', $roleOrPermission); - if (! Auth::guard($guard)->user()->hasAnyRole($rolesOrPermissions) && ! Auth::guard($guard)->user()->hasAnyPermission($rolesOrPermissions)) { + if (! $authGuard->user()->hasAnyRole($rolesOrPermissions) && ! $authGuard->user()->hasAnyPermission($rolesOrPermissions)) { throw UnauthorizedException::forRolesOrPermissions($rolesOrPermissions); } From 70d1b3a01a08b1ad64a04ed5f3972db677d53c44 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 28 Mar 2021 14:57:56 -0400 Subject: [PATCH 0402/1013] Add link to Lumen docs --- docs/installation-lumen.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/installation-lumen.md b/docs/installation-lumen.md index b2b350b51..4e7d85f7a 100644 --- a/docs/installation-lumen.md +++ b/docs/installation-lumen.md @@ -3,9 +3,11 @@ title: Installation in Lumen weight: 5 --- -NOTE: Lumen is not officially supported by this package. However, the following are some steps which may help get you started. +NOTE: Lumen is **not** officially supported by this package. However, the following are some steps which may help get you started. -First, install the package via Composer: +Lumen installation instructions can be found in the [Lumen documentation](https://lumen.laravel.com/docs/master). + +Install the permissions package via Composer: ``` bash composer require spatie/laravel-permission @@ -48,7 +50,7 @@ $app->register(Spatie\Permission\PermissionServiceProvider::class); $app->register(App\Providers\AuthServiceProvider::class); ``` -Ensure your database configuration is set in your `.env` (or `config/database.php` if you have one). +Ensure the application's database name/credentials are set in your `.env` (or `config/database.php` if you have one), and that the database exists. Run the migrations to create the tables for this package: From 90c0c930458facad1298baa24320fdd214652499 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 28 Mar 2021 15:07:22 -0400 Subject: [PATCH 0403/1013] Fix typo --- docs/installation-lumen.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation-lumen.md b/docs/installation-lumen.md index 4e7d85f7a..08881eb90 100644 --- a/docs/installation-lumen.md +++ b/docs/installation-lumen.md @@ -66,8 +66,8 @@ NOTE: Remember that Laravel's authorization layer requires that your `User` mode ### User Table NOTE: If you are working with a fresh install of Lumen, then you probably also need a migration file for your Users table. You can create your own, or you can copy a basic one from Laravel: -https://github.com/laravel/laravel/blob/master/database/migrations/2014_10_12_000000_create_users_table.php +https://github.com/laravel/laravel/blob/master/database/migrations/2014_10_12_000000_create_users_table.php(https://github.com/laravel/laravel/blob/master/database/migrations/2014_10_12_000000_create_users_table.php) (You will need to run `php artisan migrate` after adding this file.) -Remember to update your ModelFactory.php to match the fields in the migration you create/copy. +Remember to update your UserFactory.php to match the fields in the migration you create/copy. From f0fe2c879478f332b72633b14468c98a7f2c84c5 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 28 Mar 2021 15:07:55 -0400 Subject: [PATCH 0404/1013] Fix typo --- docs/installation-lumen.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation-lumen.md b/docs/installation-lumen.md index 08881eb90..be484756b 100644 --- a/docs/installation-lumen.md +++ b/docs/installation-lumen.md @@ -66,7 +66,7 @@ NOTE: Remember that Laravel's authorization layer requires that your `User` mode ### User Table NOTE: If you are working with a fresh install of Lumen, then you probably also need a migration file for your Users table. You can create your own, or you can copy a basic one from Laravel: -https://github.com/laravel/laravel/blob/master/database/migrations/2014_10_12_000000_create_users_table.php(https://github.com/laravel/laravel/blob/master/database/migrations/2014_10_12_000000_create_users_table.php) +[https://github.com/laravel/laravel/blob/master/database/migrations/2014_10_12_000000_create_users_table.php](https://github.com/laravel/laravel/blob/master/database/migrations/2014_10_12_000000_create_users_table.php) (You will need to run `php artisan migrate` after adding this file.) From 3b1017904edc63905dcc6fd3795fff95c01377c9 Mon Sep 17 00:00:00 2001 From: Jochen Sengier Date: Wed, 12 May 2021 10:45:52 +0200 Subject: [PATCH 0405/1013] Remove redundant code in getMigrationFileName() function Unless I'm mistaken, these lines can be safely deleted. --- src/PermissionServiceProvider.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index b9f17398a..867d081af 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -176,9 +176,6 @@ protected function getMigrationFileName($migrationFileName): string $filesystem = $this->app->make(Filesystem::class); return Collection::make($this->app->databasePath().DIRECTORY_SEPARATOR.'migrations'.DIRECTORY_SEPARATOR) - ->flatMap(function ($path) use ($filesystem) { - return $filesystem->glob($path.'*_create_permission_tables.php'); - })->push($this->app->databasePath()."/migrations/{$timestamp}_create_permission_tables.php") ->flatMap(function ($path) use ($filesystem, $migrationFileName) { return $filesystem->glob($path.'*_'.$migrationFileName); }) From 00a21daa278a09f51610097f4b7df6d05f3ac1de Mon Sep 17 00:00:00 2001 From: Joost de Bruijn Date: Tue, 18 May 2021 13:24:17 +0200 Subject: [PATCH 0406/1013] feat: add blade directives for hasExactRoles --- docs/basic-usage/blade-directives.md | 9 +++++++++ src/PermissionServiceProvider.php | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/docs/basic-usage/blade-directives.md b/docs/basic-usage/blade-directives.md index 1c1aaaaff..aafa016b4 100644 --- a/docs/basic-usage/blade-directives.md +++ b/docs/basic-usage/blade-directives.md @@ -89,3 +89,12 @@ Alternatively, `@unlessrole` gives the reverse for checking a singular role, lik @endunlessrole ``` +You can also determine if a user has exactly all of a given list of roles: + +```php +@hasexactroles('writer|admin'); + I am both a writer and an admin and nothing else! +@else + I do not have all of these roles or have more other roles... +@endhasexactroles +``` diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index e9e20e660..f2e44ec18 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -117,6 +117,15 @@ protected function registerBladeExtensions() $bladeCompiler->directive('endunlessrole', function () { return ''; }); + + $bladeCompiler->directive('hasexactroles', function ($arguments) { + list($roles, $guard) = explode(',', $arguments.','); + + return "check() && auth({$guard})->user()->hasExactRoles({$roles})): ?>"; + }); + $bladeCompiler->directive('endhasexactroles', function () { + return ''; + }); }); } From fd6512ecb03bcd0469e2886656e19310d2ae7212 Mon Sep 17 00:00:00 2001 From: Joost de Bruijn Date: Tue, 18 May 2021 13:24:26 +0200 Subject: [PATCH 0407/1013] feat: add tests for hasExactRoles --- tests/HasRolesTest.php | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index be20b73e1..661bf2f7d 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -484,6 +484,45 @@ public function it_can_determine_that_a_user_has_all_of_the_given_roles() $this->assertFalse($this->testUser->hasAllRoles(['testRole', 'second role'], 'fakeGuard')); } + /** @test */ + public function it_can_determine_that_a_user_has_exact_all_of_the_given_roles() + { + $roleModel = app(Role::class); + + $this->assertFalse($this->testUser->hasExactRoles($roleModel->first())); + + $this->assertFalse($this->testUser->hasExactRoles('testRole')); + + $this->assertFalse($this->testUser->hasExactRoles($roleModel->all())); + + $roleModel->create(['name' => 'second role']); + + $this->testUser->assignRole($this->testUserRole); + + $this->assertTrue($this->testUser->hasExactRoles('testRole')); + $this->assertTrue($this->testUser->hasExactRoles('testRole', 'web')); + $this->assertFalse($this->testUser->hasExactRoles('testRole', 'fakeGuard')); + + $this->assertFalse($this->testUser->hasExactRoles(['testRole', 'second role'])); + $this->assertFalse($this->testUser->hasExactRoles(['testRole', 'second role'], 'web')); + + $this->testUser->assignRole('second role'); + + $this->assertTrue($this->testUser->hasExactRoles(['testRole', 'second role'])); + $this->assertTrue($this->testUser->hasExactRoles(['testRole', 'second role'], 'web')); + $this->assertFalse($this->testUser->hasExactRoles(['testRole', 'second role'], 'fakeGuard')); + + $roleModel->create(['name' => 'third role']); + $this->testUser->assignRole('third role'); + + $this->assertFalse($this->testUser->hasExactRoles(['testRole', 'second role'])); + $this->assertFalse($this->testUser->hasExactRoles(['testRole', 'second role'], 'web')); + $this->assertFalse($this->testUser->hasExactRoles(['testRole', 'second role'], 'fakeGuard')); + $this->assertTrue($this->testUser->hasExactRoles(['testRole', 'second role', 'third role'])); + $this->assertTrue($this->testUser->hasExactRoles(['testRole', 'second role', 'third role'], 'web')); + $this->assertFalse($this->testUser->hasExactRoles(['testRole', 'second role', 'third role'], 'fakeGuard')); + } + /** @test */ public function it_can_determine_that_a_user_does_not_have_a_role_from_another_guard() { From 4a89dd3f083ddb89153367e6d087de3872c724ff Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 1 Jun 2021 20:39:38 -0400 Subject: [PATCH 0408/1013] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3aeb1a822..8296e4fce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to `laravel-permission` will be documented in this file +## 4.1.0 - 2021-06-01 +- Refactor to resolve guard only once during middleware +- Refactor service provider by extracting some methods + ## 4.0.1 - 2021-03-22 - Added note in migration for field lengths on MySQL 8. (either shorten the columns to 125 or use InnoDB) From 39d8b0078f8e092c2d0b77a78347876e2cbdc562 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 1 Jun 2021 20:45:21 -0400 Subject: [PATCH 0409/1013] Update php-cs-fixer.yml --- .github/workflows/php-cs-fixer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml index 84ab01ad2..b3c985609 100644 --- a/.github/workflows/php-cs-fixer.yml +++ b/.github/workflows/php-cs-fixer.yml @@ -13,7 +13,7 @@ jobs: - name: Fix style uses: docker://oskarstark/php-cs-fixer-ga with: - args: --config=.php_cs --allow-risky=yes + args: --config=.php_cs.dist.php --allow-risky=yes - name: Extract branch name shell: bash From e9555ad90c852232f72ff4dd3b8b44f503cd3aab Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 1 Jun 2021 20:46:10 -0400 Subject: [PATCH 0410/1013] Update .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f7129a1c7..ba274b9c4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,5 @@ vendor tests/temp .idea .phpunit.result.cache -.php_cs.cache +.php-cs-fixer.cache From 9c2380884cca1a29714b8e43d4c95d8b5f8c0f1c Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 1 Jun 2021 20:48:51 -0400 Subject: [PATCH 0411/1013] Update php cs fixer --- .php_cs => .php_cs.dist.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) rename .php_cs => .php_cs.dist.php (73%) diff --git a/.php_cs b/.php_cs.dist.php similarity index 73% rename from .php_cs rename to .php_cs.dist.php index 1c4e7d562..bd7148822 100644 --- a/.php_cs +++ b/.php_cs.dist.php @@ -1,9 +1,6 @@ notPath('bootstrap/*') - ->notPath('storage/*') - ->notPath('resources/view/mail/*') ->in([ __DIR__ . '/src', __DIR__ . '/tests', @@ -13,14 +10,14 @@ ->ignoreDotFiles(true) ->ignoreVCS(true); -return PhpCsFixer\Config::create() +return (new PhpCsFixer\Config()) ->setRules([ '@PSR2' => true, 'array_syntax' => ['syntax' => 'short'], - 'ordered_imports' => ['sortAlgorithm' => 'alpha'], + 'ordered_imports' => ['sort_algorithm' => 'alpha'], 'no_unused_imports' => true, 'not_operator_with_successor_space' => true, - 'trailing_comma_in_multiline_array' => true, + 'trailing_comma_in_multiline' => true, 'phpdoc_scalar' => true, 'unary_operator_spaces' => true, 'binary_operator_spaces' => true, @@ -29,9 +26,15 @@ ], 'phpdoc_single_line_var_spacing' => true, 'phpdoc_var_without_name' => true, + 'class_attributes_separation' => [ + 'elements' => [ + 'method' => 'one', + ], + ], 'method_argument_space' => [ 'on_multiline' => 'ensure_fully_multiline', 'keep_multiple_spaces_after_comma' => true, - ] + ], + 'single_trait_insert_per_statement' => true, ]) ->setFinder($finder); From 8660d86a2b4f35007406742a473e33952de9f428 Mon Sep 17 00:00:00 2001 From: drbyte Date: Wed, 2 Jun 2021 00:49:19 +0000 Subject: [PATCH 0412/1013] Fix styling --- tests/Admin.php | 4 +++- tests/Manager.php | 4 +++- tests/User.php | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/Admin.php b/tests/Admin.php index ede545b9e..b942a15d4 100644 --- a/tests/Admin.php +++ b/tests/Admin.php @@ -11,7 +11,9 @@ class Admin extends Model implements AuthorizableContract, AuthenticatableContract { - use HasRoles, Authorizable, Authenticatable; + use HasRoles; + use Authorizable; + use Authenticatable; protected $fillable = ['email']; diff --git a/tests/Manager.php b/tests/Manager.php index b1be89b7c..000150913 100644 --- a/tests/Manager.php +++ b/tests/Manager.php @@ -11,7 +11,9 @@ class Manager extends Model implements AuthorizableContract, AuthenticatableContract { - use HasRoles, Authorizable, Authenticatable; + use HasRoles; + use Authorizable; + use Authenticatable; protected $fillable = ['email']; diff --git a/tests/User.php b/tests/User.php index dc8e2c568..886fd8fd2 100644 --- a/tests/User.php +++ b/tests/User.php @@ -11,7 +11,9 @@ class User extends Model implements AuthorizableContract, AuthenticatableContract { - use HasRoles, Authorizable, Authenticatable; + use HasRoles; + use Authorizable; + use Authenticatable; protected $fillable = ['email']; From d4ec9f3f4ee3f3b81f8906322b9ab07f68949443 Mon Sep 17 00:00:00 2001 From: David Adi Nugroho Date: Fri, 4 Jun 2021 15:19:42 +0700 Subject: [PATCH 0413/1013] Update exceptions.md --- docs/advanced-usage/exceptions.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/advanced-usage/exceptions.md b/docs/advanced-usage/exceptions.md index fcc7f9d9e..9e2425c51 100644 --- a/docs/advanced-usage/exceptions.md +++ b/docs/advanced-usage/exceptions.md @@ -3,7 +3,7 @@ title: Exceptions weight: 3 --- -If you need to override exceptions thrown by this package, you can simply use normal [Laravel practices for handling exceptions](https://laravel.com/docs/errors#render-method). +If you need to override exceptions thrown by this package, you can simply use normal [Laravel practices for handling exceptions](https://laravel.com/docs/8.x/errors#rendering-exceptions). An example is shown below for your convenience, but nothing here is specific to this package other than the name of the exception. @@ -12,15 +12,14 @@ You can find all the exceptions added by this package in the code here: https:// **app/Exceptions/Handler.php** ```php -public function render($request, Throwable $exception) + +public function register() { - if ($exception instanceof \Spatie\Permission\Exceptions\UnauthorizedException) { + $this->renderable(function (\Spatie\Permission\Exceptions\UnauthorizedException $e, $request) { return response()->json([ 'responseMessage' => 'You do not have the required authorization.', 'responseStatus' => 403, ]); - } - - return parent::render($request, $exception); + }); } ``` From a6e4122b65094baba7f98df153af0768ef910c85 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 4 Jun 2021 19:47:08 -0400 Subject: [PATCH 0414/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8296e4fce..0cd58c39c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 4.2.0 - 2021-06-04 +- Add hasExactRoles method #1696 + ## 4.1.0 - 2021-06-01 - Refactor to resolve guard only once during middleware - Refactor service provider by extracting some methods From c740f5b99b75d195eceb791e0dd2a8e3dd2b8bb8 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Fri, 9 Jul 2021 11:13:12 +0200 Subject: [PATCH 0415/1013] Update _index.md --- docs/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_index.md b/docs/_index.md index 390f0bb4e..2bf4d0fd1 100644 --- a/docs/_index.md +++ b/docs/_index.md @@ -2,5 +2,5 @@ title: v4 slogan: Associate users with roles and permissions githubUrl: https://github.com/spatie/laravel-permission -branch: master +branch: main --- From 52606d1ada2f2b48cc35add2126afb0adc417da1 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 19 Jul 2021 18:07:43 -0400 Subject: [PATCH 0416/1013] Remind readers to use the same role name throughout their app --- docs/basic-usage/new-app.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index b35d50928..32829562f 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -119,8 +119,9 @@ php artisan migrate:fresh --seed --seeder=PermissionsDemoSeeder ``` ### Grant Super-Admin access -Super-Admins are a common feature. Using the following approach allows that when your Super-Admin user is logged in, all permission-checks in your app which call `can()` or `@can()` will return true. +Super-Admins are a common feature. The following approach allows that when your Super-Admin user is logged in, all permission-checks in your app which call `can()` or `@can()` will return true. +- Create a role named `Super-Admin`. (Or whatever name you wish; but use it consistently just like you must with any role name.) - Add a Gate::before check in your `AuthServiceProvider`: ```diff @@ -130,7 +131,7 @@ Super-Admins are a common feature. Using the following approach allows that when // -+ // Implicitly grant "Super Admin" role all permission checks using can() ++ // Implicitly grant "Super-Admin" role all permission checks using can() + Gate::before(function ($user, $ability) { + if ($user->hasRole('Super-Admin')) { + return true; From 0e39dcca89f01432207c1cbc1661ce06608ff52c Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 19 Jul 2021 21:18:58 -0400 Subject: [PATCH 0417/1013] Make role name in seeder match rest of example --- docs/basic-usage/new-app.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index 32829562f..8952029b7 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -86,7 +86,7 @@ class PermissionsDemoSeeder extends Seeder $role2->givePermissionTo('publish articles'); $role2->givePermissionTo('unpublish articles'); - $role3 = Role::create(['name' => 'super-admin']); + $role3 = Role::create(['name' => 'Super-Admin']); // gets all permissions via Gate::before rule; see AuthServiceProvider // create demo users From 7449dcb77e58a5ce3e884ca26666468acb3c1ea5 Mon Sep 17 00:00:00 2001 From: Aju Chacko Date: Thu, 22 Jul 2021 21:43:45 +0530 Subject: [PATCH 0418/1013] Update seeding.md --- docs/advanced-usage/seeding.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-usage/seeding.md b/docs/advanced-usage/seeding.md index e8b1fa266..fcff50ce7 100644 --- a/docs/advanced-usage/seeding.md +++ b/docs/advanced-usage/seeding.md @@ -75,7 +75,7 @@ $permissionsByRole = [ ]; $insertPermissions = fn ($role) => collect($permissionsByRole[$role]) - ->map(fn ($name) => DB::table()->insertGetId(['name' => $name])) + ->map(fn ($name) => DB::table('permissions')->insertGetId(['name' => $name, 'guard_name' => 'web'])) ->toArray(); $permissionIdsByRole = [ From 57b9ddb073fe554d1436b3fb95f69b8560ada220 Mon Sep 17 00:00:00 2001 From: Erik Niebla Date: Tue, 27 Jul 2021 17:30:16 -0500 Subject: [PATCH 0419/1013] Make cache more than 90% smaller --- src/Models/Permission.php | 8 ++++++++ src/PermissionRegistrar.php | 37 +++++++++++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 4de94c966..607d2a206 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -144,4 +144,12 @@ protected static function getPermissions(array $params = []): Collection ->setPermissionClass(static::class) ->getPermissions($params); } + + /** + * Set roles for cache load. + */ + public function setRolesCollection(Collection $roles) + { + $this->relations['roles'] = $roles; + } } diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 9dca1d5c4..fa6dcb3a2 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -125,13 +125,42 @@ public function getPermissions(array $params = []): Collection { if ($this->permissions === null) { $this->permissions = $this->cache->remember(self::$cacheKey, self::$cacheExpirationTime, function () { - return $this->getPermissionClass() - ->with('roles') - ->get(); + $permissions = $this->getPermissionClass()->select('id', 'name', 'guard_name') + ->with(['roles' => function ($q) { + $q->select('id', 'name', 'guard_name'); + }]) + ->get()->toArray(); + foreach ($permissions as $i => $permission) { + foreach ($permission['roles'] ?? [] as $j => $roles) { + unset($permissions[$i]['roles'][$j]['pivot']); + } + } + return $permissions; }); + + $permissions = new Collection(); + foreach ($this->permissions as $permission_array) { + $permission = new $this->permissionClass(); + foreach ($permission_array as $key => $value) { + if($key == 'roles') continue; + $permission->$key = $value; + } + + $roles = new Collection(); + foreach ($permission_array['roles'] ?? [] as $role_array) { + $role = new $this->roleClass(); + foreach ($role_array as $key => $value) { + $role->$key = $value; + } + $roles->push($role); + } + $permission->setRolesCollection($roles); + $permissions->push($permission); + } + $this->permissions = $permissions; } - $permissions = clone $this->permissions; + $permissions = $this->permissions; foreach ($params as $attr => $value) { $permissions = $permissions->where($attr, $value); From 4764a6a4c80c1c42be96e44e5c9becd129546efb Mon Sep 17 00:00:00 2001 From: Erik Niebla Date: Wed, 28 Jul 2021 11:40:24 -0500 Subject: [PATCH 0420/1013] Speed up permissions cache lookups --- src/Models/Permission.php | 31 +++++++++++++++++++++++++------ src/PermissionRegistrar.php | 37 ++++++++++++++++++++++++++++--------- 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 607d2a206..452e5a9d3 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -36,7 +36,7 @@ public static function create(array $attributes = []) { $attributes['guard_name'] = $attributes['guard_name'] ?? Guard::getDefaultName(static::class); - $permission = static::getPermissions(['name' => $attributes['name'], 'guard_name' => $attributes['guard_name']])->first(); + $permission = static::getPermission(['name' => $attributes['name'], 'guard_name' => $attributes['guard_name']]); if ($permission) { throw PermissionAlreadyExists::create($attributes['name'], $attributes['guard_name']); @@ -85,7 +85,7 @@ public function users(): BelongsToMany public static function findByName(string $name, $guardName = null): PermissionContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); - $permission = static::getPermissions(['name' => $name, 'guard_name' => $guardName])->first(); + $permission = static::getPermission(['name' => $name, 'guard_name' => $guardName]); if (! $permission) { throw PermissionDoesNotExist::create($name, $guardName); } @@ -106,7 +106,7 @@ public static function findByName(string $name, $guardName = null): PermissionCo public static function findById(int $id, $guardName = null): PermissionContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); - $permission = static::getPermissions(['id' => $id, 'guard_name' => $guardName])->first(); + $permission = static::getPermission(['id' => $id, 'guard_name' => $guardName]); if (! $permission) { throw PermissionDoesNotExist::withId($id, $guardName); @@ -126,7 +126,7 @@ public static function findById(int $id, $guardName = null): PermissionContract public static function findOrCreate(string $name, $guardName = null): PermissionContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); - $permission = static::getPermissions(['name' => $name, 'guard_name' => $guardName])->first(); + $permission = static::getPermission(['name' => $name, 'guard_name' => $guardName]); if (! $permission) { return static::query()->create(['name' => $name, 'guard_name' => $guardName]); @@ -137,16 +137,35 @@ public static function findOrCreate(string $name, $guardName = null): Permission /** * Get the current cached permissions. + * + * @param array $params + * @param bool $onlyOne + * + * @return \Illuminate\Database\Eloquent\Collection */ - protected static function getPermissions(array $params = []): Collection + protected static function getPermissions(array $params = [], bool $onlyOne = false): Collection { return app(PermissionRegistrar::class) ->setPermissionClass(static::class) - ->getPermissions($params); + ->getPermissions($params, $onlyOne); + } + + /** + * Get the current cached first permission. + * + * @param array $params + * + * @return \Spatie\Permission\Contracts\Permission + */ + protected static function getPermission(array $params = []): ?PermissionContract + { + return static::getPermissions($params, true)->first(); } /** * Set roles for cache load. + * + * @param \Illuminate\Database\Eloquent\Collection $roles */ public function setRolesCollection(Collection $roles) { diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index fa6dcb3a2..56bae28cb 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -115,13 +115,10 @@ public function clearClassPermissions() } /** - * Get the permissions based on the passed params. - * - * @param array $params - * - * @return \Illuminate\Database\Eloquent\Collection + * Load permissions from cache + * This get cache and turns array into \Illuminate\Database\Eloquent\Collection */ - public function getPermissions(array $params = []): Collection + private function loadPermissions() { if ($this->permissions === null) { $this->permissions = $this->cache->remember(self::$cacheKey, self::$cacheExpirationTime, function () { @@ -159,11 +156,33 @@ public function getPermissions(array $params = []): Collection } $this->permissions = $permissions; } + } - $permissions = $this->permissions; + /** + * Get the permissions based on the passed params. + * + * @param array $params + * @param bool $onlyOne + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getPermissions(array $params = [], bool $onlyOne = false): Collection + { + $this->loadPermissions(); + + $method = $onlyOne ? 'first' : 'filter'; + + $permissions = $this->permissions->$method(static function ($permission) use ($params) { + foreach ($params as $attr => $value) { + if ($permission->getAttribute($attr) != $value) { + return false; + } + } + return true; + }); - foreach ($params as $attr => $value) { - $permissions = $permissions->where($attr, $value); + if ($onlyOne) { + $permissions = new Collection($permissions ? [$permissions] : []); } return $permissions; From 8ac7f7bd76b7ef3cf58e26cca429bbc73fcd238b Mon Sep 17 00:00:00 2001 From: Erik Niebla Date: Thu, 29 Jul 2021 11:13:23 -0500 Subject: [PATCH 0421/1013] Better readability and performance, easy transition from Collection to array --- config/permission.php | 11 ----------- src/Models/Permission.php | 36 ++++++++++++++++++++++++++++++++---- src/Models/Role.php | 28 ++++++++++++++++++++++++++++ src/PermissionRegistrar.php | 37 +++++++++---------------------------- 4 files changed, 69 insertions(+), 43 deletions(-) diff --git a/config/permission.php b/config/permission.php index 1a4207e6c..c306a8d50 100644 --- a/config/permission.php +++ b/config/permission.php @@ -121,17 +121,6 @@ 'key' => 'spatie.permission.cache', - /* - * When checking for a permission against a model by passing a Permission - * instance to the check, this key determines what attribute on the - * Permissions model is used to cache against. - * - * Ideally, this should match your preferred way of checking permissions, eg: - * `$user->can('view-posts')` would be 'name'. - */ - - 'model_key' => 'name', - /* * You may optionally indicate a specific cache driver to use for permission and * role caching using any of the `store` drivers listed in the cache.php config diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 452e5a9d3..37f72c3aa 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -163,12 +163,40 @@ protected static function getPermission(array $params = []): ?PermissionContract } /** - * Set roles for cache load. + * Fill model from array. * - * @param \Illuminate\Database\Eloquent\Collection $roles + * @param array $attributes */ - public function setRolesCollection(Collection $roles) + protected function fillModelFromArray(array $attributes) { - $this->relations['roles'] = $roles; + if (isset($attributes['roles'])) { + $roleClass = app(PermissionRegistrar::class)->getRoleClass(); + $this->relations['roles'] = new Collection(); + + foreach ($attributes['roles'] as $value) { + $this->relations['roles']->push($roleClass::getModelFromArray($value)); + } + unset($attributes['roles']); + } + + $this->attributes = $attributes; + if (isset($attributes['id'])) { + $this->exists = true; + $this->original['id'] = $attributes['id']; + } + return $this; + } + + /** + * Get model from array. + * + * @param array $attributes + * + * @return \Spatie\Permission\Contracts\Permission + */ + public static function getModelFromArray(array $attributes): ?PermissionContract + { + $permission = new static; + return $permission->fillModelFromArray($attributes); } } diff --git a/src/Models/Role.php b/src/Models/Role.php index 5fd3177fc..a9bbe2d43 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -157,4 +157,32 @@ public function hasPermissionTo($permission): bool return $this->permissions->contains('id', $permission->id); } + + /** + * Fill model from array. + * + * @param array $attributes + */ + protected function fillModelFromArray(array $attributes) + { + $this->attributes = $attributes; + if (isset($attributes['id'])) { + $this->exists = true; + $this->original['id'] = $attributes['id']; + } + return $this; + } + + /** + * Get model from array. + * + * @param array $attributes + * + * @return \Spatie\Permission\Contracts\Role + */ + public static function getModelFromArray(array $attributes): ?RoleContract + { + $roles = new static; + return $roles->fillModelFromArray($attributes); + } } diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 56bae28cb..70d344353 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -32,9 +32,6 @@ class PermissionRegistrar /** @var string */ public static $cacheKey; - /** @var string */ - public static $cacheModelKey; - /** * PermissionRegistrar constructor. * @@ -51,17 +48,17 @@ public function __construct(CacheManager $cacheManager) public function initializeCache() { - self::$cacheExpirationTime = config('permission.cache.expiration_time', config('permission.cache_expiration_time')); + self::$cacheExpirationTime = config('permission.cache.expiration_time') ?: \DateInterval::createFromDateString('24 hours'); self::$cacheKey = config('permission.cache.key'); - self::$cacheModelKey = config('permission.cache.model_key'); $this->cache = $this->getCacheStoreFromConfig(); } protected function getCacheStoreFromConfig(): \Illuminate\Contracts\Cache\Repository { - // the 'default' fallback here is from the permission.php config file, where 'default' means to use config(cache.default) + // the 'default' fallback here is from the permission.php config file, + // where 'default' means to use config(cache.default) $cacheDriver = config('permission.cache.store', 'default'); // when 'default' is specified, no action is required since we already have the default instance @@ -123,9 +120,7 @@ private function loadPermissions() if ($this->permissions === null) { $this->permissions = $this->cache->remember(self::$cacheKey, self::$cacheExpirationTime, function () { $permissions = $this->getPermissionClass()->select('id', 'name', 'guard_name') - ->with(['roles' => function ($q) { - $q->select('id', 'name', 'guard_name'); - }]) + ->with('roles:id,name,guard_name') ->get()->toArray(); foreach ($permissions as $i => $permission) { foreach ($permission['roles'] ?? [] as $j => $roles) { @@ -134,27 +129,13 @@ private function loadPermissions() } return $permissions; }); - - $permissions = new Collection(); - foreach ($this->permissions as $permission_array) { - $permission = new $this->permissionClass(); - foreach ($permission_array as $key => $value) { - if($key == 'roles') continue; - $permission->$key = $value; - } - - $roles = new Collection(); - foreach ($permission_array['roles'] ?? [] as $role_array) { - $role = new $this->roleClass(); - foreach ($role_array as $key => $value) { - $role->$key = $value; - } - $roles->push($role); + if (is_array($this->permissions)) { + $permissions = new Collection(); + foreach ($this->permissions as $value) { + $permissions->push($this->permissionClass::getModelFromArray($value)); } - $permission->setRolesCollection($roles); - $permissions->push($permission); + $this->permissions = $permissions; } - $this->permissions = $permissions; } } From 34ad214677ec6471782b876dcfdfed0bf62bae9a Mon Sep 17 00:00:00 2001 From: drbyte Date: Tue, 17 Aug 2021 18:36:09 +0000 Subject: [PATCH 0422/1013] Fix styling --- src/Models/Permission.php | 2 ++ src/Models/Role.php | 2 ++ src/PermissionRegistrar.php | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 37f72c3aa..5f3e060f8 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -184,6 +184,7 @@ protected function fillModelFromArray(array $attributes) $this->exists = true; $this->original['id'] = $attributes['id']; } + return $this; } @@ -197,6 +198,7 @@ protected function fillModelFromArray(array $attributes) public static function getModelFromArray(array $attributes): ?PermissionContract { $permission = new static; + return $permission->fillModelFromArray($attributes); } } diff --git a/src/Models/Role.php b/src/Models/Role.php index a9bbe2d43..6810b5ae5 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -170,6 +170,7 @@ protected function fillModelFromArray(array $attributes) $this->exists = true; $this->original['id'] = $attributes['id']; } + return $this; } @@ -183,6 +184,7 @@ protected function fillModelFromArray(array $attributes) public static function getModelFromArray(array $attributes): ?RoleContract { $roles = new static; + return $roles->fillModelFromArray($attributes); } } diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 70d344353..527f017c9 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -127,6 +127,7 @@ private function loadPermissions() unset($permissions[$i]['roles'][$j]['pivot']); } } + return $permissions; }); if (is_array($this->permissions)) { @@ -159,6 +160,7 @@ public function getPermissions(array $params = [], bool $onlyOne = false): Colle return false; } } + return true; }); From 78eaa5e06c313a9f3672a7571b4d83b913721b72 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 17 Aug 2021 14:37:17 -0400 Subject: [PATCH 0423/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cd58c39c..78d91fd05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 4.3.0 - 2021-08-17 +- Speed up permissions cache lookups, and make cache smaller #1799 + ## 4.2.0 - 2021-06-04 - Add hasExactRoles method #1696 From d38849e928d6e4396fed36d9bf2f9597ac9ce7b9 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 17 Aug 2021 18:13:04 -0400 Subject: [PATCH 0424/1013] Update bug_report.md --- .github/ISSUE_TEMPLATE/bug_report.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index f4c3a68a3..328f945dd 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -8,7 +8,7 @@ assignees: '' --- **Before creating a new bug report** -Please check if there isn't a similar issue on [the issue tracker](https://github.com/spatie/laravel-persmission/issues) or in [the discussions](https://github.com/spatie/laravel-permission/discussions). +Please check if there isn't a similar issue on [the issue tracker](https://github.com/spatie/laravel-permission/issues) or in [the discussions](https://github.com/spatie/laravel-permission/discussions). **Describe the bug** A clear and concise description of what the bug is. @@ -26,12 +26,18 @@ Database version: **To Reproduce** Steps to reproduce the behavior: +Here is my example code and/or tests showing the problem in my app: + +**Example Application** +Here is a link to my Github repo containing a minimal Laravel application which shows my problem: + **Expected behavior** A clear and concise description of what you expected to happen. -**Desktop (please complete the following information):** + **Additional context** +Add any other context about the problem here. + +**Environment (please complete the following information, because it helps us investigate better):** - OS: [e.g. macOS] - Version [e.g. 22] - **Additional context** -Add any other context about the problem here. From 5e13d582a1b9ced7e4aa74ecf115b5f1b2e35383 Mon Sep 17 00:00:00 2001 From: gitetsu Date: Wed, 18 Aug 2021 11:22:43 +0900 Subject: [PATCH 0425/1013] Update docblocks scopeRole/assignRole/removeRole/syncRoles can accept an integer. --- src/Traits/HasRoles.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index f109fb0b1..c9d816692 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -52,7 +52,7 @@ public function roles(): BelongsToMany * Scope the model query to certain roles only. * * @param \Illuminate\Database\Eloquent\Builder $query - * @param string|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles + * @param string|int|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles * @param string $guard * * @return \Illuminate\Database\Eloquent\Builder @@ -86,7 +86,7 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder /** * Assign the given role to the model. * - * @param array|string|\Spatie\Permission\Contracts\Role ...$roles + * @param array|string|int|\Spatie\Permission\Contracts\Role ...$roles * * @return $this */ @@ -134,7 +134,7 @@ function ($object) use ($roles, $model) { /** * Revoke the given role from the model. * - * @param string|\Spatie\Permission\Contracts\Role $role + * @param string|int|\Spatie\Permission\Contracts\Role $role */ public function removeRole($role) { @@ -150,7 +150,7 @@ public function removeRole($role) /** * Remove all current roles and set the given ones. * - * @param array|\Spatie\Permission\Contracts\Role|string ...$roles + * @param array|\Spatie\Permission\Contracts\Role|string|int ...$roles * * @return $this */ From 5b3ea6b0e3ed08a0efc8d9b016fb0750fda7ccac Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 17 Aug 2021 23:56:34 -0400 Subject: [PATCH 0426/1013] Update version link --- docs/introduction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/introduction.md b/docs/introduction.md index f3dc122d3..17baa86d2 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -17,7 +17,7 @@ $user->assignRole('writer'); $role->givePermissionTo('edit articles'); ``` -If you're using multiple guards we've got you covered as well. Every guard will have its own set of permissions and roles that can be assigned to the guard's users. Read about it in the [using multiple guards](https://docs.spatie.be/laravel-permission/v3/basic-usage/multiple-guards/) section. +If you're using multiple guards we've got you covered as well. Every guard will have its own set of permissions and roles that can be assigned to the guard's users. Read about it in the [using multiple guards](https://docs.spatie.be/laravel-permission/v4/basic-usage/multiple-guards/) section. Because all permissions will be registered on [Laravel's gate](https://laravel.com/docs/authorization), you can check if a user has a permission with Laravel's default `can` function: From 479d1ed37f29477332117ff9a9e27826994d5b27 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 19 Aug 2021 19:57:45 -0400 Subject: [PATCH 0427/1013] Rename some tests --- tests/PermissionMiddlewareTest.php | 4 ++-- tests/RoleMiddlewareTest.php | 4 ++-- tests/RoleOrPermissionMiddlewareTest.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/PermissionMiddlewareTest.php b/tests/PermissionMiddlewareTest.php index fa187f78f..55e3ff7a6 100644 --- a/tests/PermissionMiddlewareTest.php +++ b/tests/PermissionMiddlewareTest.php @@ -177,7 +177,7 @@ public function use_not_existing_custom_guard_in_permission() } /** @test */ - public function user_can_not_access_permission_with_guard_admin_while_using_default_guard() + public function user_can_not_access_permission_with_guard_admin_while_login_using_default_guard() { Auth::login($this->testUser); @@ -190,7 +190,7 @@ public function user_can_not_access_permission_with_guard_admin_while_using_defa } /** @test */ - public function user_can_access_permission_with_guard_admin_while_using_default_guard() + public function user_can_access_permission_with_guard_admin_while_login_using_admin_guard() { Auth::guard('admin')->login($this->testAdmin); diff --git a/tests/RoleMiddlewareTest.php b/tests/RoleMiddlewareTest.php index 991e73597..70fd27e86 100644 --- a/tests/RoleMiddlewareTest.php +++ b/tests/RoleMiddlewareTest.php @@ -143,7 +143,7 @@ public function use_not_existing_custom_guard_in_role() } /** @test */ - public function user_can_not_access_role_with_guard_admin_while_using_default_guard() + public function user_can_not_access_role_with_guard_admin_while_login_using_default_guard() { Auth::login($this->testUser); @@ -156,7 +156,7 @@ public function user_can_not_access_role_with_guard_admin_while_using_default_gu } /** @test */ - public function user_can_access_role_with_guard_admin_while_using_default_guard() + public function user_can_access_role_with_guard_admin_while_login_using_admin_guard() { Auth::guard('admin')->login($this->testAdmin); diff --git a/tests/RoleOrPermissionMiddlewareTest.php b/tests/RoleOrPermissionMiddlewareTest.php index da41c13fd..00e81b290 100644 --- a/tests/RoleOrPermissionMiddlewareTest.php +++ b/tests/RoleOrPermissionMiddlewareTest.php @@ -96,7 +96,7 @@ public function use_not_existing_custom_guard_in_role_or_permission() } /** @test */ - public function user_can_not_access_permission_or_role_with_guard_admin_while_using_default_guard() + public function user_can_not_access_permission_or_role_with_guard_admin_while_login_using_default_guard() { Auth::login($this->testUser); @@ -110,7 +110,7 @@ public function user_can_not_access_permission_or_role_with_guard_admin_while_us } /** @test */ - public function user_can_access_permission_or_role_with_guard_admin_while_using_default_guard() + public function user_can_access_permission_or_role_with_guard_admin_while_login_using_admin_guard() { Auth::guard('admin')->login($this->testAdmin); From f18c4f0cf42b81ff98c204278af84354f3efb374 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 19 Aug 2021 20:14:11 -0400 Subject: [PATCH 0428/1013] Historically, since v2.0.0, the "detected guard_name" that was applied during lookups was detected from the "first possible defined guard" for that model, even if the "current" guard wasn't "first in the list". This has led to some confusion when expecting that the current user's guard would actually be used. This PR changes things to **use the current user's guard** first, assuming it's in the list of potential guards acceptable for the current User model. After that it will fallback to the old behavior of selecting the first available matchable guard for the current User model. THIS IS A BREAKING CHANGE, so be sure to test all authorization aspects of your application to ensure desired behavior. You MAY also be able to remove some old complex monkey-patching that you might have done to work around this issue in prior versions. Fixes #1682 Fixes #1608 Fixes #1384 Fixes #1515 Fixes #1516 --- src/Guard.php | 34 +++++++++++++++++++++++----- tests/HasPermissionsTest.php | 36 ++++++++++++++++++++++++++++++ tests/PermissionMiddlewareTest.php | 18 +++++++-------- tests/TestCase.php | 16 +++++++++++++ 4 files changed, 90 insertions(+), 14 deletions(-) diff --git a/src/Guard.php b/src/Guard.php index 9875aca3e..41d802c10 100644 --- a/src/Guard.php +++ b/src/Guard.php @@ -8,14 +8,16 @@ class Guard { /** - * return collection of (guard_name) property if exist on class or object - * otherwise will return collection of guards names that exists in config/auth.php. + * Return a collection of guard names suitable for the $model, + * as indicated by the presence of a $guard_name property or a guardName() method on the model. * * @param string|Model $model model class object or name * @return Collection */ public static function getNames($model): Collection { + $class = is_object($model) ? get_class($model) : $model; + if (is_object($model)) { if (\method_exists($model, 'guardName')) { $guardName = $model->guardName(); @@ -25,8 +27,6 @@ public static function getNames($model): Collection } if (! isset($guardName)) { - $class = is_object($model) ? get_class($model) : $model; - $guardName = (new \ReflectionClass($class))->getDefaultProperties()['guard_name'] ?? null; } @@ -34,6 +34,23 @@ public static function getNames($model): Collection return collect($guardName); } + return self::getConfigAuthGuards($class); + } + + /** + * Get list of relevant guards for the $class model based on config(auth) settings. + * + * Lookup flow: + * - get names of models for guards defined in auth.guards where a provider is set + * - filter for provider models matching the model $class being checked (important for Lumen) + * - keys() gives just the names of the matched guards + * - return collection of guard names + * + * @param string $class + * @return Collection + */ + protected static function getConfigAuthGuards(string $class): Collection + { return collect(config('auth.guards')) ->map(function ($guard) { if (! isset($guard['provider'])) { @@ -58,6 +75,13 @@ public static function getDefaultName($class): string { $default = config('auth.defaults.guard'); - return static::getNames($class)->first() ?: $default; + $possible_guards = static::getNames($class); + + // return current-detected auth.defaults.guard if it matches one of those that have been checked + if ($possible_guards->contains($default)) { + return $default; + } + + return $possible_guards->first() ?: $default; } } diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index c772bda47..b50646aca 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -529,4 +529,40 @@ public function it_can_check_if_there_is_any_of_the_direct_permissions_given() $this->assertTrue($this->testUser->hasAnyDirectPermission('edit-news', 'edit-blog')); $this->assertFalse($this->testUser->hasAnyDirectPermission('edit-blog', 'Edit News', ['Edit News'])); } + + /** @test */ + public function it_can_check_permission_based_on_logged_in_user_guard() + { + $this->testUser->givePermissionTo(app(Permission::class)::create([ + 'name' => 'do_that', + 'guard_name' => 'api', + ])); + $response = $this->actingAs($this->testUser, 'api') + ->json('GET', '/check-api-guard-permission'); + $response->assertJson([ + 'status' => true, + ]); + } + + /** @test */ + public function it_can_reject_permission_based_on_logged_in_user_guard() + { + $unassignedPermission = app(Permission::class)::create([ + 'name' => 'do_that', + 'guard_name' => 'api', + ]); + + $assignedPermission = app(Permission::class)::create([ + 'name' => 'do_that', + 'guard_name' => 'web', + ]); + + $this->testUser->givePermissionTo($assignedPermission); + $response = $this->withExceptionHandling() + ->actingAs($this->testUser, 'api') + ->json('GET', '/check-api-guard-permission'); + $response->assertJson([ + 'status' => false, + ]); + } } diff --git a/tests/PermissionMiddlewareTest.php b/tests/PermissionMiddlewareTest.php index 55e3ff7a6..fb49073d4 100644 --- a/tests/PermissionMiddlewareTest.php +++ b/tests/PermissionMiddlewareTest.php @@ -35,36 +35,36 @@ public function a_user_cannot_access_a_route_protected_by_the_permission_middlew { // These permissions are created fresh here in reverse order of guard being applied, so they are not "found first" in the db lookup when matching app(Permission::class)->create(['name' => 'admin-permission2', 'guard_name' => 'web']); - app(Permission::class)->create(['name' => 'admin-permission2', 'guard_name' => 'admin']); + $p1 = app(Permission::class)->create(['name' => 'admin-permission2', 'guard_name' => 'admin']); app(Permission::class)->create(['name' => 'edit-articles2', 'guard_name' => 'admin']); - app(Permission::class)->create(['name' => 'edit-articles2', 'guard_name' => 'web']); + $p2 = app(Permission::class)->create(['name' => 'edit-articles2', 'guard_name' => 'web']); - Auth::login($this->testAdmin); + Auth::guard('admin')->login($this->testAdmin); - $this->testAdmin->givePermissionTo('admin-permission2'); + $this->testAdmin->givePermissionTo($p1); $this->assertEquals( 200, - $this->runMiddleware($this->permissionMiddleware, 'admin-permission2') + $this->runMiddleware($this->permissionMiddleware, 'admin-permission2', 'admin') ); $this->assertEquals( 403, - $this->runMiddleware($this->permissionMiddleware, 'edit-articles2') + $this->runMiddleware($this->permissionMiddleware, 'edit-articles2', 'admin') ); Auth::login($this->testUser); - $this->testUser->givePermissionTo('edit-articles2'); + $this->testUser->givePermissionTo($p2); $this->assertEquals( 200, - $this->runMiddleware($this->permissionMiddleware, 'edit-articles2') + $this->runMiddleware($this->permissionMiddleware, 'edit-articles2', 'web') ); $this->assertEquals( 403, - $this->runMiddleware($this->permissionMiddleware, 'admin-permission2') + $this->runMiddleware($this->permissionMiddleware, 'admin-permission2', 'web') ); } diff --git a/tests/TestCase.php b/tests/TestCase.php index a789cc882..9a7069558 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,7 +3,9 @@ namespace Spatie\Permission\Test; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Schema; use Orchestra\Testbench\TestCase as Orchestra; use Spatie\Permission\Contracts\Permission; @@ -45,6 +47,8 @@ public function setUp(): void $this->testAdmin = Admin::first(); $this->testAdminRole = app(Role::class)->find(3); $this->testAdminPermission = app(Permission::class)->find(4); + + $this->setUpRoutes(); } /** @@ -142,4 +146,16 @@ public function createCacheTable() $table->integer('expiration'); }); } + + /** + * Create routes to test authentication with guards. + */ + public function setUpRoutes(): void + { + Route::middleware('auth:api')->get('/check-api-guard-permission', function (Request $request) { + return [ + 'status' => $request->user()->hasPermissionTo('do_that'), + ]; + }); + } } From 3cc2cbc62766d2d2e9c2f3e5be787f10d9ee74a2 Mon Sep 17 00:00:00 2001 From: Erik Niebla Date: Fri, 20 Aug 2021 09:17:54 -0500 Subject: [PATCH 0429/1013] Avoid sync again other objects on Eloquent::saved --- src/Traits/HasPermissions.php | 3 +++ src/Traits/HasRoles.php | 3 +++ tests/HasPermissionsTest.php | 10 ++++++++++ tests/HasRolesTest.php | 10 ++++++++++ 4 files changed, 26 insertions(+) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 4b7d6a3a5..c1401536f 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -348,6 +348,9 @@ public function givePermissionTo(...$permissions) $class::saved( function ($object) use ($permissions, $model) { + if ($model->getKey() != $object->getKey()) { + return; + } $model->permissions()->sync($permissions, false); $model->load('permissions'); } diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index c9d816692..3d775ad2a 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -120,6 +120,9 @@ public function assignRole(...$roles) $class::saved( function ($object) use ($roles, $model) { + if ($model->getKey() != $object->getKey()) { + return; + } $model->roles()->sync($roles, false); $model->load('roles'); } diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index c772bda47..87be413b7 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -474,13 +474,18 @@ public function calling_givePermissionTo_before_saving_object_doesnt_interfere_w $user2 = new User(['email' => 'test2@user.com']); $user2->givePermissionTo('edit-articles'); + + \DB::enableQueryLog(); $user2->save(); + $querys = \DB::getQueryLog(); + \DB::disableQueryLog(); $this->assertTrue($user->fresh()->hasPermissionTo('edit-news')); $this->assertFalse($user->fresh()->hasPermissionTo('edit-articles')); $this->assertTrue($user2->fresh()->hasPermissionTo('edit-articles')); $this->assertFalse($user2->fresh()->hasPermissionTo('edit-news')); + $this->assertSame(4, count($querys)); //avoid unnecessary sync } /** @test */ @@ -492,13 +497,18 @@ public function calling_syncPermissions_before_saving_object_doesnt_interfere_wi $user2 = new User(['email' => 'test2@user.com']); $user2->syncPermissions('edit-articles'); + + \DB::enableQueryLog(); $user2->save(); + $querys = \DB::getQueryLog(); + \DB::disableQueryLog(); $this->assertTrue($user->fresh()->hasPermissionTo('edit-news')); $this->assertFalse($user->fresh()->hasPermissionTo('edit-articles')); $this->assertTrue($user2->fresh()->hasPermissionTo('edit-articles')); $this->assertFalse($user2->fresh()->hasPermissionTo('edit-news')); + $this->assertSame(4, count($querys)); //avoid unnecessary sync } /** @test */ diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 661bf2f7d..405e6eac1 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -245,13 +245,18 @@ public function calling_syncRoles_before_saving_object_doesnt_interfere_with_oth $user2 = new User(['email' => 'admin@user.com']); $user2->syncRoles('testRole2'); + + \DB::enableQueryLog(); $user2->save(); + $querys = \DB::getQueryLog(); + \DB::disableQueryLog(); $this->assertTrue($user->fresh()->hasRole('testRole')); $this->assertFalse($user->fresh()->hasRole('testRole2')); $this->assertTrue($user2->fresh()->hasRole('testRole2')); $this->assertFalse($user2->fresh()->hasRole('testRole')); + $this->assertSame(4, count($querys)); //avoid unnecessary sync } /** @test */ @@ -263,13 +268,18 @@ public function calling_assignRole_before_saving_object_doesnt_interfere_with_ot $admin_user = new User(['email' => 'admin@user.com']); $admin_user->assignRole('testRole2'); + + \DB::enableQueryLog(); $admin_user->save(); + $querys = \DB::getQueryLog(); + \DB::disableQueryLog(); $this->assertTrue($user->fresh()->hasRole('testRole')); $this->assertFalse($user->fresh()->hasRole('testRole2')); $this->assertTrue($admin_user->fresh()->hasRole('testRole2')); $this->assertFalse($admin_user->fresh()->hasRole('testRole')); + $this->assertSame(4, count($querys)); //avoid unnecessary sync } /** @test */ From 9d012d730912d0bfe63e74e44e705a0a8a114c68 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 22 Aug 2021 13:46:13 -0400 Subject: [PATCH 0430/1013] Add Eloquent Collection note regarding roles --- docs/basic-usage/role-permissions.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/basic-usage/role-permissions.md b/docs/basic-usage/role-permissions.md index 8d1cb4350..c2c8fb764 100644 --- a/docs/basic-usage/role-permissions.md +++ b/docs/basic-usage/role-permissions.md @@ -88,9 +88,26 @@ The `givePermissionTo` and `revokePermissionTo` functions can accept a string or a `Spatie\Permission\Models\Permission` object. -**Permissions are inherited from roles automatically.** +**NOTE: Permissions are inherited from roles automatically.** +### What Permissions Does A Role Have? + +The `permissions` property on any given role returns a collection with all the related permission objects. This collection can respond to usual Eloquent Collection operations, such as count, sort, etc. + +```php +// get collection +$role->permissions; + +// return only the permission names: +$role->permissions->pluck('name'); + +// count the number of permissions assigned to a role +count($role->permissions); +// or +$role->permissions->count(); +``` + ## Assigning Direct Permissions To A User Additionally, individual permissions can be assigned to the user too. From 361af88bde9117bdc6b29556704d623bfb51a824 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 22 Aug 2021 19:27:26 -0400 Subject: [PATCH 0431/1013] Update sed for 8.55 compatibility --- docs/basic-usage/new-app.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index 8952029b7..03762bd4b 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -35,8 +35,8 @@ git commit -m "Add Spatie Laravel Permissions package" php artisan migrate:fresh # Add `HasRoles` trait to User model -sed -i '' $'s/use Notifiable;/use Notifiable;\\\n use \\\\Spatie\\\\Permission\\\\Traits\\\\HasRoles;/' app/User.php sed -i '' $'s/use HasFactory, Notifiable;/use HasFactory, Notifiable;\\\n use \\\\Spatie\\\\Permission\\\\Traits\\\\HasRoles;/' app/Models/User.php +sed -i '' $'s/use HasApiTokens, HasFactory, Notifiable;/use HasApiTokens, HasFactory, Notifiable;\\\n use \\\\Spatie\\\\Permission\\\\Traits\\\\HasRoles;/' app/Models/User.php git add . && git commit -m "Add HasRoles trait" # Add Laravel's basic auth scaffolding From dfcd5c745e31d4ca1daaf0213c904c3afb7ecc7c Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 22 Aug 2021 19:44:19 -0400 Subject: [PATCH 0432/1013] master->main --- docs/basic-usage/new-app.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index 03762bd4b..449955d9d 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -156,7 +156,7 @@ To share your app on Github for easy collaboration: ```sh git remote add origin git@github.com:YOURUSERNAME/REPONAME.git -git push -u origin master +git push -u origin main ``` The above only needs to be done once. @@ -165,7 +165,7 @@ The above only needs to be done once. ```sh git add . git commit -m "Explain what your commit is about here" -git push origin master +git push origin main ``` Repeat the above process whenever you change code that you want to share. From ccb1a90b89408a355e0faf6910cc007de1cd70ac Mon Sep 17 00:00:00 2001 From: Erik Niebla Date: Mon, 23 Aug 2021 12:44:31 -0500 Subject: [PATCH 0433/1013] Customised pivots instead of `role_id`,`permission_id` --- .github/workflows/run-tests-L7.yml | 4 ++-- .github/workflows/run-tests-L8.yml | 4 ++-- config/permission.php | 5 ++++ .../create_permission_tables.php.stub | 23 ++++++++++--------- src/Models/Permission.php | 6 ++--- src/Models/Role.php | 7 +++--- src/PermissionRegistrar.php | 9 ++++++++ src/Traits/HasPermissions.php | 2 +- src/Traits/HasRoles.php | 2 +- tests/TestCase.php | 3 ++- 10 files changed, 41 insertions(+), 24 deletions(-) diff --git a/.github/workflows/run-tests-L7.yml b/.github/workflows/run-tests-L7.yml index f601ce491..bde4ee667 100644 --- a/.github/workflows/run-tests-L7.yml +++ b/.github/workflows/run-tests-L7.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: - fail-fast: true + fail-fast: false matrix: php: [8.0, 7.4, 7.3, 7.2] laravel: [7.*, 6.*] @@ -39,7 +39,7 @@ jobs: - name: Install dependencies run: | - composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:>=4.3.4" --no-interaction --no-update + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:>=4.3.4" "mockery/mockery:^1.3.2" --no-interaction --no-update composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction - name: Execute tests diff --git a/.github/workflows/run-tests-L8.yml b/.github/workflows/run-tests-L8.yml index d8844b560..de738eb00 100644 --- a/.github/workflows/run-tests-L8.yml +++ b/.github/workflows/run-tests-L8.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: - fail-fast: true + fail-fast: false matrix: php: [8.0, 7.4, 7.3] laravel: [8.*] @@ -37,7 +37,7 @@ jobs: - name: Install dependencies run: | - composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:>=4.3.4" --no-interaction --no-update + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:>=4.3.4" "mockery/mockery:^1.3.2" --no-interaction --no-update composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction - name: Execute tests diff --git a/config/permission.php b/config/permission.php index c306a8d50..7e31a6c2a 100644 --- a/config/permission.php +++ b/config/permission.php @@ -72,6 +72,11 @@ ], 'column_names' => [ + /* + * Change this if you want to name the related pivots other than defaults + */ + 'role_pivot_key' => null, //default 'role_id', + 'permission_pivot_key' => null, //default 'permission_id', /* * Change this if you want to name the related model primary key other than diff --git a/database/migrations/create_permission_tables.php.stub b/database/migrations/create_permission_tables.php.stub index edf92e7a1..5ad2571a3 100644 --- a/database/migrations/create_permission_tables.php.stub +++ b/database/migrations/create_permission_tables.php.stub @@ -3,6 +3,7 @@ use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; +use Spatie\Permission\PermissionRegistrar; class CreatePermissionTables extends Migration { @@ -39,52 +40,52 @@ class CreatePermissionTables extends Migration }); Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) { - $table->unsignedBigInteger('permission_id'); + $table->unsignedBigInteger(PermissionRegistrar::$pivotPermission); $table->string('model_type'); $table->unsignedBigInteger($columnNames['model_morph_key']); $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index'); - $table->foreign('permission_id') + $table->foreign(PermissionRegistrar::$pivotPermission) ->references('id') ->on($tableNames['permissions']) ->onDelete('cascade'); - $table->primary(['permission_id', $columnNames['model_morph_key'], 'model_type'], + $table->primary([PermissionRegistrar::$pivotPermission, $columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_permission_model_type_primary'); }); Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames) { - $table->unsignedBigInteger('role_id'); + $table->unsignedBigInteger(PermissionRegistrar::$pivotRole); $table->string('model_type'); $table->unsignedBigInteger($columnNames['model_morph_key']); $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index'); - $table->foreign('role_id') + $table->foreign(PermissionRegistrar::$pivotRole) ->references('id') ->on($tableNames['roles']) ->onDelete('cascade'); - $table->primary(['role_id', $columnNames['model_morph_key'], 'model_type'], + $table->primary([PermissionRegistrar::$pivotRole, $columnNames['model_morph_key'], 'model_type'], 'model_has_roles_role_model_type_primary'); }); Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames) { - $table->unsignedBigInteger('permission_id'); - $table->unsignedBigInteger('role_id'); + $table->unsignedBigInteger(PermissionRegistrar::$pivotPermission); + $table->unsignedBigInteger(PermissionRegistrar::$pivotRole); - $table->foreign('permission_id') + $table->foreign(PermissionRegistrar::$pivotPermission) ->references('id') ->on($tableNames['permissions']) ->onDelete('cascade'); - $table->foreign('role_id') + $table->foreign(PermissionRegistrar::$pivotRole) ->references('id') ->on($tableNames['roles']) ->onDelete('cascade'); - $table->primary(['permission_id', 'role_id'], 'role_has_permissions_permission_id_role_id_primary'); + $table->primary([PermissionRegistrar::$pivotPermission, PermissionRegistrar::$pivotRole], 'role_has_permissions_permission_id_role_id_primary'); }); app('cache') diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 5f3e060f8..44355a048 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -53,8 +53,8 @@ public function roles(): BelongsToMany return $this->belongsToMany( config('permission.models.role'), config('permission.table_names.role_has_permissions'), - 'permission_id', - 'role_id' + PermissionRegistrar::$pivotPermission, + PermissionRegistrar::$pivotRole ); } @@ -67,7 +67,7 @@ public function users(): BelongsToMany getModelForGuard($this->attributes['guard_name']), 'model', config('permission.table_names.model_has_permissions'), - 'permission_id', + PermissionRegistrar::$pivotPermission, config('permission.column_names.model_morph_key') ); } diff --git a/src/Models/Role.php b/src/Models/Role.php index 6810b5ae5..860210884 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -9,6 +9,7 @@ use Spatie\Permission\Exceptions\RoleAlreadyExists; use Spatie\Permission\Exceptions\RoleDoesNotExist; use Spatie\Permission\Guard; +use Spatie\Permission\PermissionRegistrar; use Spatie\Permission\Traits\HasPermissions; use Spatie\Permission\Traits\RefreshesPermissionCache; @@ -50,8 +51,8 @@ public function permissions(): BelongsToMany return $this->belongsToMany( config('permission.models.permission'), config('permission.table_names.role_has_permissions'), - 'role_id', - 'permission_id' + PermissionRegistrar::$pivotRole, + PermissionRegistrar::$pivotPermission ); } @@ -64,7 +65,7 @@ public function users(): BelongsToMany getModelForGuard($this->attributes['guard_name']), 'model', config('permission.table_names.model_has_roles'), - 'role_id', + PermissionRegistrar::$pivotRole, config('permission.column_names.model_morph_key') ); } diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 527f017c9..3e2a94b48 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -26,6 +26,12 @@ class PermissionRegistrar /** @var \Illuminate\Database\Eloquent\Collection */ protected $permissions; + /** @var string */ + public static $pivotRole; + + /** @var string */ + public static $pivotPermission; + /** @var \DateInterval|int */ public static $cacheExpirationTime; @@ -52,6 +58,9 @@ public function initializeCache() self::$cacheKey = config('permission.cache.key'); + self::$pivotRole = config('permission.column_names.role_pivot_key') ?: 'role_id'; + self::$pivotPermission = config('permission.column_names.permission_pivot_key') ?: 'permission_id'; + $this->cache = $this->getCacheStoreFromConfig(); } diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 4b7d6a3a5..df2d71440 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -47,7 +47,7 @@ public function permissions(): BelongsToMany 'model', config('permission.table_names.model_has_permissions'), config('permission.column_names.model_morph_key'), - 'permission_id' + PermissionRegistrar::$pivotPermission ); } diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index c9d816692..ad099e064 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -44,7 +44,7 @@ public function roles(): BelongsToMany 'model', config('permission.table_names.model_has_roles'), config('permission.column_names.model_morph_key'), - 'role_id' + PermissionRegistrar::$pivotRole ); } diff --git a/tests/TestCase.php b/tests/TestCase.php index a789cc882..bbbd67b06 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -72,7 +72,8 @@ protected function getEnvironmentSetUp($app) 'database' => ':memory:', 'prefix' => '', ]); - + $app['config']->set('permission.column_names.role_pivot_key', 'role_test_id'); + $app['config']->set('permission.column_names.permission_pivot_key', 'permission_test_id'); $app['config']->set('view.paths', [__DIR__.'/resources/views']); // Set-up admin guard From d9115447245755f0fe20a40ef52e69b2998649e6 Mon Sep 17 00:00:00 2001 From: drbyte Date: Tue, 24 Aug 2021 04:00:59 +0000 Subject: [PATCH 0434/1013] Fix styling --- src/Traits/HasPermissions.php | 2 +- src/Traits/HasRoles.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index c1401536f..00107b942 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -349,7 +349,7 @@ public function givePermissionTo(...$permissions) $class::saved( function ($object) use ($permissions, $model) { if ($model->getKey() != $object->getKey()) { - return; + return; } $model->permissions()->sync($permissions, false); $model->load('permissions'); diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 3d775ad2a..0c57fdd68 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -121,7 +121,7 @@ public function assignRole(...$roles) $class::saved( function ($object) use ($roles, $model) { if ($model->getKey() != $object->getKey()) { - return; + return; } $model->roles()->sync($roles, false); $model->load('roles'); From dab7234595d9e4810e4e458570d11c2d9f1f2850 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 24 Aug 2021 00:01:31 -0400 Subject: [PATCH 0435/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78d91fd05..9dc2d3e0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## unreleased +- Avoid re-sync on non-persisted objects when firing Eloquent::saved #1819 + ## 4.3.0 - 2021-08-17 - Speed up permissions cache lookups, and make cache smaller #1799 From f320c7cfc63adf6b2601aaf99dce4e198e2fc30d Mon Sep 17 00:00:00 2001 From: Erik Niebla Date: Tue, 24 Aug 2021 10:00:54 -0500 Subject: [PATCH 0436/1013] Avoid custom hidden fields, avoid break replaced models --- src/Models/Permission.php | 8 +++---- src/PermissionRegistrar.php | 22 ++++++++++---------- tests/HasPermissionsWithCustomModelsTest.php | 16 ++++++++++++++ tests/Permission.php | 11 ++++++++++ tests/Role.php | 11 ++++++++++ tests/TestCase.php | 8 ++++++- 6 files changed, 59 insertions(+), 17 deletions(-) create mode 100644 tests/HasPermissionsWithCustomModelsTest.php create mode 100644 tests/Permission.php create mode 100644 tests/Role.php diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 5f3e060f8..32aca39a6 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -171,11 +171,9 @@ protected function fillModelFromArray(array $attributes) { if (isset($attributes['roles'])) { $roleClass = app(PermissionRegistrar::class)->getRoleClass(); - $this->relations['roles'] = new Collection(); - - foreach ($attributes['roles'] as $value) { - $this->relations['roles']->push($roleClass::getModelFromArray($value)); - } + $this->relations['roles'] = (new Collection($attributes['roles']))->map(function ($role) use ($roleClass) { + return $roleClass::getModelFromArray($role); + }); unset($attributes['roles']); } diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 527f017c9..1b9879792 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -121,21 +121,21 @@ private function loadPermissions() $this->permissions = $this->cache->remember(self::$cacheKey, self::$cacheExpirationTime, function () { $permissions = $this->getPermissionClass()->select('id', 'name', 'guard_name') ->with('roles:id,name,guard_name') - ->get()->toArray(); - foreach ($permissions as $i => $permission) { - foreach ($permission['roles'] ?? [] as $j => $roles) { - unset($permissions[$i]['roles'][$j]['pivot']); - } + ->get(); + + if (! method_exists($this->getPermissionClass(), 'getModelFromArray')) { + return $permissions; } - return $permissions; + // make the cache smaller using an array with only required fields + return $permissions->map(function ($permission) { + return $permission->only('id', 'name', 'guard_name') + ['roles' => $permission->roles->map->only('id', 'name', 'guard_name')->all()]; + })->all(); }); if (is_array($this->permissions)) { - $permissions = new Collection(); - foreach ($this->permissions as $value) { - $permissions->push($this->permissionClass::getModelFromArray($value)); - } - $this->permissions = $permissions; + $this->permissions = (new Collection($this->permissions))->map(function ($permission) { + return $this->permissionClass::getModelFromArray($permission); + }); } } } diff --git a/tests/HasPermissionsWithCustomModelsTest.php b/tests/HasPermissionsWithCustomModelsTest.php new file mode 100644 index 000000000..40c5b3d46 --- /dev/null +++ b/tests/HasPermissionsWithCustomModelsTest.php @@ -0,0 +1,16 @@ +assertSame(get_class($this->testUserPermission), \Spatie\Permission\Test\Permission::class); + $this->assertSame(get_class($this->testUserRole), \Spatie\Permission\Test\Role::class); + } +} diff --git a/tests/Permission.php b/tests/Permission.php new file mode 100644 index 000000000..9587e0469 --- /dev/null +++ b/tests/Permission.php @@ -0,0 +1,11 @@ +set('auth.guards.admin', ['driver' => 'session', 'provider' => 'admins']); $app['config']->set('auth.providers.admins', ['driver' => 'eloquent', 'model' => Admin::class]); - + if ($this->useCustomModels) { + $app['config']->set('permission.models.permission', \Spatie\Permission\Test\Permission::class); + $app['config']->set('permission.models.role', \Spatie\Permission\Test\Role::class); + } // Use test User model for users provider $app['config']->set('auth.providers.users.model', User::class); From dc1266bc40e4dd3820c20d659fb97d42a6ddaed9 Mon Sep 17 00:00:00 2001 From: drbyte Date: Tue, 24 Aug 2021 18:59:44 +0000 Subject: [PATCH 0437/1013] Fix styling --- tests/HasPermissionsWithCustomModelsTest.php | 2 +- tests/Permission.php | 2 +- tests/Role.php | 2 +- tests/TestCase.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/HasPermissionsWithCustomModelsTest.php b/tests/HasPermissionsWithCustomModelsTest.php index 40c5b3d46..8d0ab6442 100644 --- a/tests/HasPermissionsWithCustomModelsTest.php +++ b/tests/HasPermissionsWithCustomModelsTest.php @@ -5,7 +5,7 @@ class HasPermissionsWithCustomModelsTest extends HasPermissionsTest { /** @var bool */ - protected $useCustomModels=true; + protected $useCustomModels = true; /** @test */ public function it_can_use_custom_models() diff --git a/tests/Permission.php b/tests/Permission.php index 9587e0469..a621c53fc 100644 --- a/tests/Permission.php +++ b/tests/Permission.php @@ -5,7 +5,7 @@ class Permission extends \Spatie\Permission\Models\Permission { protected $visible = [ - 'id', + 'id', 'name', ]; } diff --git a/tests/Role.php b/tests/Role.php index f71b2378f..1977b00c2 100644 --- a/tests/Role.php +++ b/tests/Role.php @@ -5,7 +5,7 @@ class Role extends \Spatie\Permission\Models\Role { protected $visible = [ - 'id', + 'id', 'name', ]; } diff --git a/tests/TestCase.php b/tests/TestCase.php index e97b246ce..d4fe78cc6 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -32,7 +32,7 @@ abstract class TestCase extends Orchestra protected $testAdminPermission; /** @var bool */ - protected $useCustomModels=false; + protected $useCustomModels = false; public function setUp(): void { From 029884033dc6d495fcd24650a49cce34e0071502 Mon Sep 17 00:00:00 2001 From: Erik Niebla Date: Tue, 24 Aug 2021 17:38:26 -0500 Subject: [PATCH 0438/1013] Avoid BC break --- src/Models/Permission.php | 38 ------------------------------------- src/Models/Role.php | 30 ----------------------------- src/PermissionRegistrar.php | 26 ++++++++++++------------- 3 files changed, 13 insertions(+), 81 deletions(-) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 32aca39a6..a42b696cc 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -161,42 +161,4 @@ protected static function getPermission(array $params = []): ?PermissionContract { return static::getPermissions($params, true)->first(); } - - /** - * Fill model from array. - * - * @param array $attributes - */ - protected function fillModelFromArray(array $attributes) - { - if (isset($attributes['roles'])) { - $roleClass = app(PermissionRegistrar::class)->getRoleClass(); - $this->relations['roles'] = (new Collection($attributes['roles']))->map(function ($role) use ($roleClass) { - return $roleClass::getModelFromArray($role); - }); - unset($attributes['roles']); - } - - $this->attributes = $attributes; - if (isset($attributes['id'])) { - $this->exists = true; - $this->original['id'] = $attributes['id']; - } - - return $this; - } - - /** - * Get model from array. - * - * @param array $attributes - * - * @return \Spatie\Permission\Contracts\Permission - */ - public static function getModelFromArray(array $attributes): ?PermissionContract - { - $permission = new static; - - return $permission->fillModelFromArray($attributes); - } } diff --git a/src/Models/Role.php b/src/Models/Role.php index 6810b5ae5..5fd3177fc 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -157,34 +157,4 @@ public function hasPermissionTo($permission): bool return $this->permissions->contains('id', $permission->id); } - - /** - * Fill model from array. - * - * @param array $attributes - */ - protected function fillModelFromArray(array $attributes) - { - $this->attributes = $attributes; - if (isset($attributes['id'])) { - $this->exists = true; - $this->original['id'] = $attributes['id']; - } - - return $this; - } - - /** - * Get model from array. - * - * @param array $attributes - * - * @return \Spatie\Permission\Contracts\Role - */ - public static function getModelFromArray(array $attributes): ?RoleContract - { - $roles = new static; - - return $roles->fillModelFromArray($attributes); - } } diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 1b9879792..660eb9222 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -119,22 +119,22 @@ private function loadPermissions() { if ($this->permissions === null) { $this->permissions = $this->cache->remember(self::$cacheKey, self::$cacheExpirationTime, function () { - $permissions = $this->getPermissionClass()->select('id', 'name', 'guard_name') - ->with('roles:id,name,guard_name') - ->get(); - - if (! method_exists($this->getPermissionClass(), 'getModelFromArray')) { - return $permissions; - } - // make the cache smaller using an array with only required fields - return $permissions->map(function ($permission) { - return $permission->only('id', 'name', 'guard_name') + ['roles' => $permission->roles->map->only('id', 'name', 'guard_name')->all()]; - })->all(); + return $this->getPermissionClass()->select('id', 'name', 'guard_name') + ->with('roles:id,name,guard_name')->get() + ->map(function ($permission) { + return $permission->only('id', 'name', 'guard_name') + + ['roles' => $permission->roles->map->only('id', 'name', 'guard_name')->all()]; + })->all(); }); if (is_array($this->permissions)) { - $this->permissions = (new Collection($this->permissions))->map(function ($permission) { - return $this->permissionClass::getModelFromArray($permission); + $this->permissions = $this->getPermissionClass()::hydrate( + collect($this->permissions)->map(function ($item) { + return collect($item)->only('id', 'name', 'guard_name')->all(); + })->all() + ) + ->each(function ($permission, $i) { + $permission->setRelation('roles', $this->getRoleClass()::hydrate($this->permissions[$i]['roles'] ?? [])); }); } } From 0c80537bd67cb6a33b4ecc7df36c73b6dd3f69b8 Mon Sep 17 00:00:00 2001 From: Erik Niebla Date: Wed, 25 Aug 2021 09:45:11 -0500 Subject: [PATCH 0439/1013] Use alias for even smaller cache --- src/PermissionRegistrar.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 660eb9222..30c63d1e6 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -120,21 +120,25 @@ private function loadPermissions() if ($this->permissions === null) { $this->permissions = $this->cache->remember(self::$cacheKey, self::$cacheExpirationTime, function () { // make the cache smaller using an array with only required fields - return $this->getPermissionClass()->select('id', 'name', 'guard_name') - ->with('roles:id,name,guard_name')->get() + return $this->getPermissionClass()->select('id', 'id as i', 'name as n', 'guard_name as g') + ->with('roles:id,id as i,name as n,guard_name as g')->get() ->map(function ($permission) { - return $permission->only('id', 'name', 'guard_name') + - ['roles' => $permission->roles->map->only('id', 'name', 'guard_name')->all()]; + return $permission->only('i', 'n', 'g') + + ['r' => $permission->roles->map->only('i', 'n', 'g')->all()]; })->all(); }); if (is_array($this->permissions)) { $this->permissions = $this->getPermissionClass()::hydrate( collect($this->permissions)->map(function ($item) { - return collect($item)->only('id', 'name', 'guard_name')->all(); + return ['id' => $item['i'] ?? $item['id'], 'name' => $item['n'] ?? $item['name'], 'guard_name' => $item['g'] ?? $item['guard_name']]; })->all() ) ->each(function ($permission, $i) { - $permission->setRelation('roles', $this->getRoleClass()::hydrate($this->permissions[$i]['roles'] ?? [])); + $permission->setRelation('roles', $this->getRoleClass()::hydrate( + collect($this->permissions[$i]['r'] ?? $this->permissions[$i]['roles'] ?? [])->map(function ($item) { + return ['id' => $item['i'] ?? $item['id'], 'name' => $item['n'] ?? $item['name'], 'guard_name' => $item['g'] ?? $item['guard_name']]; + })->all() + )); }); } } From 7257756725c8e28706db1dc444e88558cc39d377 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 28 Aug 2021 19:24:45 -0400 Subject: [PATCH 0440/1013] Update CHANGELOG.md --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dc2d3e0c..42a896f54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file -## unreleased +## 4.4.0 - 2021-08-28 +- Avoid BC break (removed interface change) on cache change added in 4.3.0 #1826 +- Made cache even smaller #1826 - Avoid re-sync on non-persisted objects when firing Eloquent::saved #1819 ## 4.3.0 - 2021-08-17 From f57eca701320816e49fe557884e0bed3f627dee5 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 31 Aug 2021 00:27:49 -0400 Subject: [PATCH 0441/1013] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42a896f54..357d9b698 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to `laravel-permission` will be documented in this file +## PENDING 5.0.0 - 2021-08-31 +- Change default-guard-lookup to prefer current user's guard (see BC note in #1817 ) + + ## 4.4.0 - 2021-08-28 - Avoid BC break (removed interface change) on cache change added in 4.3.0 #1826 - Made cache even smaller #1826 From c1b27d19d86ff35f6290b2650321f19e45f36bdc Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 31 Aug 2021 00:32:00 -0400 Subject: [PATCH 0442/1013] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 357d9b698..16263e84e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to `laravel-permission` will be documented in this file ## PENDING 5.0.0 - 2021-08-31 - Change default-guard-lookup to prefer current user's guard (see BC note in #1817 ) +- Customized pivots instead of `role_id`,`permission_id` #1823 ## 4.4.0 - 2021-08-28 From e0868b4d7aa88b21e755ebbe6a20b9af086bf8c5 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 31 Aug 2021 04:15:26 -0400 Subject: [PATCH 0443/1013] Update composer.json --- composer.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/composer.json b/composer.json index 2de4b582e..86e3f8017 100644 --- a/composer.json +++ b/composer.json @@ -51,6 +51,9 @@ "providers": [ "Spatie\\Permission\\PermissionServiceProvider" ] + }, + "branch-alias": { + "dev-master": "5.x-dev" } }, "scripts": { From baeee799c73bca27b41e50f7ed76ae5da0a113a3 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 31 Aug 2021 04:29:13 -0400 Subject: [PATCH 0444/1013] Update TestCase.php --- tests/TestCase.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/TestCase.php b/tests/TestCase.php index 5b260dbd2..7fad8cd6f 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -83,6 +83,9 @@ protected function getEnvironmentSetUp($app) $app['config']->set('permission.column_names.permission_pivot_key', 'permission_test_id'); $app['config']->set('view.paths', [__DIR__.'/resources/views']); + // ensure api guard exists (required since Laravel 8.55) + $app['config']->set('auth.guards.api', ['driver' => 'session', 'provider' => 'users']); + // Set-up admin guard $app['config']->set('auth.guards.admin', ['driver' => 'session', 'provider' => 'admins']); $app['config']->set('auth.providers.admins', ['driver' => 'eloquent', 'model' => Admin::class]); From 80499095e3220d78eec2008f6ccfdf9bb72ca825 Mon Sep 17 00:00:00 2001 From: Erik Niebla Date: Tue, 31 Aug 2021 09:14:36 -0500 Subject: [PATCH 0445/1013] Teams support --- config/permission.php | 17 +++ database/migrations/add_teams_fields.php.stub | 73 ++++++++++ .../create_permission_tables.php.stub | 40 +++++- docs/basic-usage/artisan.md | 7 + docs/basic-usage/teams-permissions.md | 68 ++++++++++ docs/installation-laravel.md | 1 + docs/installation-lumen.md | 2 + src/Commands/CreatePermission.php | 2 +- src/Commands/CreateRole.php | 20 ++- src/Commands/Show.php | 35 +++-- src/Commands/UpgradeForTeams.php | 125 ++++++++++++++++++ src/Models/Role.php | 33 ++++- src/PermissionRegistrar.php | 27 ++++ src/PermissionServiceProvider.php | 1 + src/Traits/HasPermissions.php | 22 ++- src/Traits/HasRoles.php | 30 ++++- src/helpers.php | 11 ++ tests/CommandTest.php | 50 +++++++ tests/HasPermissionsTest.php | 4 +- tests/HasRolesTest.php | 2 +- tests/TeamHasPermissionsTest.php | 74 +++++++++++ tests/TeamHasRolesTest.php | 62 +++++++++ tests/TestCase.php | 20 ++- 23 files changed, 689 insertions(+), 37 deletions(-) create mode 100644 database/migrations/add_teams_fields.php.stub create mode 100644 docs/basic-usage/teams-permissions.md create mode 100644 src/Commands/UpgradeForTeams.php create mode 100644 tests/TeamHasPermissionsTest.php create mode 100644 tests/TeamHasRolesTest.php diff --git a/config/permission.php b/config/permission.php index 7e31a6c2a..c7029fa5b 100644 --- a/config/permission.php +++ b/config/permission.php @@ -87,8 +87,25 @@ */ 'model_morph_key' => 'model_id', + + /* + * Change this if you want to use the teams feature and your related model's + * foreign key is other than `team_id`. + */ + + 'team_foreign_key' => 'team_id', ], + /* + * When set to true the package implements teams using the 'team_foreign_key'. If you want + * the migrations to register the 'team_foreign_key', you must set this to true + * before doing the migration. If you already did the migration then you must make a new + * migration to also add 'team_foreign_key' to 'roles', 'model_has_roles', and + * 'model_has_permissions'(view the latest version of package's migration file) + */ + + 'teams' => false, + /* * When set to true, the required permission names are added to the exception * message. This could be considered an information leak in some contexts, so diff --git a/database/migrations/add_teams_fields.php.stub b/database/migrations/add_teams_fields.php.stub new file mode 100644 index 000000000..727104f17 --- /dev/null +++ b/database/migrations/add_teams_fields.php.stub @@ -0,0 +1,73 @@ +unsignedBigInteger($columnNames['team_foreign_key'])->nullable(); + $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index'); + } + }); + + Schema::table($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) { + if (! Schema::hasColumn($tableNames['model_has_permissions'], $columnNames['team_foreign_key'])) { + $table->unsignedBigInteger($columnNames['team_foreign_key'])->default('1');; + $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index'); + + $table->dropPrimary(); + $table->primary([$columnNames['team_foreign_key'], 'permission_id', $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } + }); + + Schema::table($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames) { + if (! Schema::hasColumn($tableNames['model_has_roles'], $columnNames['team_foreign_key'])) { + $table->unsignedBigInteger($columnNames['team_foreign_key'])->default('1');; + $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index'); + + $table->dropPrimary(); + $table->primary([$columnNames['team_foreign_key'], 'role_id', $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary'); + } + }); + + app('cache') + ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null) + ->forget(config('permission.cache.key')); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + + } +} diff --git a/database/migrations/create_permission_tables.php.stub b/database/migrations/create_permission_tables.php.stub index 5ad2571a3..f20ef752b 100644 --- a/database/migrations/create_permission_tables.php.stub +++ b/database/migrations/create_permission_tables.php.stub @@ -16,10 +16,14 @@ class CreatePermissionTables extends Migration { $tableNames = config('permission.table_names'); $columnNames = config('permission.column_names'); + $teams = config('permission.teams'); if (empty($tableNames)) { throw new \Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.'); } + if ($teams && empty($columnNames['team_foreign_key'] ?? null)) { + throw new \Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.'); + } Schema::create($tableNames['permissions'], function (Blueprint $table) { $table->bigIncrements('id'); @@ -30,16 +34,23 @@ class CreatePermissionTables extends Migration $table->unique(['name', 'guard_name']); }); - Schema::create($tableNames['roles'], function (Blueprint $table) { + Schema::create($tableNames['roles'], function (Blueprint $table) use ($teams, $columnNames) { $table->bigIncrements('id'); + if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing + $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable(); + $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index'); + } $table->string('name'); // For MySQL 8.0 use string('name', 125); $table->string('guard_name'); // For MySQL 8.0 use string('guard_name', 125); $table->timestamps(); - - $table->unique(['name', 'guard_name']); + if ($teams || config('permission.testing')) { + $table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']); + } else { + $table->unique(['name', 'guard_name']); + } }); - Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) { + Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames, $teams) { $table->unsignedBigInteger(PermissionRegistrar::$pivotPermission); $table->string('model_type'); @@ -50,12 +61,20 @@ class CreatePermissionTables extends Migration ->references('id') ->on($tableNames['permissions']) ->onDelete('cascade'); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index'); - $table->primary([PermissionRegistrar::$pivotPermission, $columnNames['model_morph_key'], 'model_type'], + $table->primary([$columnNames['team_foreign_key'], PermissionRegistrar::$pivotPermission, $columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_permission_model_type_primary'); + } else { + $table->primary([PermissionRegistrar::$pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } + }); - Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames) { + Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames, $teams) { $table->unsignedBigInteger(PermissionRegistrar::$pivotRole); $table->string('model_type'); @@ -66,9 +85,16 @@ class CreatePermissionTables extends Migration ->references('id') ->on($tableNames['roles']) ->onDelete('cascade'); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index'); - $table->primary([PermissionRegistrar::$pivotRole, $columnNames['model_morph_key'], 'model_type'], + $table->primary([$columnNames['team_foreign_key'], PermissionRegistrar::$pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary'); + } else { + $table->primary([PermissionRegistrar::$pivotRole, $columnNames['model_morph_key'], 'model_type'], 'model_has_roles_role_model_type_primary'); + } }); Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames) { diff --git a/docs/basic-usage/artisan.md b/docs/basic-usage/artisan.md index b8c98dd53..8cf340d1b 100644 --- a/docs/basic-usage/artisan.md +++ b/docs/basic-usage/artisan.md @@ -31,6 +31,13 @@ When creating roles you can also create and link permissions at the same time: php artisan permission:create-role writer web "create articles|edit articles" ``` +When creating roles with teams enabled you can set the team id by adding the `--team-id` parameter: + +```bash +php artisan permission:create-role --team-id=1 writer +php artisan permission:create-role writer api --team-id=1 +``` + ## Displaying roles and permissions in the console There is also a `show` command to show a table of roles and permissions per guard: diff --git a/docs/basic-usage/teams-permissions.md b/docs/basic-usage/teams-permissions.md new file mode 100644 index 000000000..58e0055b7 --- /dev/null +++ b/docs/basic-usage/teams-permissions.md @@ -0,0 +1,68 @@ +--- +title: Teams permissions +weight: 3 +--- + +NOTE: Those changes must be made before performing the migration. If you have already run the migration and want to upgrade your solution, you can run the artisan console command `php artisan permission:setup-teams`, to create a new migration file named [xxxx_xx_xx_xx_add_teams_fields.php](https://github.com/spatie/laravel-permission/blob/master/database/migrations/add_teams_fields.php.stub) and then run `php artisan migrate` to upgrade your database tables. + +When enabled, teams permissions offers you a flexible control for a variety of scenarios. The idea behind teams permissions is inspired by the default permission implementation of [Laratrust](https://laratrust.santigarcor.me/). + + +Teams permissions can be enabled in the permission config file: + +```php +// config/permission.php +'teams' => true, +``` + +Also, if you want to use a custom foreign key for teams you must change in the permission config file: +```php +// config/permission.php +'team_foreign_key' => 'custom_team_id', +``` + +## Working with Teams Permissions + +After implements on login a solution for select a team on authentication (for example set `team_id` of the current selected team on **session**: `session(['team_id' => $team->team_id]);` ), +we can set global `team_id` from anywhere, but works better if you create a `Middleware`, example: + +```php +namespace App\Http\Middleware; + +class TeamsPermission{ + + public function handle($request, \Closure $next){ + if(!empty(auth()->user())){ + // session value set on login + app(\Spatie\Permission\PermissionRegistrar::class)->setPermissionsTeamId(session('team_id')); + } + // other custom ways to get team_id + /*if(!empty(auth('api')->user())){ + // `getTeamIdFromToken()` example of custom method for getting the set team_id + app(\Spatie\Permission\PermissionRegistrar::class)->setPermissionsTeamId(auth('api')->user()->getTeamIdFromToken()); + }*/ + + return $next($request); + } +} +``` +NOTE: You must add your custom `Middleware` to `$middlewarePriority` on `app/Http/Kernel.php`. + +## Roles Creating + +When creating a role you can pass the `team_id` as an optional parameter + +```php +// with null team_id it creates a global role, global roles can be used from any team and they are unique +Role::create(['name' => 'writer', 'team_id' => null]); + +// creates a role with team_id = 1, team roles can have the same name on different teams +Role::create(['name' => 'reader', 'team_id' => 1]); + +// creating a role without team_id makes the role take the default global team_id +Role::create(['name' => 'reviewer']); +``` + +## Roles/Permissions Assignment & Removal + +The role/permission assignment and removal are the same, but they take the global `team_id` set on login for sync. diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 5d6baf281..c87a5b29f 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -33,6 +33,7 @@ This package can be used with Laravel 6.0 or higher. ``` 6. NOTE: If you are using UUIDs, see the Advanced section of the docs on UUID steps, before you continue. It explains some changes you may want to make to the migrations and config file before continuing. It also mentions important considerations after extending this package's models for UUID capability. + If you are going to use teams feature, you have to update your [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) and set `'teams' => true,`, if you want to use a custom foreign key for teams you must change `team_foreign_key`. 7. Clear your config cache. This package requires access to the `permission` config. Generally it's bad practice to do config-caching in a development environment. If you've been caching configurations locally, clear your config cache with either of these commands: diff --git a/docs/installation-lumen.md b/docs/installation-lumen.md index be484756b..e1a825170 100644 --- a/docs/installation-lumen.md +++ b/docs/installation-lumen.md @@ -52,6 +52,8 @@ $app->register(App\Providers\AuthServiceProvider::class); Ensure the application's database name/credentials are set in your `.env` (or `config/database.php` if you have one), and that the database exists. +NOTE: If you are going to use teams feature, you have to update your [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) and set `'teams' => true,`, if you want to use a custom foreign key for teams you must change `team_foreign_key`. + Run the migrations to create the tables for this package: ```bash diff --git a/src/Commands/CreatePermission.php b/src/Commands/CreatePermission.php index f71a63240..c3bc20693 100644 --- a/src/Commands/CreatePermission.php +++ b/src/Commands/CreatePermission.php @@ -19,6 +19,6 @@ public function handle() $permission = $permissionClass::findOrCreate($this->argument('name'), $this->argument('guard')); - $this->info("Permission `{$permission->name}` created"); + $this->info("Permission `{$permission->name}` ".($permission->wasRecentlyCreated ? 'created' : 'already exists')); } } diff --git a/src/Commands/CreateRole.php b/src/Commands/CreateRole.php index 426d6f0c7..805d5eeb3 100644 --- a/src/Commands/CreateRole.php +++ b/src/Commands/CreateRole.php @@ -5,13 +5,15 @@ use Illuminate\Console\Command; use Spatie\Permission\Contracts\Permission as PermissionContract; use Spatie\Permission\Contracts\Role as RoleContract; +use Spatie\Permission\PermissionRegistrar; class CreateRole extends Command { protected $signature = 'permission:create-role {name : The name of the role} {guard? : The name of the guard} - {permissions? : A list of permissions to assign to the role, separated by | }'; + {permissions? : A list of permissions to assign to the role, separated by | } + {--team-id=}'; protected $description = 'Create a role'; @@ -19,11 +21,25 @@ public function handle() { $roleClass = app(RoleContract::class); + $teamIdAux = app(PermissionRegistrar::class)->getPermissionsTeamId(); + app(PermissionRegistrar::class)->setPermissionsTeamId($this->option('team-id') ?: null); + + if (! PermissionRegistrar::$teams && $this->option('team-id')) { + $this->warn("Teams feature disabled, argument --team-id has no effect. Either enable it in permissions config file or remove --team-id parameter"); + return; + } + $role = $roleClass::findOrCreate($this->argument('name'), $this->argument('guard')); + app(PermissionRegistrar::class)->setPermissionsTeamId($teamIdAux); + + $teams_key = PermissionRegistrar::$teamsKey; + if (PermissionRegistrar::$teams && $this->option('team-id') && is_null($role->$teams_key)) { + $this->warn("Role `{$role->name}` already exists on the global team; argument --team-id has no effect"); + } $role->givePermissionTo($this->makePermissions($this->argument('permissions'))); - $this->info("Role `{$role->name}` created"); + $this->info("Role `{$role->name}` ".($role->wasRecentlyCreated ? 'created' : 'updated')); } /** diff --git a/src/Commands/Show.php b/src/Commands/Show.php index c0aa0e33d..3aded0491 100644 --- a/src/Commands/Show.php +++ b/src/Commands/Show.php @@ -6,6 +6,7 @@ use Illuminate\Support\Collection; use Spatie\Permission\Contracts\Permission as PermissionContract; use Spatie\Permission\Contracts\Role as RoleContract; +use Symfony\Component\Console\Helper\TableCell; class Show extends Command { @@ -19,6 +20,7 @@ public function handle() { $permissionClass = app(PermissionContract::class); $roleClass = app(RoleContract::class); + $team_key = config('permission.column_names.team_foreign_key'); $style = $this->argument('style') ?? 'default'; $guard = $this->argument('guard'); @@ -32,20 +34,37 @@ public function handle() foreach ($guards as $guard) { $this->info("Guard: $guard"); - $roles = $roleClass::whereGuardName($guard)->orderBy('name')->get()->mapWithKeys(function ($role) { - return [$role->name => $role->permissions->pluck('name')]; - }); + $roles = $roleClass::whereGuardName($guard) + ->when(config('permission.teams'), function ($q) use ($team_key) { + $q->orderBy($team_key); + }) + ->orderBy('name')->get()->mapWithKeys(function ($role) use ($team_key) { + return [$role->name.'_'.($role->$team_key ?: '') => ['permissions' => $role->permissions->pluck('id'), $team_key => $role->$team_key ]]; + }); - $permissions = $permissionClass::whereGuardName($guard)->orderBy('name')->pluck('name'); + $permissions = $permissionClass::whereGuardName($guard)->orderBy('name')->pluck('name', 'id'); - $body = $permissions->map(function ($permission) use ($roles) { - return $roles->map(function (Collection $role_permissions) use ($permission) { - return $role_permissions->contains($permission) ? ' ✔' : ' ·'; + $body = $permissions->map(function ($permission, $id) use ($roles) { + return $roles->map(function (array $role_data) use ($id) { + return $role_data['permissions']->contains($id) ? ' ✔' : ' ·'; })->prepend($permission); }); + if (config('permission.teams')) { + $teams = $roles->groupBy($team_key)->values()->map(function ($group, $id) { + return new TableCell('Team ID: '.($id ?: 'NULL'), ['colspan' => $group->count()]); + }); + } + $this->table( - $roles->keys()->prepend('')->toArray(), + array_merge([ + config('permission.teams') ? $teams->prepend('')->toArray() : [], + $roles->keys()->map(function ($val) { + $name = explode('_', $val); + return $name[0]; + }) + ->prepend('')->toArray() + ]), $body->toArray(), $style ); diff --git a/src/Commands/UpgradeForTeams.php b/src/Commands/UpgradeForTeams.php new file mode 100644 index 000000000..7e70ef5f8 --- /dev/null +++ b/src/Commands/UpgradeForTeams.php @@ -0,0 +1,125 @@ +error('Teams feature is disabled in your permission.php file.'); + $this->warn('Please enable the teams setting in your configuration.'); + return; + } + + $this->line(''); + $this->info("The teams feature setup is going to add a migration and a model"); + + $existingMigrations = $this->alreadyExistingMigrations(); + + if ($existingMigrations) { + $this->line(''); + + $this->warn($this->getExistingMigrationsWarning($existingMigrations)); + } + + $this->line(''); + + if (! $this->confirm("Proceed with the migration creation?", "yes")) { + return; + } + + $this->line(''); + + $this->line("Creating migration"); + + if ($this->createMigration()) { + $this->info("Migration created successfully."); + } else { + $this->error( + "Couldn't create migration.\n". + "Check the write permissions within the database/migrations directory." + ); + } + + $this->line(''); + } + + /** + * Create the migration. + * + * @return bool + */ + protected function createMigration() + { + try { + $migrationStub = __DIR__."/../../database/migrations/{$this->migrationSuffix}.stub"; + copy($migrationStub, $this->getMigrationPath()); + return true; + } catch (\Throwable $e) { + $this->error($e->getMessage()); + return false; + } + } + + /** + * Build a warning regarding possible duplication + * due to already existing migrations. + * + * @param array $existingMigrations + * @return string + */ + protected function getExistingMigrationsWarning(array $existingMigrations) + { + if (count($existingMigrations) > 1) { + $base = "Setup teams migrations already exist.\nFollowing files were found: "; + } else { + $base = "Setup teams migration already exists.\nFollowing file was found: "; + } + + return $base . array_reduce($existingMigrations, function ($carry, $fileName) { + return $carry . "\n - " . $fileName; + }); + } + + /** + * Check if there is another migration + * with the same suffix. + * + * @return array + */ + protected function alreadyExistingMigrations() + { + $matchingFiles = glob($this->getMigrationPath('*')); + + return array_map(function ($path) { + return basename($path); + }, $matchingFiles); + } + + /** + * Get the migration path. + * + * The date parameter is optional for ability + * to provide a custom value or a wildcard. + * + * @param string|null $date + * @return string + */ + protected function getMigrationPath($date = null) + { + $date = $date ?: date('Y_m_d_His'); + + return database_path("migrations/${date}_{$this->migrationSuffix}"); + } +} diff --git a/src/Models/Role.php b/src/Models/Role.php index 11c497280..3ef1a8313 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -36,7 +36,15 @@ public static function create(array $attributes = []) { $attributes['guard_name'] = $attributes['guard_name'] ?? Guard::getDefaultName(static::class); - if (static::where('name', $attributes['name'])->where('guard_name', $attributes['guard_name'])->first()) { + $params = ['name' => $attributes['name'], 'guard_name' => $attributes['guard_name']]; + if (PermissionRegistrar::$teams) { + if (array_key_exists(PermissionRegistrar::$teamsKey, $attributes)) { + $params[PermissionRegistrar::$teamsKey] = $attributes[PermissionRegistrar::$teamsKey]; + } else { + $attributes[PermissionRegistrar::$teamsKey] = app(PermissionRegistrar::class)->getPermissionsTeamId(); + } + } + if (static::findByParam($params)) { throw RoleAlreadyExists::create($attributes['name'], $attributes['guard_name']); } @@ -84,7 +92,7 @@ public static function findByName(string $name, $guardName = null): RoleContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); - $role = static::where('name', $name)->where('guard_name', $guardName)->first(); + $role = static::findByParam(['name' => $name, 'guard_name' => $guardName]); if (! $role) { throw RoleDoesNotExist::named($name); @@ -97,7 +105,7 @@ public static function findById(int $id, $guardName = null): RoleContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); - $role = static::where('id', $id)->where('guard_name', $guardName)->first(); + $role = static::findByParam(['id' => $id, 'guard_name' => $guardName]); if (! $role) { throw RoleDoesNotExist::withId($id); @@ -118,15 +126,30 @@ public static function findOrCreate(string $name, $guardName = null): RoleContra { $guardName = $guardName ?? Guard::getDefaultName(static::class); - $role = static::where('name', $name)->where('guard_name', $guardName)->first(); + $role = static::findByParam(['name' => $name, 'guard_name' => $guardName]); if (! $role) { - return static::query()->create(['name' => $name, 'guard_name' => $guardName]); + return static::query()->create(['name' => $name, 'guard_name' => $guardName] + (PermissionRegistrar::$teams ? [PermissionRegistrar::$teamsKey => app(PermissionRegistrar::class)->getPermissionsTeamId()] : [])); } return $role; } + protected static function findByParam(array $params = []) + { + $query = static::when(PermissionRegistrar::$teams, function ($q) use ($params) { + $q->where(function ($q) use ($params) { + $q->whereNull(PermissionRegistrar::$teamsKey) + ->orWhere(PermissionRegistrar::$teamsKey, $params[PermissionRegistrar::$teamsKey] ?? app(PermissionRegistrar::class)->getPermissionsTeamId()); + }); + }); + unset($params[PermissionRegistrar::$teamsKey]); + foreach ($params as $key => $value) { + $query->where($key, $value); + } + return $query->first(); + } + /** * Determine if the user may perform the given permission. * diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 4f23c9f7f..b3ae4a7b5 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -35,6 +35,15 @@ class PermissionRegistrar /** @var \DateInterval|int */ public static $cacheExpirationTime; + /** @var bool */ + public static $teams; + + /** @var string */ + public static $teamsKey; + + /** @var int */ + protected $teamId = null; + /** @var string */ public static $cacheKey; @@ -56,6 +65,9 @@ public function initializeCache() { self::$cacheExpirationTime = config('permission.cache.expiration_time') ?: \DateInterval::createFromDateString('24 hours'); + self::$teams = config('permission.teams', false); + self::$teamsKey = config('permission.column_names.team_foreign_key'); + self::$cacheKey = config('permission.cache.key'); self::$pivotRole = config('permission.column_names.role_pivot_key') ?: 'role_id'; @@ -83,6 +95,21 @@ protected function getCacheStoreFromConfig(): \Illuminate\Contracts\Cache\Reposi return $this->cacheManager->store($cacheDriver); } + /** + * Set the team id for teams/groups support, this id is used when querying permissions/roles + * + * @param int $id + */ + public function setPermissionsTeamId(?int $id) + { + $this->teamId = $id; + } + + public function getPermissionsTeamId(): ?int + { + return $this->teamId; + } + /** * Register the permission check method on the gate. * We resolve the Gate fresh here, for benefit of long-running instances. diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 6a237fece..c0ae0ed15 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -63,6 +63,7 @@ protected function registerCommands() Commands\CreateRole::class, Commands\CreatePermission::class, Commands\Show::class, + Commands\UpgradeForTeams::class, ]); } diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index b0776f089..7d3451db4 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Support\Collection; use Spatie\Permission\Contracts\Permission; +use Spatie\Permission\Contracts\Role; use Spatie\Permission\Exceptions\GuardDoesNotMatch; use Spatie\Permission\Exceptions\PermissionDoesNotExist; use Spatie\Permission\Exceptions\WildcardPermissionInvalidArgument; @@ -48,7 +49,12 @@ public function permissions(): BelongsToMany config('permission.table_names.model_has_permissions'), config('permission.column_names.model_morph_key'), PermissionRegistrar::$pivotPermission - ); + ) + ->where(function ($q) { + $q->when(PermissionRegistrar::$teams, function ($q) { + $q->where(PermissionRegistrar::$teamsKey, app(PermissionRegistrar::class)->getPermissionsTeamId()); + }); + }); } /** @@ -335,13 +341,21 @@ public function givePermissionTo(...$permissions) ->each(function ($permission) { $this->ensureModelSharesGuard($permission); }) - ->map->id - ->all(); + ->map(function ($permission) { + return ['id' => $permission->id, 'values' => PermissionRegistrar::$teams && !is_a($this, Role::class) ? + [PermissionRegistrar::$teamsKey => app(PermissionRegistrar::class)->getPermissionsTeamId()] : [] + ]; + }) + ->pluck('values', 'id')->toArray(); $model = $this->getModel(); if ($model->exists) { - $this->permissions()->sync($permissions, false); + if (PermissionRegistrar::$teams && !is_a($this, Role::class)) { + $this->permissions()->wherePivot(PermissionRegistrar::$teamsKey, app(PermissionRegistrar::class)->getPermissionsTeamId())->sync($permissions, false); + } else { + $this->permissions()->sync($permissions, false); + } $model->load('permissions'); } else { $class = \get_class($model); diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index e5470322d..b0003e5d3 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -5,6 +5,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Support\Collection; +use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Contracts\Role; use Spatie\Permission\PermissionRegistrar; @@ -39,13 +40,24 @@ public function getRoleClass() */ public function roles(): BelongsToMany { + $model_has_roles = config('permission.table_names.model_has_roles'); return $this->morphToMany( config('permission.models.role'), 'model', - config('permission.table_names.model_has_roles'), + $model_has_roles, config('permission.column_names.model_morph_key'), PermissionRegistrar::$pivotRole - ); + ) + ->where(function ($q) use ($model_has_roles) { + $q->when(PermissionRegistrar::$teams, function ($q) use ($model_has_roles) { + $teamId = app(PermissionRegistrar::class)->getPermissionsTeamId(); + $q->where($model_has_roles.'.'.PermissionRegistrar::$teamsKey, $teamId) + ->where(function ($q) use ($teamId) { + $teamField = config('permission.table_names.roles').'.'.PermissionRegistrar::$teamsKey; + $q->whereNull($teamField)->orWhere($teamField, $teamId); + }); + }); + }); } /** @@ -107,13 +119,21 @@ public function assignRole(...$roles) ->each(function ($role) { $this->ensureModelSharesGuard($role); }) - ->map->id - ->all(); + ->map(function ($role) { + return ['id' => $role->id, 'values' => PermissionRegistrar::$teams && !is_a($this, Permission::class) ? + [PermissionRegistrar::$teamsKey => app(PermissionRegistrar::class)->getPermissionsTeamId()] : [] + ]; + }) + ->pluck('values', 'id')->toArray(); $model = $this->getModel(); if ($model->exists) { - $this->roles()->sync($roles, false); + if (PermissionRegistrar::$teams && !is_a($this, Permission::class)) { + $this->roles()->wherePivot(PermissionRegistrar::$teamsKey, app(PermissionRegistrar::class)->getPermissionsTeamId())->sync($roles, false); + } else { + $this->roles()->sync($roles, false); + } $model->load('roles'); } else { $class = \get_class($model); diff --git a/src/helpers.php b/src/helpers.php index 7f79e5764..2b80ea417 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -18,3 +18,14 @@ function getModelForGuard(string $guard) })->get($guard); } } + +if (! function_exists('setPermissionsTeamId')) { + /** + * @param int $id + * + */ + function setPermissionsTeamId(int $id) + { + app(\Spatie\Permission\PermissionRegistrar::class)->setPermissionsTeamId($id); + } +} diff --git a/tests/CommandTest.php b/tests/CommandTest.php index 2071e45a9..603af80d2 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -130,4 +130,54 @@ public function it_can_show_permissions_for_guard() $this->assertTrue(strpos($output, 'Guard: web') !== false); $this->assertTrue(strpos($output, 'Guard: admin') === false); } + + /** @test */ + public function it_can_setup_teams_upgrade() + { + config()->set('permission.teams', true); + + $this->artisan('permission:setup-teams') + ->expectsQuestion('Proceed with the migration creation?', 'yes') + ->assertExitCode(0); + + $matchingFiles = glob(database_path('migrations/*_add_teams_fields.php')); + $this->assertTrue(count($matchingFiles) > 0); + + include_once $matchingFiles[count($matchingFiles)-1]; + (new \AddTeamsFields())->up(); + (new \AddTeamsFields())->up(); //test upgrade teams migration fresh + + Role::create(['name' => 'new-role', 'team_test_id' => 1]); + $role = Role::where('name', 'new-role')->first(); + $this->assertNotNull($role); + $this->assertSame(1, (int) $role->team_test_id); + + // remove migration + foreach ($matchingFiles as $file) { + unlink($file); + } + } + + /** @test */ + public function it_can_show_roles_by_teams() + { + config()->set('permission.teams', true); + app(\Spatie\Permission\PermissionRegistrar::class)->initializeCache(); + + Role::create(['name' => 'testRoleTeam', 'team_test_id' => 1]); + Role::create(['name' => 'testRoleTeam', 'team_test_id' => 2]); // same name different team + Artisan::call('permission:show'); + + $output = Artisan::output(); + + // | | Team ID: NULL | Team ID: 1 | Team ID: 2 | + // | | testRole | testRole2 | testRoleTeam | testRoleTeam | + if (method_exists($this, 'assertMatchesRegularExpression')) { + $this->assertMatchesRegularExpression('/\|\s+\|\s+Team ID: NULL\s+\|\s+Team ID: 1\s+\|\s+Team ID: 2\s+\|/', $output); + $this->assertMatchesRegularExpression('/\|\s+\|\s+testRole\s+\|\s+testRole2\s+\|\s+testRoleTeam\s+\|\s+testRoleTeam\s+\|/', $output); + } else { // phpUnit 9/8 + $this->assertRegExp('/\|\s+\|\s+Team ID: NULL\s+\|\s+Team ID: 1\s+\|\s+Team ID: 2\s+\|/', $output); + $this->assertRegExp('/\|\s+\|\s+testRole\s+\|\s+testRole2\s+\|\s+testRoleTeam\s+\|\s+testRoleTeam\s+\|/', $output); + } + } } diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index c67c20c68..7507837b6 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -516,8 +516,8 @@ public function it_can_retrieve_permission_names() { $this->testUser->givePermissionTo('edit-news', 'edit-articles'); $this->assertEquals( - collect(['edit-news', 'edit-articles']), - $this->testUser->getPermissionNames() + collect(['edit-articles', 'edit-news']), + $this->testUser->getPermissionNames()->sort()->values() ); } diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 405e6eac1..daa556444 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -568,7 +568,7 @@ public function it_can_retrieve_role_names() $this->assertEquals( collect(['testRole', 'testRole2']), - $this->testUser->getRoleNames() + $this->testUser->getRoleNames()->sort()->values() ); } diff --git a/tests/TeamHasPermissionsTest.php b/tests/TeamHasPermissionsTest.php new file mode 100644 index 000000000..9170166ae --- /dev/null +++ b/tests/TeamHasPermissionsTest.php @@ -0,0 +1,74 @@ +setPermissionsTeamId(1); + $this->testUser->load('permissions'); + $this->testUser->givePermissionTo('edit-articles', 'edit-news'); + + $this->setPermissionsTeamId(2); + $this->testUser->load('permissions'); + $this->testUser->givePermissionTo('edit-articles', 'edit-blog'); + + $this->setPermissionsTeamId(1); + $this->testUser->load('permissions'); + $this->assertEquals( + collect(['edit-articles', 'edit-news']), + $this->testUser->getPermissionNames()->sort()->values() + ); + $this->assertTrue($this->testUser->hasAllDirectPermissions(['edit-articles', 'edit-news'])); + $this->assertFalse($this->testUser->hasAllDirectPermissions(['edit-articles', 'edit-blog'])); + + $this->setPermissionsTeamId(2); + $this->testUser->load('permissions'); + $this->assertEquals( + collect(['edit-articles', 'edit-blog']), + $this->testUser->getPermissionNames()->sort()->values() + ); + $this->assertTrue($this->testUser->hasAllDirectPermissions(['edit-articles', 'edit-blog'])); + $this->assertFalse($this->testUser->hasAllDirectPermissions(['edit-articles', 'edit-news'])); + } + + /** @test */ + public function it_can_list_all_the_coupled_permissions_both_directly_and_via_roles_on_same_user_on_different_teams() + { + $this->testUserRole->givePermissionTo('edit-articles'); + + $this->setPermissionsTeamId(1); + $this->testUser->load('permissions'); + $this->testUser->assignRole('testRole'); + $this->testUser->givePermissionTo('edit-news'); + + $this->setPermissionsTeamId(2); + $this->testUser->load('permissions'); + $this->testUser->assignRole('testRole'); + $this->testUser->givePermissionTo('edit-blog'); + + $this->setPermissionsTeamId(1); + $this->testUser->load('roles'); + $this->testUser->load('permissions'); + + $this->assertEquals( + collect(['edit-articles', 'edit-news']), + $this->testUser->getAllPermissions()->pluck('name')->sort()->values() + ); + + $this->setPermissionsTeamId(2); + $this->testUser->load('roles'); + $this->testUser->load('permissions'); + + $this->assertEquals( + collect(['edit-articles', 'edit-blog']), + $this->testUser->getAllPermissions()->pluck('name')->sort()->values() + ); + } +} diff --git a/tests/TeamHasRolesTest.php b/tests/TeamHasRolesTest.php new file mode 100644 index 000000000..cdf120513 --- /dev/null +++ b/tests/TeamHasRolesTest.php @@ -0,0 +1,62 @@ +create(['name' => 'testRole3']); //team_test_id = 1 by main class + app(Role::class)->create(['name' => 'testRole3', 'team_test_id' => 2]); + app(Role::class)->create(['name' => 'testRole4', 'team_test_id' => null]); //global role + + $testRole3Team1 = app(Role::class)->where(['name' => 'testRole3', 'team_test_id' => 1])->first(); + $testRole3Team2 = app(Role::class)->where(['name' => 'testRole3', 'team_test_id' => 2])->first(); + $testRole4NoTeam = app(Role::class)->where(['name' => 'testRole4', 'team_test_id' => null])->first(); + $this->assertNotNull($testRole3Team1); + $this->assertNotNull($testRole4NoTeam); + + $this->setPermissionsTeamId(1); + $this->testUser->load('roles'); + + $this->testUser->assignRole('testRole', 'testRole2'); + + $this->setPermissionsTeamId(2); + $this->testUser->load('roles'); + + $this->testUser->assignRole('testRole', 'testRole3'); + + $this->setPermissionsTeamId(1); + $this->testUser->load('roles'); + + $this->assertEquals( + collect(['testRole', 'testRole2']), + $this->testUser->getRoleNames()->sort()->values() + ); + $this->assertTrue($this->testUser->hasExactRoles(['testRole', 'testRole2'])); + + $this->testUser->assignRole('testRole3', 'testRole4'); + $this->assertTrue($this->testUser->hasExactRoles(['testRole', 'testRole2', 'testRole3', 'testRole4'])); + $this->assertTrue($this->testUser->hasRole($testRole3Team1)); //testRole3 team=1 + $this->assertTrue($this->testUser->hasRole($testRole4NoTeam)); // global role team=null + + $this->setPermissionsTeamId(2); + $this->testUser->load('roles'); + + $this->assertEquals( + collect(['testRole', 'testRole3']), + $this->testUser->getRoleNames()->sort()->values() + ); + $this->assertTrue($this->testUser->hasExactRoles(['testRole', 'testRole3'])); + $this->assertTrue($this->testUser->hasRole($testRole3Team2)); //testRole3 team=2 + $this->testUser->assignRole('testRole4'); + $this->assertTrue($this->testUser->hasExactRoles(['testRole', 'testRole3', 'testRole4'])); + $this->assertTrue($this->testUser->hasRole($testRole4NoTeam)); // global role team=null + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 7fad8cd6f..1ea141566 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -36,12 +36,18 @@ abstract class TestCase extends Orchestra /** @var bool */ protected $useCustomModels = false; + /** @var bool */ + protected $hasTeams=false; + public function setUp(): void { parent::setUp(); // Note: this also flushes the cache from within the migration $this->setUpDatabase($this->app); + if ($this->hasTeams) { + $this->setPermissionsTeamId(1); + } $this->testUser = User::first(); $this->testUserRole = app(Role::class)->find(1); @@ -73,6 +79,10 @@ protected function getPackageProviders($app) */ protected function getEnvironmentSetUp($app) { + $app['config']->set('permission.teams', $this->hasTeams); + $app['config']->set('permission.testing', true); //fix sqlite + $app['config']->set('permission.column_names.model_morph_key', 'model_test_id'); + $app['config']->set('permission.column_names.team_foreign_key', 'team_test_id'); $app['config']->set('database.default', 'sqlite'); $app['config']->set('database.connections.sqlite', [ 'driver' => 'sqlite', @@ -106,8 +116,6 @@ protected function getEnvironmentSetUp($app) */ protected function setUpDatabase($app) { - $app['config']->set('permission.column_names.model_morph_key', 'model_test_id'); - $app['db']->connection()->getSchemaBuilder()->create('users', function (Blueprint $table) { $table->increments('id'); $table->string('email'); @@ -148,6 +156,14 @@ protected function reloadPermissions() app(PermissionRegistrar::class)->forgetCachedPermissions(); } + /** + * Change the team_id + */ + protected function setPermissionsTeamId(int $id) + { + app(PermissionRegistrar::class)->setPermissionsTeamId($id); + } + public function createCacheTable() { Schema::create('cache', function ($table) { From faf255feca50ffc32a33703a94aae870714c07bd Mon Sep 17 00:00:00 2001 From: drbyte Date: Tue, 31 Aug 2021 18:10:52 +0000 Subject: [PATCH 0446/1013] Fix styling --- src/Commands/CreateRole.php | 1 + src/Commands/Show.php | 3 ++- src/Commands/UpgradeForTeams.php | 6 ++++-- src/Models/Role.php | 3 ++- src/Traits/HasPermissions.php | 6 +++--- src/Traits/HasRoles.php | 7 ++++--- tests/CommandTest.php | 2 +- tests/TeamHasPermissionsTest.php | 3 +-- tests/TeamHasRolesTest.php | 2 +- tests/TestCase.php | 2 +- 10 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/Commands/CreateRole.php b/src/Commands/CreateRole.php index 805d5eeb3..fdd76ad10 100644 --- a/src/Commands/CreateRole.php +++ b/src/Commands/CreateRole.php @@ -26,6 +26,7 @@ public function handle() if (! PermissionRegistrar::$teams && $this->option('team-id')) { $this->warn("Teams feature disabled, argument --team-id has no effect. Either enable it in permissions config file or remove --team-id parameter"); + return; } diff --git a/src/Commands/Show.php b/src/Commands/Show.php index 3aded0491..f7cc2b343 100644 --- a/src/Commands/Show.php +++ b/src/Commands/Show.php @@ -61,9 +61,10 @@ public function handle() config('permission.teams') ? $teams->prepend('')->toArray() : [], $roles->keys()->map(function ($val) { $name = explode('_', $val); + return $name[0]; }) - ->prepend('')->toArray() + ->prepend('')->toArray(), ]), $body->toArray(), $style diff --git a/src/Commands/UpgradeForTeams.php b/src/Commands/UpgradeForTeams.php index 7e70ef5f8..39dd1376c 100644 --- a/src/Commands/UpgradeForTeams.php +++ b/src/Commands/UpgradeForTeams.php @@ -7,7 +7,6 @@ class UpgradeForTeams extends Command { - protected $signature = 'permission:setup-teams'; protected $description = 'Setup the teams feature by generating the associated migration.'; @@ -16,9 +15,10 @@ class UpgradeForTeams extends Command public function handle() { - if (!Config::get('permission.teams')) { + if (! Config::get('permission.teams')) { $this->error('Teams feature is disabled in your permission.php file.'); $this->warn('Please enable the teams setting in your configuration.'); + return; } @@ -65,9 +65,11 @@ protected function createMigration() try { $migrationStub = __DIR__."/../../database/migrations/{$this->migrationSuffix}.stub"; copy($migrationStub, $this->getMigrationPath()); + return true; } catch (\Throwable $e) { $this->error($e->getMessage()); + return false; } } diff --git a/src/Models/Role.php b/src/Models/Role.php index 3ef1a8313..a0fd14e92 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -105,7 +105,7 @@ public static function findById(int $id, $guardName = null): RoleContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); - $role = static::findByParam(['id' => $id, 'guard_name' => $guardName]); + $role = static::findByParam(['id' => $id, 'guard_name' => $guardName]); if (! $role) { throw RoleDoesNotExist::withId($id); @@ -147,6 +147,7 @@ protected static function findByParam(array $params = []) foreach ($params as $key => $value) { $query->where($key, $value); } + return $query->first(); } diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 7d3451db4..128351186 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -342,8 +342,8 @@ public function givePermissionTo(...$permissions) $this->ensureModelSharesGuard($permission); }) ->map(function ($permission) { - return ['id' => $permission->id, 'values' => PermissionRegistrar::$teams && !is_a($this, Role::class) ? - [PermissionRegistrar::$teamsKey => app(PermissionRegistrar::class)->getPermissionsTeamId()] : [] + return ['id' => $permission->id, 'values' => PermissionRegistrar::$teams && ! is_a($this, Role::class) ? + [PermissionRegistrar::$teamsKey => app(PermissionRegistrar::class)->getPermissionsTeamId()] : [], ]; }) ->pluck('values', 'id')->toArray(); @@ -351,7 +351,7 @@ public function givePermissionTo(...$permissions) $model = $this->getModel(); if ($model->exists) { - if (PermissionRegistrar::$teams && !is_a($this, Role::class)) { + if (PermissionRegistrar::$teams && ! is_a($this, Role::class)) { $this->permissions()->wherePivot(PermissionRegistrar::$teamsKey, app(PermissionRegistrar::class)->getPermissionsTeamId())->sync($permissions, false); } else { $this->permissions()->sync($permissions, false); diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index b0003e5d3..f9f70b498 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -41,6 +41,7 @@ public function getRoleClass() public function roles(): BelongsToMany { $model_has_roles = config('permission.table_names.model_has_roles'); + return $this->morphToMany( config('permission.models.role'), 'model', @@ -120,8 +121,8 @@ public function assignRole(...$roles) $this->ensureModelSharesGuard($role); }) ->map(function ($role) { - return ['id' => $role->id, 'values' => PermissionRegistrar::$teams && !is_a($this, Permission::class) ? - [PermissionRegistrar::$teamsKey => app(PermissionRegistrar::class)->getPermissionsTeamId()] : [] + return ['id' => $role->id, 'values' => PermissionRegistrar::$teams && ! is_a($this, Permission::class) ? + [PermissionRegistrar::$teamsKey => app(PermissionRegistrar::class)->getPermissionsTeamId()] : [], ]; }) ->pluck('values', 'id')->toArray(); @@ -129,7 +130,7 @@ public function assignRole(...$roles) $model = $this->getModel(); if ($model->exists) { - if (PermissionRegistrar::$teams && !is_a($this, Permission::class)) { + if (PermissionRegistrar::$teams && ! is_a($this, Permission::class)) { $this->roles()->wherePivot(PermissionRegistrar::$teamsKey, app(PermissionRegistrar::class)->getPermissionsTeamId())->sync($roles, false); } else { $this->roles()->sync($roles, false); diff --git a/tests/CommandTest.php b/tests/CommandTest.php index 603af80d2..be1541f88 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -143,7 +143,7 @@ public function it_can_setup_teams_upgrade() $matchingFiles = glob(database_path('migrations/*_add_teams_fields.php')); $this->assertTrue(count($matchingFiles) > 0); - include_once $matchingFiles[count($matchingFiles)-1]; + include_once $matchingFiles[count($matchingFiles) - 1]; (new \AddTeamsFields())->up(); (new \AddTeamsFields())->up(); //test upgrade teams migration fresh diff --git a/tests/TeamHasPermissionsTest.php b/tests/TeamHasPermissionsTest.php index 9170166ae..62d3545e0 100644 --- a/tests/TeamHasPermissionsTest.php +++ b/tests/TeamHasPermissionsTest.php @@ -5,8 +5,7 @@ class TeamHasPermissionsTest extends HasPermissionsTest { /** @var bool */ - protected $hasTeams=true; - + protected $hasTeams = true; /** @test */ public function it_can_assign_same_and_different_permission_on_same_user_on_different_teams() diff --git a/tests/TeamHasRolesTest.php b/tests/TeamHasRolesTest.php index cdf120513..19c745e2a 100644 --- a/tests/TeamHasRolesTest.php +++ b/tests/TeamHasRolesTest.php @@ -7,7 +7,7 @@ class TeamHasRolesTest extends HasRolesTest { /** @var bool */ - protected $hasTeams=true; + protected $hasTeams = true; /** @test */ public function it_can_assign_same_and_different_roles_on_same_user_different_teams() diff --git a/tests/TestCase.php b/tests/TestCase.php index 1ea141566..f282b915d 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -37,7 +37,7 @@ abstract class TestCase extends Orchestra protected $useCustomModels = false; /** @var bool */ - protected $hasTeams=false; + protected $hasTeams = false; public function setUp(): void { From 579cc110f4afdf8a29d412f7aa153fc5559484d5 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 31 Aug 2021 14:11:38 -0400 Subject: [PATCH 0447/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16263e84e..b0a014730 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file -## PENDING 5.0.0 - 2021-08-31 +## 5.0.0 - 2021-08-31 - Change default-guard-lookup to prefer current user's guard (see BC note in #1817 ) +- Teams/Groups feature (see docs, or PR #1804) - Customized pivots instead of `role_id`,`permission_id` #1823 From e50b26851d202c657638bf07310db39639c25ff1 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 31 Aug 2021 14:17:50 -0400 Subject: [PATCH 0448/1013] Update version link --- docs/introduction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/introduction.md b/docs/introduction.md index 17baa86d2..466402858 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -17,7 +17,7 @@ $user->assignRole('writer'); $role->givePermissionTo('edit articles'); ``` -If you're using multiple guards we've got you covered as well. Every guard will have its own set of permissions and roles that can be assigned to the guard's users. Read about it in the [using multiple guards](https://docs.spatie.be/laravel-permission/v4/basic-usage/multiple-guards/) section. +If you're using multiple guards we've got you covered as well. Every guard will have its own set of permissions and roles that can be assigned to the guard's users. Read about it in the [using multiple guards](https://docs.spatie.be/laravel-permission/v5/basic-usage/multiple-guards/) section. Because all permissions will be registered on [Laravel's gate](https://laravel.com/docs/authorization), you can check if a user has a permission with Laravel's default `can` function: From e5f8f818f1783d3f1b8cfd8912f947bc056cf421 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 31 Aug 2021 14:22:06 -0400 Subject: [PATCH 0449/1013] Update docs link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c06c55a16..df0e89b52 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ ## Documentation, Installation, and Usage Instructions -See the [DOCUMENTATION](https://docs.spatie.be/laravel-permission/v4/introduction/) for detailed installation and usage instructions. +See the [DOCUMENTATION](https://docs.spatie.be/laravel-permission/) for detailed installation and usage instructions. ## What It Does This package allows you to manage user permissions and roles in a database. From 8f966252547a8d0d8187897b586f67142106e286 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 31 Aug 2021 15:05:40 -0400 Subject: [PATCH 0450/1013] Update changelog.md --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 27d734a9d..3280d4f29 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -3,4 +3,4 @@ title: Changelog weight: 10 --- -All notable changes to laravel-permission are documented [on GitHub](https://github.com/spatie/laravel-permission/blob/master/CHANGELOG.md) +All notable changes to laravel-permission are documented [on GitHub](https://github.com/spatie/laravel-permission/blob/main/CHANGELOG.md) From dfcbbe0f9c5c8d8d06fccaa9dab894236ce4c491 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 31 Aug 2021 15:15:24 -0400 Subject: [PATCH 0451/1013] Update branch links --- CHANGELOG.md | 4 ++-- composer.json | 1 + docs/advanced-usage/exceptions.md | 2 +- docs/basic-usage/teams-permissions.md | 2 +- docs/installation-laravel.md | 6 +++--- docs/installation-lumen.md | 6 +++--- docs/prerequisites.md | 2 +- 7 files changed, 12 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0a014730..64c43460c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -193,8 +193,8 @@ https://github.com/laravel/framework/commit/fd6eb89b62ec09df1ffbee164831a827e83f The following changes are not "breaking", but worth making the updates to your app for consistency. 1. Config file: The `config/permission.php` file changed to move cache-related settings into a sub-array. **You should review the changes and merge the updates into your own config file.** Specifically the `expiration_time` value has moved into a sub-array entry, and the old top-level entry is no longer used. -See the master config file here: -https://github.com/spatie/laravel-permission/blob/master/config/permission.php +See the original config file here: +https://github.com/spatie/laravel-permission/blob/main/config/permission.php 2. Cache Resets: If your `app` or `tests` are clearing the cache by specifying the cache key, **it is better to use the built-in forgetCachedPermissions() method** so that it properly handles tagged cache entries. Here is the recommended change: ```diff diff --git a/composer.json b/composer.json index 86e3f8017..2810431cc 100644 --- a/composer.json +++ b/composer.json @@ -53,6 +53,7 @@ ] }, "branch-alias": { + "dev-main": "5.x-dev", "dev-master": "5.x-dev" } }, diff --git a/docs/advanced-usage/exceptions.md b/docs/advanced-usage/exceptions.md index 9e2425c51..b71939a3d 100644 --- a/docs/advanced-usage/exceptions.md +++ b/docs/advanced-usage/exceptions.md @@ -7,7 +7,7 @@ If you need to override exceptions thrown by this package, you can simply use no An example is shown below for your convenience, but nothing here is specific to this package other than the name of the exception. -You can find all the exceptions added by this package in the code here: https://github.com/spatie/laravel-permission/tree/master/src/Exceptions +You can find all the exceptions added by this package in the code here: https://github.com/spatie/laravel-permission/tree/main/src/Exceptions **app/Exceptions/Handler.php** diff --git a/docs/basic-usage/teams-permissions.md b/docs/basic-usage/teams-permissions.md index 58e0055b7..b0688ba66 100644 --- a/docs/basic-usage/teams-permissions.md +++ b/docs/basic-usage/teams-permissions.md @@ -3,7 +3,7 @@ title: Teams permissions weight: 3 --- -NOTE: Those changes must be made before performing the migration. If you have already run the migration and want to upgrade your solution, you can run the artisan console command `php artisan permission:setup-teams`, to create a new migration file named [xxxx_xx_xx_xx_add_teams_fields.php](https://github.com/spatie/laravel-permission/blob/master/database/migrations/add_teams_fields.php.stub) and then run `php artisan migrate` to upgrade your database tables. +NOTE: Those changes must be made before performing the migration. If you have already run the migration and want to upgrade your solution, you can run the artisan console command `php artisan permission:setup-teams`, to create a new migration file named [xxxx_xx_xx_xx_add_teams_fields.php](https://github.com/spatie/laravel-permission/blob/main/database/migrations/add_teams_fields.php.stub) and then run `php artisan migrate` to upgrade your database tables. When enabled, teams permissions offers you a flexible control for a variety of scenarios. The idea behind teams permissions is inspired by the default permission implementation of [Laratrust](https://laratrust.santigarcor.me/). diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index c87a5b29f..c37db5d91 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -26,14 +26,14 @@ This package can be used with Laravel 6.0 or higher. ]; ``` -5. You should publish [the migration](https://github.com/spatie/laravel-permission/blob/master/database/migrations/create_permission_tables.php.stub) and the [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) with: +5. You should publish [the migration](https://github.com/spatie/laravel-permission/blob/main/database/migrations/create_permission_tables.php.stub) and the [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/main/config/permission.php) with: ``` php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" ``` 6. NOTE: If you are using UUIDs, see the Advanced section of the docs on UUID steps, before you continue. It explains some changes you may want to make to the migrations and config file before continuing. It also mentions important considerations after extending this package's models for UUID capability. - If you are going to use teams feature, you have to update your [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) and set `'teams' => true,`, if you want to use a custom foreign key for teams you must change `team_foreign_key`. + If you are going to use teams feature, you have to update your [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/main/config/permission.php) and set `'teams' => true,`, if you want to use a custom foreign key for teams you must change `team_foreign_key`. 7. Clear your config cache. This package requires access to the `permission` config. Generally it's bad practice to do config-caching in a development environment. If you've been caching configurations locally, clear your config cache with either of these commands: @@ -52,4 +52,4 @@ This package can be used with Laravel 6.0 or higher. You can view the default config file contents at: -[https://github.com/spatie/laravel-permission/blob/master/config/permission.php](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) +[https://github.com/spatie/laravel-permission/blob/main/config/permission.php](https://github.com/spatie/laravel-permission/blob/main/config/permission.php) diff --git a/docs/installation-lumen.md b/docs/installation-lumen.md index e1a825170..ff3d98455 100644 --- a/docs/installation-lumen.md +++ b/docs/installation-lumen.md @@ -5,7 +5,7 @@ weight: 5 NOTE: Lumen is **not** officially supported by this package. However, the following are some steps which may help get you started. -Lumen installation instructions can be found in the [Lumen documentation](https://lumen.laravel.com/docs/master). +Lumen installation instructions can be found in the [Lumen documentation](https://lumen.laravel.com/docs/main). Install the permissions package via Composer: @@ -52,7 +52,7 @@ $app->register(App\Providers\AuthServiceProvider::class); Ensure the application's database name/credentials are set in your `.env` (or `config/database.php` if you have one), and that the database exists. -NOTE: If you are going to use teams feature, you have to update your [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) and set `'teams' => true,`, if you want to use a custom foreign key for teams you must change `team_foreign_key`. +NOTE: If you are going to use teams feature, you have to update your [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/main/config/permission.php) and set `'teams' => true,`, if you want to use a custom foreign key for teams you must change `team_foreign_key`. Run the migrations to create the tables for this package: @@ -68,7 +68,7 @@ NOTE: Remember that Laravel's authorization layer requires that your `User` mode ### User Table NOTE: If you are working with a fresh install of Lumen, then you probably also need a migration file for your Users table. You can create your own, or you can copy a basic one from Laravel: -[https://github.com/laravel/laravel/blob/master/database/migrations/2014_10_12_000000_create_users_table.php](https://github.com/laravel/laravel/blob/master/database/migrations/2014_10_12_000000_create_users_table.php) +[https://github.com/laravel/laravel/blob/main/database/migrations/2014_10_12_000000_create_users_table.php](https://github.com/laravel/laravel/blob/main/database/migrations/2014_10_12_000000_create_users_table.php) (You will need to run `php artisan migrate` after adding this file.) diff --git a/docs/prerequisites.md b/docs/prerequisites.md index ddcafbdb6..4508c6cdd 100644 --- a/docs/prerequisites.md +++ b/docs/prerequisites.md @@ -45,5 +45,5 @@ This package publishes a `config/permission.php` file. If you already have a fil MySQL 8.0 limits index keys to 1000 characters. This package publishes a migration which combines multiple columns in single index. With `utf8mb4` the 4-bytes-per-character requirement of `mb4` means the max length of the columns in the hybrid index can only be `125` characters. -Thus in your AppServiceProvider you will need to set `Schema::defaultStringLength(125)`. [See the Laravel Docs for instructions](https://laravel.com/docs/master/migrations#index-lengths-mysql-mariadb). +Thus in your AppServiceProvider you will need to set `Schema::defaultStringLength(125)`. [See the Laravel Docs for instructions](https://laravel.com/docs/migrations#index-lengths-mysql-mariadb). From e62b309d266373d06b96eb5e2300522b02e1524d Mon Sep 17 00:00:00 2001 From: Erik Niebla Date: Tue, 31 Aug 2021 14:37:59 -0500 Subject: [PATCH 0452/1013] Avoid unnecessary cache forget --- src/Traits/HasPermissions.php | 8 ++++-- src/Traits/HasRoles.php | 8 ++++-- tests/CacheTest.php | 46 ++++++++++++++++++++++++++++++++++- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 128351186..5ac94cbda 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -371,7 +371,9 @@ function ($object) use ($permissions, $model) { ); } - $this->forgetCachedPermissions(); + if (is_a($this, get_class(app(PermissionRegistrar::class)->getRoleClass()))) { + $this->forgetCachedPermissions(); + } return $this; } @@ -401,7 +403,9 @@ public function revokePermissionTo($permission) { $this->permissions()->detach($this->getStoredPermission($permission)); - $this->forgetCachedPermissions(); + if (is_a($this, get_class(app(PermissionRegistrar::class)->getRoleClass()))) { + $this->forgetCachedPermissions(); + } $this->load('permissions'); diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index f9f70b498..006b742ed 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -150,7 +150,9 @@ function ($object) use ($roles, $model) { ); } - $this->forgetCachedPermissions(); + if (is_a($this, get_class($this->getPermissionClass()))) { + $this->forgetCachedPermissions(); + } return $this; } @@ -166,7 +168,9 @@ public function removeRole($role) $this->load('roles'); - $this->forgetCachedPermissions(); + if (is_a($this, get_class($this->getPermissionClass()))) { + $this->forgetCachedPermissions(); + } return $this; } diff --git a/tests/CacheTest.php b/tests/CacheTest.php index 1a1caa0ff..abb718bc7 100644 --- a/tests/CacheTest.php +++ b/tests/CacheTest.php @@ -104,7 +104,23 @@ public function it_flushes_the_cache_when_updating_a_role() } /** @test */ - public function it_flushes_the_cache_when_removing_a_role_from_a_user() + public function removing_a_permission_from_a_user_should_not_flush_the_cache() + { + $this->testUser->givePermissionTo('edit-articles'); + + $this->registrar->getPermissions(); + + $this->testUser->revokePermissionTo('edit-articles'); + + $this->resetQueryCount(); + + $this->registrar->getPermissions(); + + $this->assertQueryCount(0); + } + + /** @test */ + public function removing_a_role_from_a_user_should_not_flush_the_cache() { $this->testUser->assignRole('testRole'); @@ -116,6 +132,34 @@ public function it_flushes_the_cache_when_removing_a_role_from_a_user() $this->registrar->getPermissions(); + $this->assertQueryCount(0); + } + + /** @test */ + public function it_flushes_the_cache_when_removing_a_role_from_a_permission() + { + $this->testUserPermission->assignRole('testRole'); + + $this->registrar->getPermissions(); + + $this->testUserPermission->removeRole('testRole'); + + $this->resetQueryCount(); + + $this->registrar->getPermissions(); + + $this->assertQueryCount($this->cache_init_count + $this->cache_load_count + $this->cache_run_count); + } + + /** @test */ + public function it_flushes_the_cache_when_assign_a_permission_to_a_role() + { + $this->testUserRole->givePermissionTo('edit-articles'); + + $this->resetQueryCount(); + + $this->registrar->getPermissions(); + $this->assertQueryCount($this->cache_init_count + $this->cache_load_count + $this->cache_run_count); } From c2dee58d820711249d54773ed798fdad9f799dea Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 31 Aug 2021 16:21:28 -0400 Subject: [PATCH 0453/1013] Update CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64c43460c..cf2a5cdf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.1.0 - 2021-08-31 +- No longer flush cache on User role/perm assignment changes #1832 + NOTE: You should test your app to be sure that you don't accidentally have deep dependencies on cache resets happening automatically in these cases. + ALSO NOTE: If you have added custom code which depended on these flush operations, you may need to add your own cache-reset calls. + ## 5.0.0 - 2021-08-31 - Change default-guard-lookup to prefer current user's guard (see BC note in #1817 ) - Teams/Groups feature (see docs, or PR #1804) From 0ad6bf81d06a0f416740301c1b3dd7eb85fb8c92 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 31 Aug 2021 16:28:10 -0400 Subject: [PATCH 0454/1013] Update cache.md --- docs/advanced-usage/cache.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/advanced-usage/cache.md b/docs/advanced-usage/cache.md index 1272b7ea6..3b1f8160e 100644 --- a/docs/advanced-usage/cache.md +++ b/docs/advanced-usage/cache.md @@ -10,9 +10,6 @@ Role and Permission data are cached to speed up performance. When you **use the built-in functions** for manipulating roles and permissions, the cache is automatically reset for you, and relations are automatically reloaded for the current model record: ```php -$user->assignRole('writer'); -$user->removeRole('writer'); -$user->syncRoles(params); $role->givePermissionTo('edit articles'); $role->revokePermissionTo('edit articles'); $role->syncPermissions(params); @@ -25,6 +22,14 @@ HOWEVER, if you manipulate permission/role data directly in the database instead Additionally, because the Role and Permission models are Eloquent models which implement the `RefreshesPermissionCache` trait, creating and deleting Roles and Permissions will automatically clear the cache. If you have created your own models which do not extend the default models then you will need to implement the trait yourself. +**NOTE: User-specific role/permission assignments are kept in-memory since v4.4.0, so the cache-reset is no longer called since v5.1.0 when updating User-related assignments.** +Examples: +```php +// These do not call a cache-reset, because the User-related assignments are in-memory. +$user->assignRole('writer'); +$user->removeRole('writer'); +$user->syncRoles(params); +``` ### Manual cache reset To manually reset the cache for this package, you can run the following in your app code: From 9b7523b5e76e8667813f35a884c52c15cf6c5caa Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Wed, 1 Sep 2021 09:34:47 +0200 Subject: [PATCH 0455/1013] Update _index.md --- docs/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_index.md b/docs/_index.md index 2bf4d0fd1..591491f89 100644 --- a/docs/_index.md +++ b/docs/_index.md @@ -1,5 +1,5 @@ --- -title: v4 +title: v5 slogan: Associate users with roles and permissions githubUrl: https://github.com/spatie/laravel-permission branch: main From 9006b9d9fcac07a74ce3ed7aca8d81887cf02604 Mon Sep 17 00:00:00 2001 From: Danilo Pinotti Date: Wed, 1 Sep 2021 11:16:51 -0300 Subject: [PATCH 0456/1013] Avoid Roles over-hydratation --- src/PermissionRegistrar.php | 73 +++++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 30c63d1e6..f46e7fbb7 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -32,6 +32,9 @@ class PermissionRegistrar /** @var string */ public static $cacheKey; + /** @var array */ + private $cachedRoles = []; + /** * PermissionRegistrar constructor. * @@ -117,30 +120,36 @@ public function clearClassPermissions() */ private function loadPermissions() { - if ($this->permissions === null) { - $this->permissions = $this->cache->remember(self::$cacheKey, self::$cacheExpirationTime, function () { - // make the cache smaller using an array with only required fields - return $this->getPermissionClass()->select('id', 'id as i', 'name as n', 'guard_name as g') - ->with('roles:id,id as i,name as n,guard_name as g')->get() - ->map(function ($permission) { - return $permission->only('i', 'n', 'g') + - ['r' => $permission->roles->map->only('i', 'n', 'g')->all()]; - })->all(); + if ($this->permissions !== null) { + return; + } + + $this->permissions = $this->cache->remember(self::$cacheKey, self::$cacheExpirationTime, function () { + // make the cache smaller using an array with only required fields + return $this->getPermissionClass()->select('id', 'id as i', 'name as n', 'guard_name as g') + ->with('roles:id,id as i,name as n,guard_name as g')->get() + ->map(function ($permission) { + return $permission->only('i', 'n', 'g') + + ['r' => $permission->roles->map->only('i', 'n', 'g')->all()]; + })->all(); + }); + + if (is_array($this->permissions)) { + $this->permissions = $this->getPermissionClass()::hydrate( + collect($this->permissions)->map(function ($item) { + return ['id' => $item['i'] ?? $item['id'], 'name' => $item['n'] ?? $item['name'], 'guard_name' => $item['g'] ?? $item['guard_name']]; + })->all() + ) + ->each(function ($permission, $i) { + $roles = Collection::make($this->permissions[$i]['r'] ?? $this->permissions[$i]['roles'] ?? []) + ->map(function ($item) { + return $this->getHydratedRole($item); + }); + + $permission->setRelation('roles', $roles); }); - if (is_array($this->permissions)) { - $this->permissions = $this->getPermissionClass()::hydrate( - collect($this->permissions)->map(function ($item) { - return ['id' => $item['i'] ?? $item['id'], 'name' => $item['n'] ?? $item['name'], 'guard_name' => $item['g'] ?? $item['guard_name']]; - })->all() - ) - ->each(function ($permission, $i) { - $permission->setRelation('roles', $this->getRoleClass()::hydrate( - collect($this->permissions[$i]['r'] ?? $this->permissions[$i]['roles'] ?? [])->map(function ($item) { - return ['id' => $item['i'] ?? $item['id'], 'name' => $item['n'] ?? $item['name'], 'guard_name' => $item['g'] ?? $item['guard_name']]; - })->all() - )); - }); - } + + $this->cachedRoles = []; } } @@ -211,4 +220,22 @@ public function getCacheStore(): \Illuminate\Contracts\Cache\Store { return $this->cache->getStore(); } + + private function getHydratedRole(array $item) + { + $roleId = $item['i'] ?? $item['id']; + + if (isset($this->cachedRoles[$roleId])) { + return $this->cachedRoles[$roleId]; + } + + $roleClass = $this->getRoleClass(); + $roleInstance = new $roleClass; + return $this->cachedRoles[$roleId] = $roleInstance->newFromBuilder([ + 'id' => $roleId, + 'name' => $item['n'] ?? $item['name'], + 'guard_name' => $item['g'] ?? $item['guard_name'], + ]); + + } } From 43e848efcd84b2f199c7a51a94d4db69d51b9632 Mon Sep 17 00:00:00 2001 From: Danilo Pinotti Date: Wed, 1 Sep 2021 12:24:36 -0300 Subject: [PATCH 0457/1013] Add test --- tests/CacheTest.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/CacheTest.php b/tests/CacheTest.php index 1a1caa0ff..19406486d 100644 --- a/tests/CacheTest.php +++ b/tests/CacheTest.php @@ -203,6 +203,17 @@ public function get_all_permissions_should_use_the_cache() $this->assertQueryCount(2); } + /** @test */ + public function get_all_permissions_should_not_over_hydrate_roles() + { + $this->testUserRole->givePermissionTo(['edit-articles', 'edit-news']); + $permissions = $this->registrar->getPermissions(); + $roles = $permissions->flatMap->roles; + + // Should have same object reference + $this->assertSame($roles[0], $roles[1]); + } + /** @test */ public function it_can_reset_the_cache_with_artisan_command() { From 75c52d2948f87fa887065b1d42d71f1d9b90a6a5 Mon Sep 17 00:00:00 2001 From: drbyte Date: Wed, 1 Sep 2021 17:39:35 +0000 Subject: [PATCH 0458/1013] Fix styling --- src/PermissionRegistrar.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index f46e7fbb7..c1d630970 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -231,11 +231,11 @@ private function getHydratedRole(array $item) $roleClass = $this->getRoleClass(); $roleInstance = new $roleClass; + return $this->cachedRoles[$roleId] = $roleInstance->newFromBuilder([ 'id' => $roleId, 'name' => $item['n'] ?? $item['name'], 'guard_name' => $item['g'] ?? $item['guard_name'], ]); - } } From 3c9d7ae7683081ee90a4e2297f4e58aff3492a1e Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 1 Sep 2021 13:40:58 -0400 Subject: [PATCH 0459/1013] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42a896f54..1a9242af6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `laravel-permission` will be documented in this file +## 4.4.1 - 2021-09-01 +- Avoid Roles over-hydration #1834 + ## 4.4.0 - 2021-08-28 - Avoid BC break (removed interface change) on cache change added in 4.3.0 #1826 - Made cache even smaller #1826 From 27a7ad757157666eeb9401d17ae6e082b4b021b0 Mon Sep 17 00:00:00 2001 From: Niels Vanpachtenbeke <10651054+Nielsvanpach@users.noreply.github.com> Date: Tue, 7 Sep 2021 09:41:58 +0200 Subject: [PATCH 0460/1013] Update .php_cs.dist.php --- .php_cs.dist.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.php_cs.dist.php b/.php_cs.dist.php index bd7148822..e89f88628 100644 --- a/.php_cs.dist.php +++ b/.php_cs.dist.php @@ -12,7 +12,7 @@ return (new PhpCsFixer\Config()) ->setRules([ - '@PSR2' => true, + '@PSR12' => true, 'array_syntax' => ['syntax' => 'short'], 'ordered_imports' => ['sort_algorithm' => 'alpha'], 'no_unused_imports' => true, From 2512d5a22aaae0bab8d62a02e502a603776da9da Mon Sep 17 00:00:00 2001 From: Nielsvanpach Date: Tue, 7 Sep 2021 07:42:33 +0000 Subject: [PATCH 0461/1013] Fix styling --- src/PermissionRegistrar.php | 2 +- src/Traits/HasPermissions.php | 4 ++-- src/WildcardPermission.php | 6 +++--- tests/HasPermissionsWithCustomModelsTest.php | 2 +- tests/TestCase.php | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index c82309f8c..c3308b201 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -266,7 +266,7 @@ private function getHydratedRole(array $item) } $roleClass = $this->getRoleClass(); - $roleInstance = new $roleClass; + $roleInstance = new $roleClass(); return $this->cachedRoles[$roleId] = $roleInstance->newFromBuilder([ 'id' => $roleId, diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 5ac94cbda..00b7a82ae 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -139,7 +139,7 @@ public function hasPermissionTo($permission, $guardName = null): bool } if (! $permission instanceof Permission) { - throw new PermissionDoesNotExist; + throw new PermissionDoesNotExist(); } return $this->hasDirectPermission($permission) || $this->hasPermissionViaRole($permission); @@ -285,7 +285,7 @@ public function hasDirectPermission($permission): bool } if (! $permission instanceof Permission) { - throw new PermissionDoesNotExist; + throw new PermissionDoesNotExist(); } return $this->permissions->contains('id', $permission->id); diff --git a/src/WildcardPermission.php b/src/WildcardPermission.php index 22d142ba0..a1b20a707 100644 --- a/src/WildcardPermission.php +++ b/src/WildcardPermission.php @@ -8,13 +8,13 @@ class WildcardPermission { /** @var string */ - const WILDCARD_TOKEN = '*'; + public const WILDCARD_TOKEN = '*'; /** @var string */ - const PART_DELIMITER = '.'; + public const PART_DELIMITER = '.'; /** @var string */ - const SUBPART_DELIMITER = ','; + public const SUBPART_DELIMITER = ','; /** @var string */ protected $permission; diff --git a/tests/HasPermissionsWithCustomModelsTest.php b/tests/HasPermissionsWithCustomModelsTest.php index 8d0ab6442..92970121a 100644 --- a/tests/HasPermissionsWithCustomModelsTest.php +++ b/tests/HasPermissionsWithCustomModelsTest.php @@ -6,7 +6,7 @@ class HasPermissionsWithCustomModelsTest extends HasPermissionsTest { /** @var bool */ protected $useCustomModels = true; - + /** @test */ public function it_can_use_custom_models() { diff --git a/tests/TestCase.php b/tests/TestCase.php index f282b915d..350d90188 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -32,7 +32,7 @@ abstract class TestCase extends Orchestra /** @var \Spatie\Permission\Models\Permission */ protected $testAdminPermission; - + /** @var bool */ protected $useCustomModels = false; From f827b6dc097e9d019946d6a81c0d3c782afb4d37 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Thu, 28 Oct 2021 02:38:55 -0500 Subject: [PATCH 0462/1013] Fix detaching on all teams intstead of only current #1888 (#1890) Co-authored-by: Erik Niebla --- src/Traits/HasPermissions.php | 24 +++++++++++++++------ src/Traits/HasRoles.php | 24 +++++++++++++++------ tests/TeamHasPermissionsTest.php | 33 ++++++++++++++++++++++++++++ tests/TeamHasRolesTest.php | 37 ++++++++++++++++++++++++++++++-- 4 files changed, 102 insertions(+), 16 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 00b7a82ae..d6377a4ce 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -317,6 +317,20 @@ public function getAllPermissions(): Collection return $permissions->sort()->values(); } + /** + * Add teams pivot if teams are enabled + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + protected function getPermissionsRelation(){ + $relation = $this->permissions(); + if (PermissionRegistrar::$teams && ! is_a($this, Role::class)) { + $relation->wherePivot(PermissionRegistrar::$teamsKey, app(PermissionRegistrar::class)->getPermissionsTeamId()); + } + + return $relation; + } + /** * Grant the given permission(s) to a role. * @@ -351,11 +365,7 @@ public function givePermissionTo(...$permissions) $model = $this->getModel(); if ($model->exists) { - if (PermissionRegistrar::$teams && ! is_a($this, Role::class)) { - $this->permissions()->wherePivot(PermissionRegistrar::$teamsKey, app(PermissionRegistrar::class)->getPermissionsTeamId())->sync($permissions, false); - } else { - $this->permissions()->sync($permissions, false); - } + $this->getPermissionsRelation()->sync($permissions, false); $model->load('permissions'); } else { $class = \get_class($model); @@ -387,7 +397,7 @@ function ($object) use ($permissions, $model) { */ public function syncPermissions(...$permissions) { - $this->permissions()->detach(); + $this->getPermissionsRelation()->detach(); return $this->givePermissionTo($permissions); } @@ -401,7 +411,7 @@ public function syncPermissions(...$permissions) */ public function revokePermissionTo($permission) { - $this->permissions()->detach($this->getStoredPermission($permission)); + $this->getPermissionsRelation()->detach($this->getStoredPermission($permission)); if (is_a($this, get_class(app(PermissionRegistrar::class)->getRoleClass()))) { $this->forgetCachedPermissions(); diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 006b742ed..214c34c40 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -96,6 +96,20 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder }); } + /** + * Add teams pivot if teams are enabled + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + protected function getRolesRelation(){ + $relation = $this->roles(); + if (PermissionRegistrar::$teams && ! is_a($this, Permission::class)) { + $relation->wherePivot(PermissionRegistrar::$teamsKey, app(PermissionRegistrar::class)->getPermissionsTeamId()); + } + + return $relation; + } + /** * Assign the given role to the model. * @@ -130,11 +144,7 @@ public function assignRole(...$roles) $model = $this->getModel(); if ($model->exists) { - if (PermissionRegistrar::$teams && ! is_a($this, Permission::class)) { - $this->roles()->wherePivot(PermissionRegistrar::$teamsKey, app(PermissionRegistrar::class)->getPermissionsTeamId())->sync($roles, false); - } else { - $this->roles()->sync($roles, false); - } + $this->getRolesRelation()->sync($roles, false); $model->load('roles'); } else { $class = \get_class($model); @@ -164,7 +174,7 @@ function ($object) use ($roles, $model) { */ public function removeRole($role) { - $this->roles()->detach($this->getStoredRole($role)); + $this->getRolesRelation()->detach($this->getStoredRole($role)); $this->load('roles'); @@ -184,7 +194,7 @@ public function removeRole($role) */ public function syncRoles(...$roles) { - $this->roles()->detach(); + $this->getRolesRelation()->detach(); return $this->assignRole($roles); } diff --git a/tests/TeamHasPermissionsTest.php b/tests/TeamHasPermissionsTest.php index 62d3545e0..4fc345225 100644 --- a/tests/TeamHasPermissionsTest.php +++ b/tests/TeamHasPermissionsTest.php @@ -70,4 +70,37 @@ public function it_can_list_all_the_coupled_permissions_both_directly_and_via_ro $this->testUser->getAllPermissions()->pluck('name')->sort()->values() ); } + + /** @test */ + public function it_can_sync_or_remove_permission_without_detach_on_different_teams() + { + $this->setPermissionsTeamId(1); + $this->testUser->load('permissions'); + $this->testUser->syncPermissions('edit-articles', 'edit-news'); + + $this->setPermissionsTeamId(2); + $this->testUser->load('permissions'); + $this->testUser->syncPermissions('edit-articles', 'edit-blog'); + + $this->setPermissionsTeamId(1); + $this->testUser->load('permissions'); + + $this->assertEquals( + collect(['edit-articles', 'edit-news']), + $this->testUser->getPermissionNames()->sort()->values() + ); + + $this->testUser->revokePermissionTo('edit-articles'); + $this->assertEquals( + collect(['edit-news']), + $this->testUser->getPermissionNames()->sort()->values() + ); + + $this->setPermissionsTeamId(2); + $this->testUser->load('permissions'); + $this->assertEquals( + collect(['edit-articles', 'edit-blog']), + $this->testUser->getPermissionNames()->sort()->values() + ); + } } diff --git a/tests/TeamHasRolesTest.php b/tests/TeamHasRolesTest.php index 19c745e2a..387322f44 100644 --- a/tests/TeamHasRolesTest.php +++ b/tests/TeamHasRolesTest.php @@ -24,12 +24,10 @@ public function it_can_assign_same_and_different_roles_on_same_user_different_te $this->setPermissionsTeamId(1); $this->testUser->load('roles'); - $this->testUser->assignRole('testRole', 'testRole2'); $this->setPermissionsTeamId(2); $this->testUser->load('roles'); - $this->testUser->assignRole('testRole', 'testRole3'); $this->setPermissionsTeamId(1); @@ -59,4 +57,39 @@ public function it_can_assign_same_and_different_roles_on_same_user_different_te $this->assertTrue($this->testUser->hasExactRoles(['testRole', 'testRole3', 'testRole4'])); $this->assertTrue($this->testUser->hasRole($testRole4NoTeam)); // global role team=null } + + /** @test */ + public function it_can_sync_or_remove_roles_without_detach_on_different_teams(){ + app(Role::class)->create(['name' => 'testRole3', 'team_test_id' => 2]); + + $this->setPermissionsTeamId(1); + $this->testUser->load('roles'); + $this->testUser->syncRoles('testRole', 'testRole2'); + + $this->setPermissionsTeamId(2); + $this->testUser->load('roles'); + $this->testUser->syncRoles('testRole', 'testRole3'); + + $this->setPermissionsTeamId(1); + $this->testUser->load('roles'); + + $this->assertEquals( + collect(['testRole', 'testRole2']), + $this->testUser->getRoleNames()->sort()->values() + ); + + $this->testUser->removeRole('testRole'); + $this->assertEquals( + collect(['testRole2']), + $this->testUser->getRoleNames()->sort()->values() + ); + + $this->setPermissionsTeamId(2); + $this->testUser->load('roles'); + + $this->assertEquals( + collect(['testRole', 'testRole3']), + $this->testUser->getRoleNames()->sort()->values() + ); + } } From 05b8206832314b2efdcaa12c1efcec563def54b0 Mon Sep 17 00:00:00 2001 From: freekmurze Date: Thu, 28 Oct 2021 07:39:33 +0000 Subject: [PATCH 0463/1013] Fix styling --- src/Traits/HasPermissions.php | 3 ++- src/Traits/HasRoles.php | 3 ++- tests/TeamHasRolesTest.php | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index d6377a4ce..3e0d4072b 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -322,7 +322,8 @@ public function getAllPermissions(): Collection * * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ - protected function getPermissionsRelation(){ + protected function getPermissionsRelation() + { $relation = $this->permissions(); if (PermissionRegistrar::$teams && ! is_a($this, Role::class)) { $relation->wherePivot(PermissionRegistrar::$teamsKey, app(PermissionRegistrar::class)->getPermissionsTeamId()); diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 214c34c40..24283ff66 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -101,7 +101,8 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder * * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ - protected function getRolesRelation(){ + protected function getRolesRelation() + { $relation = $this->roles(); if (PermissionRegistrar::$teams && ! is_a($this, Permission::class)) { $relation->wherePivot(PermissionRegistrar::$teamsKey, app(PermissionRegistrar::class)->getPermissionsTeamId()); diff --git a/tests/TeamHasRolesTest.php b/tests/TeamHasRolesTest.php index 387322f44..b07614d8e 100644 --- a/tests/TeamHasRolesTest.php +++ b/tests/TeamHasRolesTest.php @@ -59,7 +59,8 @@ public function it_can_assign_same_and_different_roles_on_same_user_different_te } /** @test */ - public function it_can_sync_or_remove_roles_without_detach_on_different_teams(){ + public function it_can_sync_or_remove_roles_without_detach_on_different_teams() + { app(Role::class)->create(['name' => 'testRole3', 'team_test_id' => 2]); $this->setPermissionsTeamId(1); From 8b304f2890be313b0b31cd5bb54af0cd2a89b469 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Thu, 28 Oct 2021 02:39:58 -0500 Subject: [PATCH 0464/1013] Add uuid compatibility on team_id (#1857) Co-authored-by: Erik Niebla --- src/PermissionRegistrar.php | 13 ++++++++++--- src/helpers.php | 14 ++++++++++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index c3308b201..cdf157e0b 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -101,14 +101,21 @@ protected function getCacheStoreFromConfig(): \Illuminate\Contracts\Cache\Reposi /** * Set the team id for teams/groups support, this id is used when querying permissions/roles * - * @param int $id + * @param int|string|\Illuminate\Database\Eloquent\Model $id */ - public function setPermissionsTeamId(?int $id) + public function setPermissionsTeamId($id) { + if ($id instanceof \Illuminate\Database\Eloquent\Model) { + $id = $id->getKey(); + } $this->teamId = $id; } - public function getPermissionsTeamId(): ?int + /** + * + * @return int|string + */ + public function getPermissionsTeamId() { return $this->teamId; } diff --git a/src/helpers.php b/src/helpers.php index 2b80ea417..f4df0a7cc 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -21,11 +21,21 @@ function getModelForGuard(string $guard) if (! function_exists('setPermissionsTeamId')) { /** - * @param int $id + * @param int|string|\Illuminate\Database\Eloquent\Model $id * */ - function setPermissionsTeamId(int $id) + function setPermissionsTeamId($id) { app(\Spatie\Permission\PermissionRegistrar::class)->setPermissionsTeamId($id); } } + +if (! function_exists('getPermissionsTeamId')) { + /** + * @return int|string + */ + function getPermissionsTeamId() + { + app(\Spatie\Permission\PermissionRegistrar::class)->getPermissionsTeamId(); + } +} From f1bdffdb16a7579a5978e13815be05e9df31d3e9 Mon Sep 17 00:00:00 2001 From: Tim Schwartz Date: Thu, 28 Oct 2021 02:43:49 -0500 Subject: [PATCH 0465/1013] Adds setRoleClass method to PermissionRegistrar (#1867) --- src/PermissionRegistrar.php | 11 ++++++++++- tests/RoleTest.php | 27 +++++++++++++++++++++++++++ tests/RuntimeRole.php | 11 +++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/RuntimeRole.php diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index cdf157e0b..038da6c38 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -240,7 +240,8 @@ public function getPermissionClass(): Permission public function setPermissionClass($permissionClass) { $this->permissionClass = $permissionClass; - + config()->set('permission.models.permission', $permissionClass); + app()->bind(Permission::class, $permissionClass); return $this; } @@ -254,6 +255,14 @@ public function getRoleClass(): Role return app($this->roleClass); } + public function setRoleClass($roleClass) + { + $this->roleClass = $roleClass; + config()->set('permission.models.role', $roleClass); + app()->bind(Role::class, $roleClass); + return $this; + } + /** * Get the instance of the Cache Store. * diff --git a/tests/RoleTest.php b/tests/RoleTest.php index 89d51804a..3ea137902 100644 --- a/tests/RoleTest.php +++ b/tests/RoleTest.php @@ -8,6 +8,8 @@ use Spatie\Permission\Exceptions\RoleAlreadyExists; use Spatie\Permission\Exceptions\RoleDoesNotExist; use Spatie\Permission\Models\Permission; +use Spatie\Permission\Test\RuntimeRole; +use Spatie\Permission\PermissionRegistrar; class RoleTest extends TestCase { @@ -239,4 +241,29 @@ public function it_belongs_to_the_default_guard_by_default() $this->testUserRole->guard_name ); } + + /** @test */ + public function it_can_change_role_class_on_runtime() + { + $role = app(Role::class)->create(['name' => 'test-role-old']); + $this->assertNotInstanceOf(RuntimeRole::class, $role); + + $role->givePermissionTo('edit-articles'); + + app('config')->set('permission.models.role', RuntimeRole::class); + app()->bind(Role::class, RuntimeRole::class); + app(PermissionRegistrar::class)->setRoleClass(RuntimeRole::class); + + $permission = app(Permission::class)->findByName('edit-articles'); + $this->assertInstanceOf(RuntimeRole::class, $permission->roles[0]); + $this->assertSame('test-role-old', $permission->roles[0]->name); + + $role = app(Role::class)->create(['name' => 'test-role']); + $this->assertInstanceOf(RuntimeRole::class, $role); + + $this->testUser->assignRole('test-role'); + $this->assertTrue($this->testUser->hasRole('test-role')); + $this->assertInstanceOf(RuntimeRole::class, $this->testUser->roles[0]); + $this->assertSame('test-role', $this->testUser->roles[0]->name); + } } diff --git a/tests/RuntimeRole.php b/tests/RuntimeRole.php new file mode 100644 index 000000000..82f24a09f --- /dev/null +++ b/tests/RuntimeRole.php @@ -0,0 +1,11 @@ + Date: Thu, 28 Oct 2021 11:14:14 +0330 Subject: [PATCH 0466/1013] Load permissions for preventLazyLoading (#1884) --- src/Commands/Show.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Commands/Show.php b/src/Commands/Show.php index f7cc2b343..fbd61967a 100644 --- a/src/Commands/Show.php +++ b/src/Commands/Show.php @@ -35,6 +35,7 @@ public function handle() $this->info("Guard: $guard"); $roles = $roleClass::whereGuardName($guard) + ->with('permissions') ->when(config('permission.teams'), function ($q) use ($team_key) { $q->orderBy($team_key); }) From 5285f6b936819d1eef3c3b2d36397ba62e6a76cd Mon Sep 17 00:00:00 2001 From: freekmurze Date: Thu, 28 Oct 2021 07:45:03 +0000 Subject: [PATCH 0467/1013] Fix styling --- src/PermissionRegistrar.php | 2 ++ tests/RoleTest.php | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 038da6c38..0326be646 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -242,6 +242,7 @@ public function setPermissionClass($permissionClass) $this->permissionClass = $permissionClass; config()->set('permission.models.permission', $permissionClass); app()->bind(Permission::class, $permissionClass); + return $this; } @@ -260,6 +261,7 @@ public function setRoleClass($roleClass) $this->roleClass = $roleClass; config()->set('permission.models.role', $roleClass); app()->bind(Role::class, $roleClass); + return $this; } diff --git a/tests/RoleTest.php b/tests/RoleTest.php index 3ea137902..61a958b8d 100644 --- a/tests/RoleTest.php +++ b/tests/RoleTest.php @@ -8,7 +8,6 @@ use Spatie\Permission\Exceptions\RoleAlreadyExists; use Spatie\Permission\Exceptions\RoleDoesNotExist; use Spatie\Permission\Models\Permission; -use Spatie\Permission\Test\RuntimeRole; use Spatie\Permission\PermissionRegistrar; class RoleTest extends TestCase From 2843185ab8cec3c08470024248e99ce55606f3ac Mon Sep 17 00:00:00 2001 From: erikn69 Date: Thu, 28 Oct 2021 02:46:03 -0500 Subject: [PATCH 0468/1013] Doc for `Super Admin` on teams (#1845) Co-authored-by: Erik Niebla --- docs/basic-usage/teams-permissions.md | 32 ++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/docs/basic-usage/teams-permissions.md b/docs/basic-usage/teams-permissions.md index b0688ba66..a9152ee71 100644 --- a/docs/basic-usage/teams-permissions.md +++ b/docs/basic-usage/teams-permissions.md @@ -53,7 +53,7 @@ NOTE: You must add your custom `Middleware` to `$middlewarePriority` on `app/Htt When creating a role you can pass the `team_id` as an optional parameter ```php -// with null team_id it creates a global role, global roles can be used from any team and they are unique +// with null team_id it creates a global role, global roles can be assigned to any team and they are unique Role::create(['name' => 'writer', 'team_id' => null]); // creates a role with team_id = 1, team roles can have the same name on different teams @@ -66,3 +66,33 @@ Role::create(['name' => 'reviewer']); ## Roles/Permissions Assignment & Removal The role/permission assignment and removal are the same, but they take the global `team_id` set on login for sync. + +## Defining a Super-Admin on Teams + +Global roles can be assigned to different teams, `team_id` as the primary key of the relationships is always required. If you want a "Super Admin" global role for a user, when you creates a new team you must assign it to your user. Example: + +```php +namespace App\Models; + +class YourTeamModel extends \Illuminate\Database\Eloquent\Model +{ + // ... + public static function boot() + { + parent::boot(); + + // here assign this team to a global user with global default role + self::created(function ($model) { + // get session team_id for restore it later + $session_team_id = getPermissionsTeamId(); + // set actual new team_id to package instance + setPermissionsTeamId($model); + // get the admin user and assign roles/permissions on new team model + User::find('your_user_id')->assignRole('Super Admin'); + // restore session team_id to package instance + setPermissionsTeamId($session_team_id); + } + } + // ... +} +``` From b7a2df8fa8804ccb194d3759f59b690ad314efe5 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Thu, 28 Oct 2021 09:56:18 +0200 Subject: [PATCH 0469/1013] Update CHANGELOG.md --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e42e28728..c5cdc94b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.2.0 - 2021-10-28 + +- [V5] Fix detaching on all teams intstead of only current #1888 by @erikn69 in https://github.com/spatie/laravel-permission/pull/1890 +- [V5] Add uuid compatibility support on teams by @erikn69 in https://github.com/spatie/laravel-permission/pull/1857 +- Adds setRoleClass method to PermissionRegistrar by @timschwartz in https://github.com/spatie/laravel-permission/pull/1867 +- Load permissions for preventLazyLoading by @bahramsadin in https://github.com/spatie/laravel-permission/pull/1884 +- [V5] Doc for `Super Admin` on teams by @erikn69 in https://github.com/spatie/laravel-permission/pull/1845 + ## 5.1.1 - 2021-09-01 - Avoid Roles over-hydration #1834 From fb4f0278eec48be3e8fb019dd283ea18c25e30d5 Mon Sep 17 00:00:00 2001 From: Rizkhal Date: Fri, 29 Oct 2021 21:55:28 +0900 Subject: [PATCH 0470/1013] Update blade-directives.md (#1903) --- docs/basic-usage/blade-directives.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/blade-directives.md b/docs/basic-usage/blade-directives.md index aafa016b4..a3a4b8af1 100644 --- a/docs/basic-usage/blade-directives.md +++ b/docs/basic-usage/blade-directives.md @@ -92,7 +92,7 @@ Alternatively, `@unlessrole` gives the reverse for checking a singular role, lik You can also determine if a user has exactly all of a given list of roles: ```php -@hasexactroles('writer|admin'); +@hasexactroles('writer|admin') I am both a writer and an admin and nothing else! @else I do not have all of these roles or have more other roles... From b3a995c436e9cd616217153fd49531845bd7c219 Mon Sep 17 00:00:00 2001 From: Muhammad Umar Ulhaq <15181389+ulhaq@users.noreply.github.com> Date: Fri, 29 Oct 2021 15:02:34 +0200 Subject: [PATCH 0471/1013] Option for custom logic for checking permissions (#1891) * option for custom logic for checking permissions added * tests * docs for Custom Permission Check added --- config/permission.php | 7 ++++ .../advanced-usage/custom-permission-check.md | 29 +++++++++++++++ docs/advanced-usage/other.md | 2 +- docs/advanced-usage/phpstorm.md | 2 +- docs/advanced-usage/timestamps.md | 2 +- docs/advanced-usage/ui-options.md | 2 +- docs/advanced-usage/uuid.md | 2 +- src/PermissionServiceProvider.php | 6 ++-- tests/CustomGateTest.php | 35 +++++++++++++++++++ tests/TestCase.php | 1 + 10 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 docs/advanced-usage/custom-permission-check.md create mode 100644 tests/CustomGateTest.php diff --git a/config/permission.php b/config/permission.php index c7029fa5b..5b6e184c3 100644 --- a/config/permission.php +++ b/config/permission.php @@ -96,6 +96,13 @@ 'team_foreign_key' => 'team_id', ], + /* + * When set to true, the method for checking permissions will be registered on the gate. + * Set this to false, if you want to implement custom logic for checking permissions. + */ + + 'register_permission_check_method' => true, + /* * When set to true the package implements teams using the 'team_foreign_key'. If you want * the migrations to register the 'team_foreign_key', you must set this to true diff --git a/docs/advanced-usage/custom-permission-check.md b/docs/advanced-usage/custom-permission-check.md new file mode 100644 index 000000000..f6bfb84d8 --- /dev/null +++ b/docs/advanced-usage/custom-permission-check.md @@ -0,0 +1,29 @@ +--- +title: Custom Permission Check +weight: 6 +--- + +By default, a method is registered on [Laravel's gate](https://laravel.com/docs/authorization). This method is responsible for checking if the user has the required permission or not. Whether a user has a permission or not is determined by checking the user's permissions stored in the database. + +However, in some cases, you might want to implement custom logic for checking if the user has a permission or not. + +Let's say that your application uses access tokens for authentication and when issuing the tokens, you add a custom claim containing all the permissions the user has. In this case, if you want to check whether the user has the required permission or not based on the permissions in your custom claim in the access token, then you need to implement your own logic for handling this. + +You could, for example, create a `before` method to handle this: + +**app/Providers/AuthServiceProvider.php** +```php +public function boot() +{ + ... + + Gate::before(function ($user, $ability) { + return $user->hasTokenPermission($ability) ?: null; + }); +} +``` +Here `hasTokenPermission` is a custom method you need to implement yourself. + +### Register Permission Check Method +By default, `register_permission_check_method` is set to `true`. +Only set this to false if you want to implement custom logic for checking permissions. \ No newline at end of file diff --git a/docs/advanced-usage/other.md b/docs/advanced-usage/other.md index 662dff4cf..5de498560 100644 --- a/docs/advanced-usage/other.md +++ b/docs/advanced-usage/other.md @@ -1,6 +1,6 @@ --- title: Other -weight: 8 +weight: 9 --- Schema Diagram: diff --git a/docs/advanced-usage/phpstorm.md b/docs/advanced-usage/phpstorm.md index 435680b72..205d6a0b3 100644 --- a/docs/advanced-usage/phpstorm.md +++ b/docs/advanced-usage/phpstorm.md @@ -1,6 +1,6 @@ --- title: PhpStorm Interaction -weight: 7 +weight: 8 --- # Extending PhpStorm diff --git a/docs/advanced-usage/timestamps.md b/docs/advanced-usage/timestamps.md index a19247658..7ab577ba4 100644 --- a/docs/advanced-usage/timestamps.md +++ b/docs/advanced-usage/timestamps.md @@ -1,6 +1,6 @@ --- title: Timestamps -weight: 8 +weight: 10 --- ### Excluding Timestamps from JSON diff --git a/docs/advanced-usage/ui-options.md b/docs/advanced-usage/ui-options.md index 347142464..787804ec4 100644 --- a/docs/advanced-usage/ui-options.md +++ b/docs/advanced-usage/ui-options.md @@ -1,6 +1,6 @@ --- title: UI Options -weight: 10 +weight: 11 --- ## Need a UI? diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index a37d1e437..bebbeef73 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -1,6 +1,6 @@ --- title: UUID -weight: 6 +weight: 7 --- If you're using UUIDs or GUIDs for your User models there are a few considerations to note. diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index c0ae0ed15..c185022e6 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -22,8 +22,10 @@ public function boot(PermissionRegistrar $permissionLoader) $this->registerModelBindings(); - $permissionLoader->clearClassPermissions(); - $permissionLoader->registerPermissions(); + if ($this->app->config['permission.register_permission_check_method']) { + $permissionLoader->clearClassPermissions(); + $permissionLoader->registerPermissions(); + } $this->app->singleton(PermissionRegistrar::class, function ($app) use ($permissionLoader) { return $permissionLoader; diff --git a/tests/CustomGateTest.php b/tests/CustomGateTest.php new file mode 100644 index 000000000..74b963105 --- /dev/null +++ b/tests/CustomGateTest.php @@ -0,0 +1,35 @@ +set('permission.register_permission_check_method', false); + } + + /** @test */ + public function it_doesnt_register_the_method_for_checking_permissions_on_the_gate() + { + $this->testUser->givePermissionTo('edit-articles'); + + $this->assertEmpty(app(Gate::class)->abilities()); + $this->assertFalse($this->testUser->can('edit-articles')); + } + + /** @test */ + public function it_can_authorize_using_custom_method_for_checking_permissions() + { + app(Gate::class)->define('edit-articles', function () { + return true; + }); + + $this->assertArrayHasKey('edit-articles', app(Gate::class)->abilities()); + $this->assertTrue($this->testUser->can('edit-articles')); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 350d90188..72c527df8 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -79,6 +79,7 @@ protected function getPackageProviders($app) */ protected function getEnvironmentSetUp($app) { + $app['config']->set('permission.register_permission_check_method', true); $app['config']->set('permission.teams', $this->hasTeams); $app['config']->set('permission.testing', true); //fix sqlite $app['config']->set('permission.column_names.model_morph_key', 'model_test_id'); From 1c9304af746b42308a1d1da2989e12a1439571a9 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Fri, 29 Oct 2021 15:03:02 +0200 Subject: [PATCH 0472/1013] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5cdc94b6..38f70fb79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.3.0 - 2021-10-29 + +- Option for custom logic for checking permissions (#1891) + ## 5.2.0 - 2021-10-28 - [V5] Fix detaching on all teams intstead of only current #1888 by @erikn69 in https://github.com/spatie/laravel-permission/pull/1890 From ee0a50ad174e7f8b14bd17dd2a79ab51ccbac3df Mon Sep 17 00:00:00 2001 From: erikn69 Date: Thu, 4 Nov 2021 06:03:16 -0500 Subject: [PATCH 0473/1013] Fix hints, support int on `scopePermission()` (#1863) (#1908) Co-authored-by: Erik Niebla Co-authored-by: Erik Niebla --- src/Traits/HasPermissions.php | 33 +++++++++++++-------------------- src/Traits/HasRoles.php | 8 ++++---- tests/HasPermissionsTest.php | 16 ++++++++++++++++ 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 3e0d4072b..bfe9a926b 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -16,6 +16,7 @@ trait HasPermissions { + /** @var string */ private $permissionClass; public static function bootHasPermissions() @@ -61,7 +62,7 @@ public function permissions(): BelongsToMany * Scope the model query to certain permissions only. * * @param \Illuminate\Database\Eloquent\Builder $query - * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions + * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions * * @return \Illuminate\Database\Eloquent\Builder */ @@ -86,9 +87,10 @@ public function scopePermission(Builder $query, $permissions): Builder } /** - * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions + * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions * * @return array + * @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist */ protected function convertToPermissionModels($permissions): array { @@ -102,8 +104,9 @@ protected function convertToPermissionModels($permissions): array if ($permission instanceof Permission) { return $permission; } + $method = is_string($permission) ? 'findByName' : 'findById'; - return $this->getPermissionClass()->findByName($permission, $this->getDefaultGuardName()); + return $this->getPermissionClass()->{$method}($permission, $this->getDefaultGuardName()); }, $permissions); } @@ -184,15 +187,6 @@ protected function hasWildcardPermission($permission, $guardName = null): bool return false; } - /** - * @deprecated since 2.35.0 - * @alias of hasPermissionTo() - */ - public function hasUncachedPermissionTo($permission, $guardName = null): bool - { - return $this->hasPermissionTo($permission, $guardName); - } - /** * An alias to hasPermissionTo(), but avoids throwing an exception. * @@ -213,10 +207,9 @@ public function checkPermissionTo($permission, $guardName = null): bool /** * Determine if the model has any of the given permissions. * - * @param array ...$permissions + * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions * * @return bool - * @throws \Exception */ public function hasAnyPermission(...$permissions): bool { @@ -234,7 +227,7 @@ public function hasAnyPermission(...$permissions): bool /** * Determine if the model has all of the given permissions. * - * @param array ...$permissions + * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions * * @return bool * @throws \Exception @@ -335,7 +328,7 @@ protected function getPermissionsRelation() /** * Grant the given permission(s) to a role. * - * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions + * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions * * @return $this */ @@ -392,7 +385,7 @@ function ($object) use ($permissions, $model) { /** * Remove all current permissions and set the given ones. * - * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions + * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions * * @return $this */ @@ -429,7 +422,7 @@ public function getPermissionNames(): Collection } /** - * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions + * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions * * @return \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Permission[]|\Illuminate\Support\Collection */ @@ -487,7 +480,7 @@ public function forgetCachedPermissions() /** * Check if the model has All of the requested Direct permissions. - * @param array ...$permissions + * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions * @return bool */ public function hasAllDirectPermissions(...$permissions): bool @@ -505,7 +498,7 @@ public function hasAllDirectPermissions(...$permissions): bool /** * Check if the model has Any of the requested Direct permissions. - * @param array ...$permissions + * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions * @return bool */ public function hasAnyDirectPermission(...$permissions): bool diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 24283ff66..e9b7b5145 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -13,6 +13,7 @@ trait HasRoles { use HasPermissions; + /** @var string */ private $roleClass; public static function bootHasRoles() @@ -86,9 +87,8 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder } $method = is_numeric($role) ? 'findById' : 'findByName'; - $guard = $guard ?: $this->getDefaultGuardName(); - return $this->getRoleClass()->{$method}($role, $guard); + return $this->getRoleClass()->{$method}($role, $guard ?: $this->getDefaultGuardName()); }, $roles); return $query->whereHas('roles', function (Builder $subQuery) use ($roles) { @@ -114,7 +114,7 @@ protected function getRolesRelation() /** * Assign the given role to the model. * - * @param array|string|int|\Spatie\Permission\Contracts\Role ...$roles + * @param array|string|int|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection ...$roles * * @return $this */ @@ -189,7 +189,7 @@ public function removeRole($role) /** * Remove all current roles and set the given ones. * - * @param array|\Spatie\Permission\Contracts\Role|string|int ...$roles + * @param array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection|string|int ...$roles * * @return $this */ diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index 7507837b6..08a208533 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -65,6 +65,22 @@ public function it_can_scope_users_using_a_string() $this->assertEquals(1, $scopedUsers2->count()); } + /** @test */ + public function it_can_scope_users_using_a_int() + { + $user1 = User::create(['email' => 'user1@test.com']); + $user2 = User::create(['email' => 'user2@test.com']); + $user1->givePermissionTo([1, 2]); + $this->testUserRole->givePermissionTo(1); + $user2->assignRole('testRole'); + + $scopedUsers1 = User::permission(1)->get(); + $scopedUsers2 = User::permission([2])->get(); + + $this->assertEquals(2, $scopedUsers1->count()); + $this->assertEquals(1, $scopedUsers2->count()); + } + /** @test */ public function it_can_scope_users_using_an_array() { From 193e50e44dce7b289f475a7df8782c9be0467e39 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Thu, 4 Nov 2021 12:06:46 +0100 Subject: [PATCH 0474/1013] Update CHANGELOG.md --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38f70fb79..91a4f20c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.3.1 - 2021-11-04 + +- Fix hints, support int on scopePermission (#1908) + ## 5.3.0 - 2021-10-29 - Option for custom logic for checking permissions (#1891) @@ -19,7 +23,7 @@ All notable changes to `laravel-permission` will be documented in this file ## 5.1.0 - 2021-08-31 - No longer flush cache on User role/perm assignment changes #1832 - NOTE: You should test your app to be sure that you don't accidentally have deep dependencies on cache resets happening automatically in these cases. + NOTE: You should test your app to be sure that you don't accidentally have deep dependencies on cache resets happening automatically in these cases. ALSO NOTE: If you have added custom code which depended on these flush operations, you may need to add your own cache-reset calls. ## 5.0.0 - 2021-08-31 From 31ede8fc58c15c1af1a5d83606805be12dbdccaa Mon Sep 17 00:00:00 2001 From: Adriaan Marain Date: Mon, 15 Nov 2021 13:28:47 +0100 Subject: [PATCH 0475/1013] Add changelog workflow (automated commit) --- .github/workflows/update-changelog.yml | 28 ++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/update-changelog.yml diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml new file mode 100644 index 000000000..fa56639f2 --- /dev/null +++ b/.github/workflows/update-changelog.yml @@ -0,0 +1,28 @@ +name: "Update Changelog" + +on: + release: + types: [released] + +jobs: + update: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + ref: main + + - name: Update Changelog + uses: stefanzweifel/changelog-updater-action@v1 + with: + latest-version: ${{ github.event.release.name }} + release-notes: ${{ github.event.release.body }} + + - name: Commit updated CHANGELOG + uses: stefanzweifel/git-auto-commit-action@v4 + with: + branch: main + commit_message: Update CHANGELOG + file_pattern: CHANGELOG.md From fb9e680990b83e6db363b708c4e8fe0d73cfb726 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Wed, 17 Nov 2021 05:44:26 -0500 Subject: [PATCH 0476/1013] Support for custom key names on Role,Permission (#1913) Co-authored-by: Erik Niebla --- src/Models/Permission.php | 4 +++- src/Models/Role.php | 4 +++- src/Traits/HasPermissions.php | 15 ++++++++++----- src/Traits/HasRoles.php | 19 ++++++++++++------- tests/HasPermissionsTest.php | 4 ++-- tests/HasRolesTest.php | 22 +++++++++++----------- 6 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index d38c16d4b..2b253d6f6 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -18,13 +18,15 @@ class Permission extends Model implements PermissionContract use HasRoles; use RefreshesPermissionCache; - protected $guarded = ['id']; + protected $guarded = []; public function __construct(array $attributes = []) { $attributes['guard_name'] = $attributes['guard_name'] ?? config('auth.defaults.guard'); parent::__construct($attributes); + + $this->guarded[] = $this->primaryKey; } public function getTable() diff --git a/src/Models/Role.php b/src/Models/Role.php index a0fd14e92..2c6fcfd25 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -18,13 +18,15 @@ class Role extends Model implements RoleContract use HasPermissions; use RefreshesPermissionCache; - protected $guarded = ['id']; + protected $guarded = []; public function __construct(array $attributes = []) { $attributes['guard_name'] = $attributes['guard_name'] ?? config('auth.defaults.guard'); parent::__construct($attributes); + + $this->guarded[] = $this->primaryKey; } public function getTable() diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index bfe9a926b..e55e73e40 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -76,11 +76,15 @@ public function scopePermission(Builder $query, $permissions): Builder return $query->where(function (Builder $query) use ($permissions, $rolesWithPermissions) { $query->whereHas('permissions', function (Builder $subQuery) use ($permissions) { - $subQuery->whereIn(config('permission.table_names.permissions').'.id', \array_column($permissions, 'id')); + $permissionClass = $this->getPermissionClass(); + $key = (new $permissionClass)->getKeyName(); + $subQuery->whereIn(config('permission.table_names.permissions').".$key", \array_column($permissions, $key)); }); if (count($rolesWithPermissions) > 0) { $query->orWhereHas('roles', function (Builder $subQuery) use ($rolesWithPermissions) { - $subQuery->whereIn(config('permission.table_names.roles').'.id', \array_column($rolesWithPermissions, 'id')); + $roleClass = $this->getRoleClass(); + $key = (new $roleClass)->getKeyName(); + $subQuery->whereIn(config('permission.table_names.roles').".$key", \array_column($rolesWithPermissions, $key)); }); } }); @@ -281,7 +285,7 @@ public function hasDirectPermission($permission): bool throw new PermissionDoesNotExist(); } - return $this->permissions->contains('id', $permission->id); + return $this->permissions->contains($permission->getKeyName(), $permission->getKey()); } /** @@ -334,6 +338,7 @@ protected function getPermissionsRelation() */ public function givePermissionTo(...$permissions) { + $permissionClass = $this->getPermissionClass(); $permissions = collect($permissions) ->flatten() ->map(function ($permission) { @@ -350,11 +355,11 @@ public function givePermissionTo(...$permissions) $this->ensureModelSharesGuard($permission); }) ->map(function ($permission) { - return ['id' => $permission->id, 'values' => PermissionRegistrar::$teams && ! is_a($this, Role::class) ? + return [$permission->getKeyName() => $permission->getKey(), 'values' => PermissionRegistrar::$teams && ! is_a($this, Role::class) ? [PermissionRegistrar::$teamsKey => app(PermissionRegistrar::class)->getPermissionsTeamId()] : [], ]; }) - ->pluck('values', 'id')->toArray(); + ->pluck('values', (new $permissionClass)->getKeyName())->toArray(); $model = $this->getModel(); diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index e9b7b5145..fa1e0aea2 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -92,7 +92,9 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder }, $roles); return $query->whereHas('roles', function (Builder $subQuery) use ($roles) { - $subQuery->whereIn(config('permission.table_names.roles').'.id', \array_column($roles, 'id')); + $roleClass = $this->getRoleClass(); + $key = (new $roleClass)->getKeyName(); + $subQuery->whereIn(config('permission.table_names.roles').".$key", \array_column($roles, $key)); }); } @@ -120,6 +122,7 @@ protected function getRolesRelation() */ public function assignRole(...$roles) { + $roleClass = $this->getRoleClass(); $roles = collect($roles) ->flatten() ->map(function ($role) { @@ -136,11 +139,11 @@ public function assignRole(...$roles) $this->ensureModelSharesGuard($role); }) ->map(function ($role) { - return ['id' => $role->id, 'values' => PermissionRegistrar::$teams && ! is_a($this, Permission::class) ? + return [$role->getKeyName() => $role->getKey(), 'values' => PermissionRegistrar::$teams && ! is_a($this, Permission::class) ? [PermissionRegistrar::$teamsKey => app(PermissionRegistrar::class)->getPermissionsTeamId()] : [], ]; }) - ->pluck('values', 'id')->toArray(); + ->pluck('values', (new $roleClass)->getKeyName())->toArray(); $model = $this->getModel(); @@ -220,13 +223,15 @@ public function hasRole($roles, string $guard = null): bool } if (is_int($roles)) { + $roleClass = $this->getRoleClass(); + $key = (new $roleClass)->getKeyName(); return $guard - ? $this->roles->where('guard_name', $guard)->contains('id', $roles) - : $this->roles->contains('id', $roles); + ? $this->roles->where('guard_name', $guard)->contains($key, $roles) + : $this->roles->contains($key, $roles); } if ($roles instanceof Role) { - return $this->roles->contains('id', $roles->id); + return $this->roles->contains($roles->getKeyName(), $roles->getKey()); } if (is_array($roles)) { @@ -276,7 +281,7 @@ public function hasAllRoles($roles, string $guard = null): bool } if ($roles instanceof Role) { - return $this->roles->contains('id', $roles->id); + return $this->roles->contains($roles->getKeyName(), $roles->getKey()); } $roles = collect()->make($roles)->map(function ($role) { diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index 08a208533..11dd4b0f2 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -418,7 +418,7 @@ public function it_can_sync_multiple_permissions_by_id() { $this->testUser->givePermissionTo('edit-news'); - $ids = app(Permission::class)::whereIn('name', ['edit-articles', 'edit-blog'])->pluck('id'); + $ids = app(Permission::class)::whereIn('name', ['edit-articles', 'edit-blog'])->pluck($this->testUserPermission->getKeyName()); $this->testUser->syncPermissions($ids); @@ -434,7 +434,7 @@ public function sync_permission_ignores_null_inputs() { $this->testUser->givePermissionTo('edit-news'); - $ids = app(Permission::class)::whereIn('name', ['edit-articles', 'edit-blog'])->pluck('id'); + $ids = app(Permission::class)::whereIn('name', ['edit-articles', 'edit-blog'])->pluck($this->testUserPermission->getKeyName()); $ids->push(null); diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index daa556444..bd7415070 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -22,13 +22,13 @@ public function it_can_determine_that_the_user_does_not_have_a_role() $this->assertTrue($this->testUser->hasRole($role->name)); $this->assertTrue($this->testUser->hasRole($role->name, $role->guard_name)); $this->assertTrue($this->testUser->hasRole([$role->name, 'fakeRole'], $role->guard_name)); - $this->assertTrue($this->testUser->hasRole($role->id, $role->guard_name)); - $this->assertTrue($this->testUser->hasRole([$role->id, 'fakeRole'], $role->guard_name)); + $this->assertTrue($this->testUser->hasRole($role->getKey(), $role->guard_name)); + $this->assertTrue($this->testUser->hasRole([$role->getKey(), 'fakeRole'], $role->guard_name)); $this->assertFalse($this->testUser->hasRole($role->name, 'fakeGuard')); $this->assertFalse($this->testUser->hasRole([$role->name, 'fakeRole'], 'fakeGuard')); - $this->assertFalse($this->testUser->hasRole($role->id, 'fakeGuard')); - $this->assertFalse($this->testUser->hasRole([$role->id, 'fakeRole'], 'fakeGuard')); + $this->assertFalse($this->testUser->hasRole($role->getKey(), 'fakeGuard')); + $this->assertFalse($this->testUser->hasRole([$role->getKey(), 'fakeRole'], 'fakeGuard')); $role = app(Role::class)->findOrCreate('testRoleInWebGuard2', 'web'); $this->assertFalse($this->testUser->hasRole($role)); @@ -87,7 +87,7 @@ public function it_can_assign_a_role_using_an_object() /** @test */ public function it_can_assign_a_role_using_an_id() { - $this->testUser->assignRole($this->testUserRole->id); + $this->testUser->assignRole($this->testUserRole->getKey()); $this->assertTrue($this->testUser->hasRole($this->testUserRole)); } @@ -95,7 +95,7 @@ public function it_can_assign_a_role_using_an_id() /** @test */ public function it_can_assign_multiple_roles_at_once() { - $this->testUser->assignRole($this->testUserRole->id, 'testRole2'); + $this->testUser->assignRole($this->testUserRole->getKey(), 'testRole2'); $this->assertTrue($this->testUser->hasRole('testRole')); @@ -105,7 +105,7 @@ public function it_can_assign_multiple_roles_at_once() /** @test */ public function it_can_assign_multiple_roles_using_an_array() { - $this->testUser->assignRole([$this->testUserRole->id, 'testRole2']); + $this->testUser->assignRole([$this->testUserRole->getKey(), 'testRole2']); $this->assertTrue($this->testUser->hasRole('testRole')); @@ -115,7 +115,7 @@ public function it_can_assign_multiple_roles_using_an_array() /** @test */ public function it_does_not_remove_already_associated_roles_when_assigning_new_roles() { - $this->testUser->assignRole($this->testUserRole->id); + $this->testUser->assignRole($this->testUserRole->getKey()); $this->testUser->assignRole('testRole2'); @@ -125,9 +125,9 @@ public function it_does_not_remove_already_associated_roles_when_assigning_new_r /** @test */ public function it_does_not_throw_an_exception_when_assigning_a_role_that_is_already_assigned() { - $this->testUser->assignRole($this->testUserRole->id); + $this->testUser->assignRole($this->testUserRole->getKey()); - $this->testUser->assignRole($this->testUserRole->id); + $this->testUser->assignRole($this->testUserRole->getKey()); $this->assertTrue($this->testUser->fresh()->hasRole('testRole')); } @@ -352,7 +352,7 @@ public function it_can_scope_users_using_an_array_of_ids_and_names() $roleName = $this->testUserRole->name; - $otherRoleId = app(Role::class)->find(2)->id; + $otherRoleId = app(Role::class)->findByName('testRole2')->getKey(); $scopedUsers = User::role([$roleName, $otherRoleId])->get(); From 10d4c14dd66f82ce777ab76f5708bd14ff5a1ef8 Mon Sep 17 00:00:00 2001 From: freekmurze Date: Wed, 17 Nov 2021 10:45:32 +0000 Subject: [PATCH 0477/1013] Fix styling --- src/Traits/HasPermissions.php | 6 +++--- src/Traits/HasRoles.php | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index e55e73e40..6baecb5dc 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -77,13 +77,13 @@ public function scopePermission(Builder $query, $permissions): Builder return $query->where(function (Builder $query) use ($permissions, $rolesWithPermissions) { $query->whereHas('permissions', function (Builder $subQuery) use ($permissions) { $permissionClass = $this->getPermissionClass(); - $key = (new $permissionClass)->getKeyName(); + $key = (new $permissionClass())->getKeyName(); $subQuery->whereIn(config('permission.table_names.permissions').".$key", \array_column($permissions, $key)); }); if (count($rolesWithPermissions) > 0) { $query->orWhereHas('roles', function (Builder $subQuery) use ($rolesWithPermissions) { $roleClass = $this->getRoleClass(); - $key = (new $roleClass)->getKeyName(); + $key = (new $roleClass())->getKeyName(); $subQuery->whereIn(config('permission.table_names.roles').".$key", \array_column($rolesWithPermissions, $key)); }); } @@ -359,7 +359,7 @@ public function givePermissionTo(...$permissions) [PermissionRegistrar::$teamsKey => app(PermissionRegistrar::class)->getPermissionsTeamId()] : [], ]; }) - ->pluck('values', (new $permissionClass)->getKeyName())->toArray(); + ->pluck('values', (new $permissionClass())->getKeyName())->toArray(); $model = $this->getModel(); diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index fa1e0aea2..54378b236 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -93,7 +93,7 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder return $query->whereHas('roles', function (Builder $subQuery) use ($roles) { $roleClass = $this->getRoleClass(); - $key = (new $roleClass)->getKeyName(); + $key = (new $roleClass())->getKeyName(); $subQuery->whereIn(config('permission.table_names.roles').".$key", \array_column($roles, $key)); }); } @@ -143,7 +143,7 @@ public function assignRole(...$roles) [PermissionRegistrar::$teamsKey => app(PermissionRegistrar::class)->getPermissionsTeamId()] : [], ]; }) - ->pluck('values', (new $roleClass)->getKeyName())->toArray(); + ->pluck('values', (new $roleClass())->getKeyName())->toArray(); $model = $this->getModel(); @@ -224,7 +224,8 @@ public function hasRole($roles, string $guard = null): bool if (is_int($roles)) { $roleClass = $this->getRoleClass(); - $key = (new $roleClass)->getKeyName(); + $key = (new $roleClass())->getKeyName(); + return $guard ? $this->roles->where('guard_name', $guard)->contains($key, $roles) : $this->roles->contains($key, $roles); From 7b0e0442b3c9e11d4c001f163bfd3967a103c676 Mon Sep 17 00:00:00 2001 From: freekmurze Date: Wed, 17 Nov 2021 10:48:30 +0000 Subject: [PATCH 0478/1013] Update CHANGELOG --- CHANGELOG.md | 221 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 184 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91a4f20c4..80a6bb6d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.3.2 - 2021-11-17 + +## What's Changed + +- [V5] Support for custom key names on Role,Permission by @erikn69 in https://github.com/spatie/laravel-permission/pull/1913 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.3.1...5.3.2 + ## 5.3.1 - 2021-11-04 - Fix hints, support int on scopePermission (#1908) @@ -19,40 +27,50 @@ All notable changes to `laravel-permission` will be documented in this file - [V5] Doc for `Super Admin` on teams by @erikn69 in https://github.com/spatie/laravel-permission/pull/1845 ## 5.1.1 - 2021-09-01 + - Avoid Roles over-hydration #1834 ## 5.1.0 - 2021-08-31 + - No longer flush cache on User role/perm assignment changes #1832 - NOTE: You should test your app to be sure that you don't accidentally have deep dependencies on cache resets happening automatically in these cases. - ALSO NOTE: If you have added custom code which depended on these flush operations, you may need to add your own cache-reset calls. +- NOTE: You should test your app to be sure that you don't accidentally have deep dependencies on cache resets happening automatically in these cases. +- ALSO NOTE: If you have added custom code which depended on these flush operations, you may need to add your own cache-reset calls. ## 5.0.0 - 2021-08-31 + - Change default-guard-lookup to prefer current user's guard (see BC note in #1817 ) - Teams/Groups feature (see docs, or PR #1804) - Customized pivots instead of `role_id`,`permission_id` #1823 ## 4.4.1 - 2021-09-01 + - Avoid Roles over-hydration #1834 ## 4.4.0 - 2021-08-28 + - Avoid BC break (removed interface change) on cache change added in 4.3.0 #1826 - Made cache even smaller #1826 - Avoid re-sync on non-persisted objects when firing Eloquent::saved #1819 ## 4.3.0 - 2021-08-17 + - Speed up permissions cache lookups, and make cache smaller #1799 ## 4.2.0 - 2021-06-04 + - Add hasExactRoles method #1696 ## 4.1.0 - 2021-06-01 + - Refactor to resolve guard only once during middleware - Refactor service provider by extracting some methods ## 4.0.1 - 2021-03-22 + - Added note in migration for field lengths on MySQL 8. (either shorten the columns to 125 or use InnoDB) ## 4.0.0 - 2021-01-27 + - Drop support on Laravel 5.8 #1615 - Fix bug when adding roles to a model that doesn't yet exist #1663 - Enforce unique constraints on database level #1261 @@ -63,121 +81,154 @@ This package now requires PHP 7.2.5 and Laravel 6.0 or higher. If you are on a PHP version below 7.2.5 or a Laravel version below 6.0 you can use an older version of this package. ## 3.18.0 - 2020-11-27 + - Allow PHP 8.0 ## 3.17.0 - 2020-09-16 + - Optional `$guard` parameter may be passed to `RoleMiddleware`, `PermissionMiddleware`, and `RoleOrPermissionMiddleware`. See #1565 ## 3.16.0 - 2020-08-18 + - Added Laravel 8 support ## 3.15.0 - 2020-08-15 + - Change `users` relationship type to BelongsToMany ## 3.14.0 - 2020-08-15 + - Declare table relations earlier to improve guarded/fillable detection accuracy (relates to Aug 2020 Laravel security patch) ## 3.13.0 - 2020-05-19 + - Provide migration error text to stop caching local config when installing packages. ## 3.12.0 - 2020-05-14 + - Add missing config setting for `display_role_in_exception` - Ensure artisan `permission:show` command uses configured models ## 3.11.0 - 2020-03-03 + - Allow guardName() as a function with priority over $guard_name property #1395 ## 3.10.1 - 2020-03-03 + - Update patch to handle intermittent error in #1370 ## 3.10.0 - 2020-03-02 + - Ugly patch to handle intermittent error: `Trying to access array offset on value of type null` in #1370 ## 3.9.0 - 2020-02-26 + - Add Wildcard Permissions feature #1381 (see PR or docs for details) ## 3.8.0 - 2020-02-18 + - Clear in-memory permissions on boot, for benefit of long running processes like Swoole. #1378 ## 3.7.2 - 2020-02-17 + - Refine test for Lumen dependency. Ref #1371, Fixes #1372. ## 3.7.1 - 2020-02-15 + - Internal refactoring of scopes to use whereIn instead of orWhere #1334, #1335 - Internal refactoring to flatten collection on splat #1341 ## 3.7.0 - 2020-02-15 + - Added methods to check any/all when querying direct permissions #1245 - Removed older Lumen dependencies #1371 ## 3.6.0 - 2020-01-17 + - Added Laravel 7.0 support - Allow splat operator for passing roles to `hasAnyRole()` ## 3.5.0 - 2020-01-07 + - Added missing `guardName` to Exception `PermissionDoesNotExist` #1316 ## 3.4.1 - 2019-12-28 + - Fix 3.4.0 for Lumen ## 3.4.0 - 2019-12-27 + - Make compatible with Swoole - ie: for long-running Laravel instances ## 3.3.1 - 2019-12-24 + - Expose Artisan commands to app layer, not just to console ## 3.3.0 - 2019-11-22 + - Remove duplicate and unreachable code - Remove checks for older Laravel versions ## 3.2.0 - 2019-10-16 + - Implementation of optional guard check for hasRoles and hasAllRoles - See #1236 ## 3.1.0 - 2019-10-16 + - Use bigIncrements/bigInteger in migration - See #1224 ## 3.0.0 - 2019-09-02 + - Update dependencies to allow for Laravel 6.0 - Drop support for Laravel 5.7 and older, and PHP 7.1 and older. (They can use v2 of this package until they upgrade.) -To be clear: v3 requires minimum Laravel 5.8 and PHP 7.2 - +- To be clear: v3 requires minimum Laravel 5.8 and PHP 7.2 ## 2.38.0 - 2019-09-02 + - Allow support for multiple role/permission models - Load roles relationship only when missing - Wrap helpers in function_exists() check ## 2.37.0 - 2019-04-09 + - Added `permission:show` CLI command to display a table of roles/permissions - `removeRole` now returns the model, consistent with other methods - model `$guarded` properties updated to `protected` - README updates ## 2.36.1 - 2019-03-05 + - reverts the changes made in 2.36.0 due to some reported breaks. ## 2.36.0 - 2019-03-04 + - improve performance by reducing another iteration in processing query results and returning earlier ## 2.35.0 - 2019-03-01 + - overhaul internal caching strategy for better performance and fix cache miss when permission names contained spaces - deprecated hasUncachedPermissionTo() (use hasPermissionTo() instead) - added getPermissionNames() method ## 2.34.0 - 2019-02-26 + - Add explicit pivotKeys to roles/permissions BelongsToMany relationships ## 2.33.0 - 2019-02-20 + - Laravel 5.8 compatibility ## 2.32.0 - 2019-02-13 + - Fix duplicate permissions being created through artisan command ## 2.31.0 - 2019-02-03 + - Add custom guard query to role scope - Remove use of array_wrap helper function due to future deprecation ## 2.30.0 - 2019-01-28 + - Change cache config time to DateInterval instead of integer This is in preparation for compatibility with Laravel 5.8's cache TTL change to seconds instead of minutes. @@ -190,321 +241,410 @@ https://laravel-news.com/cache-ttl-change-coming-to-laravel-5-8 https://github.com/laravel/framework/commit/fd6eb89b62ec09df1ffbee164831a827e83fa61d ## 2.29.0 - 2018-12-15 + - Fix bound `saved` event from firing on all subsequent models when calling assignRole or givePermissionTo on unsaved models. However, it is preferable to save the model first, and then add roles/permissions after saving. See #971. ## 2.28.2 - 2018-12-10 + - Use config settings for cache reset in migration stub ## 2.28.1 - 2018-12-07 + - Remove use of Cache facade, for Lumen compatibility ## 2.28.0 - 2018-11-30 -- Rename `getCacheKey` method in HasPermissions trait to `getPermissionCacheKey` for clearer specificity. + +- Rename `getCacheKey` method in HasPermissions trait to `getPermissionCacheKey` for clearer specificity. ## 2.27.0 - 2018-11-21 + - Add ability to specify a cache driver for roles/permissions caching ## 2.26.2 - 2018-11-20 + - Added the ability to reset the permissions cache via an Artisan command: -`php artisan permission:cache-reset` +- `php artisan permission:cache-reset` ## 2.26.1 - 2018-11-19 + - minor update to de-duplicate code overhead - numerous internal updates to cache tests infrastructure ## 2.26.0 - 2018-11-19 + - Substantial speed increase by caching the associations between models and permissions -### NOTES: ### +### NOTES: + The following changes are not "breaking", but worth making the updates to your app for consistency. 1. Config file: The `config/permission.php` file changed to move cache-related settings into a sub-array. **You should review the changes and merge the updates into your own config file.** Specifically the `expiration_time` value has moved into a sub-array entry, and the old top-level entry is no longer used. -See the original config file here: -https://github.com/spatie/laravel-permission/blob/main/config/permission.php +2. See the original config file here: +3. https://github.com/spatie/laravel-permission/blob/main/config/permission.php +4. +5. Cache Resets: If your `app` or `tests` are clearing the cache by specifying the cache key, **it is better to use the built-in forgetCachedPermissions() method** so that it properly handles tagged cache entries. Here is the recommended change: +6. -2. Cache Resets: If your `app` or `tests` are clearing the cache by specifying the cache key, **it is better to use the built-in forgetCachedPermissions() method** so that it properly handles tagged cache entries. Here is the recommended change: ```diff - app()['cache']->forget('spatie.permission.cache'); + $this->app->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions(); -``` - -3. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. +``` +1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. ## 2.25.0 - 2018-11-07 -- A model's `roles` and `permissions` relations (respectively) are now automatically reloaded after an Assign/Remove role or Grant/Revoke of permissions. This means there's no longer a need to call `->fresh()` on the model if the only reason is to reload the role/permission relations. (That said, you may want to call it for other reasons.) + +- A model's `roles` and `permissions` relations (respectively) are now automatically reloaded after an Assign/Remove role or Grant/Revoke of permissions. This means there's no longer a need to call `->fresh()` on the model if the only reason is to reload the role/permission relations. (That said, you may want to call it for other reasons.) - Added support for passing id to HasRole() ## 2.24.0 - 2018-11-06 + - Fix operator used on RoleOrPermissionMiddleware, and avoid throwing PermissionDoesNotExist if invalid permission passed - Auto-reload model role relation after using AssignRole - Avoid empty permission creation when using the CreateRole command ## 2.23.0 - 2018-10-15 + - Avoid unnecessary queries of user roles when fetching all permissions ## 2.22.1 - 2018-10-15 + - Fix Lumen issue with Route helper added in 2.22.0 ## 2.22.0 - 2018-10-11 + - Added `Route::role()` and `Route::permission()` middleware helper functions - Added new `role_or_permission` middleware to allow specifying "or" combinations ## 2.21.0 - 2018-09-29 + - Revert changes from 2.17.1 in order to support Lumen 5.7 ## 2.20.0 - 2018-09-19 -- It will sync roles/permissions to models that are not persisted, by registering a `saved` callback. -(It would previously throw an Integrity constraint violation QueryException on the pivot table insertion.) + +- It will sync roles/permissions to models that are not persisted, by registering a `saved` callback. +- (It would previously throw an Integrity constraint violation QueryException on the pivot table insertion.) ## 2.19.2 - 2018-09-19 + - add `@elserole` directive: - Usage: +- Usage: + ```php @role('roleA') // user hasRole 'roleA' @elserole('roleB') // user hasRole 'roleB' but not 'roleA' @endrole -``` +``` ## 2.19.1 - 2018-09-14 + - Spark-related fix to accommodate missing guard[providers] config ## 2.19.0 - 2018-09-10 + - Add ability to pass in IDs or mixed values to `role` scope - Add `@unlessrole`/`@endunlessrole` Blade directives ## 2.18.0 - 2018-09-06 + - Expanded CLI `permission:create-role` command to create optionally create-and-link permissions in one command. Also now no longer throws an error if the role already exists. ## 2.17.1 - 2018-08-28 -- Require laravel/framework instead of illuminate/* starting from ~5.4.0 + +- Require laravel/framework instead of illuminate/* starting from ~5.4.0 - Removed old dependency for illuminate/database@~5.3.0 (Laravel 5.3 is not supported) ## 2.17.0 - 2018-08-24 + - Laravel 5.7 compatibility ## 2.16.0 - 2018-08-20 + - Replace static Permission::class and Role::class with dynamic value (allows custom models more easily) - Added type checking in hasPermissionTo and hasDirectPermission ## 2.15.0 - 2018-08-15 + - Make assigning the same role or permission twice not throw an exception ## 2.14.0 - 2018-08-13 + - Allow using another key name than `model_id` by defining new `columns` array with `model_morph_key` key in config file. This improves UUID compatibility as discussed in #777. ## 2.13.0 - 2018-08-02 + - Fix issue with null values passed to syncPermissions & syncRoles ## 2.12.2 - 2018-06-13 + - added hasAllPermissions method ## 2.12.1 - 2018-04-23 -- Reverted 2.12.0. REVERTS: "Add ability to pass guard name to gate methods like can()". Requires reworking of guard handling if we're going to add this feature. + +- Reverted 2.12.0. REVERTS: "Add ability to pass guard name to gate methods like can()". Requires reworking of guard handling if we're going to add this feature. ## 2.12.0 - 2018-04-22 + - Add ability to pass guard name to gate methods like can() ## 2.11.0 - 2018-04-16 + - Improve speed of permission lookups with findByName, findById, findOrCreate ## 2.10.0 - 2018-04-15 + - changes the type-hinted Authenticatable to Authorizable in the PermissionRegistrar. -(Previously it was expecting models to implement the Authenticatable contract; but really that should have been Authorizable, since that's where the Gate functionality really is.) +- (Previously it was expecting models to implement the Authenticatable contract; but really that should have been Authorizable, since that's where the Gate functionality really is.) ## 2.9.2 - 2018-03-12 + - Now findOrCreate() exists for both Roles and Permissions - Internal code refactoring for future dev work ## 2.9.1 - 2018-02-23 + - Permissions now support passing integer id for sync, find, hasPermissionTo and hasDirectPermissionTo ## 2.9.0 - 2018-02-07 + - add compatibility with Laravel 5.6 - Allow assign/sync/remove Roles from Permission model ## 2.8.2 - 2018-02-07 + - Allow a collection containing a model to be passed to role/permission scopes ## 2.8.1 - 2018-02-03 + - Fix compatibility with Spark v2.0 to v5.0 ## 2.8.0 - 2018-01-25 + - Support getting guard_name from extended model when using static methods ## 2.7.9 - 2018-01-23 + Changes related to throwing UnauthorizedException: - - When UnauthorizedException is thrown, a property is added with the expected role/permission which triggered it - - A configuration option may be set to include the list of required roles/permissions in the message + +- When UnauthorizedException is thrown, a property is added with the expected role/permission which triggered it +- A configuration option may be set to include the list of required roles/permissions in the message ## 2.7.8 - 2018-01-02 -- REVERTED: Dynamic permission_id and role_id columns according to tables name -NOTE: This Dynamic field naming was a breaking change, so we've removed it for now. + +- REVERTED: Dynamic permission_id and role_id columns according to tables name +- NOTE: This Dynamic field naming was a breaking change, so we've removed it for now. BEST NOT TO USE v2.7.7 if you've changed tablenames in the config file. ## 2.7.7 - 2017-12-31 + - updated `HasPermissions::getStoredPermission` to allow a collection to be returned, and to fix query when passing multiple permissions -- Give and revoke multiple permissions -- Dynamic permission_id and role_id columns according to tables name -- Add findOrCreate function to Permission model +- Give and revoke multiple permissions +- Dynamic permission_id and role_id columns according to tables name +- Add findOrCreate function to Permission model - Improved Lumen support -- Allow guard name to be null for find role by id +- Allow guard name to be null for find role by id ## 2.7.6 - 2017-11-27 + - added Lumen support - updated `HasRole::assignRole` and `HasRole::syncRoles` to accept role id's in addition to role names as arguments ## 2.7.5 - 2017-10-26 + - fixed `Gate::before` for custom gate callbacks ## 2.7.4 - 2017-10-26 + - added cache clearing command in `up` migration for permission tables - use config_path helper for better Lumen support ## 2.7.3 - 2017-10-21 + - refactor middleware to throw custom `UnauthorizedException` (which raises an HttpException with 403 response) -The 403 response is backward compatible +- The 403 response is backward compatible ## 2.7.2 - 2017-10-18 -- refactor `PermissionRegistrar` to use `$gate->before()` + +- refactor `PermissionRegistrar` to use `$gate->before()` - removed `log_registration_exception` as it is no longer relevant ## 2.7.1 - 2017-10-12 + - fixed a bug where `Role`s and `Permission`s got detached when soft deleting a model ## 2.7.0 - 2017-09-27 + - add support for L5.3 ## 2.6.0 - 2017-09-10 + - add `permission` scope ## 2.5.4 - 2017-09-07 + - register the blade directives in the register method of the service provider ## 2.5.3 - 2017-09-07 + - register the blade directives in the boot method of the service provider ## 2.5.2 - 2017-09-05 + - let middleware use caching ## 2.5.1 - 2017-09-02 + - add getRoleNames() method to return a collection of assigned roles ## 2.5.0 - 2017-08-30 + - add compatibility with Laravel 5.5 ## 2.4.2 - 2017-08-11 + - automatically detach roles and permissions when a user gets deleted ## 2.4.1 - 2017-08-05 + - fix processing of pipe symbols in `@hasanyrole` and `@hasallroles` Blade directives ## 2.4.0 -2017-08-05 + - add `PermissionMiddleware` and `RoleMiddleware` ## 2.3.2 - 2017-07-28 + - allow `hasAnyPermission` to take an array of permissions ## 2.3.1 - 2017-07-27 + - fix commands not using custom models ## 2.3.0 - 2017-07-25 + - add `create-permission` and `create-role` commands ## 2.2.0 - 2017-07-01 + - `hasanyrole` and `hasallrole` can accept multiple roles ## 2.1.6 - 2017-06-06 + - fixed a bug where `hasPermissionTo` wouldn't use the right guard name ## 2.1.5 - 2017-05-17 + - fixed a bug that didn't allow you to assign a role or permission when using multiple guards ## 2.1.4 - 2017-05-10 + - add `model_type` to the primary key of tables that use a polymorphic relationship ## 2.1.3 - 2017-04-21 + - fixed a bug where the role()/permission() relation to user models would be saved incorrectly - added users() relation on Permission and Role ## 2.1.2 - 2017-04-20 + - fix a bug where the `role()`/`permission()` relation to user models would be saved incorrectly - add `users()` relation on `Permission` and `Role` ## 2.0.2 - 2017-04-13 + - check for duplicates when adding new roles and permissions ## 2.0.1 - 2017-04-11 + - fix the order of the `foreignKey` and `relatedKey` in the relations ## 2.0.0 - 2017-04-10 + - Requires minimum Laravel 5.4 - cache expiration is now configurable and set to one day by default - roles and permissions can now be assigned to any model through the `HasRoles` trait - removed deprecated `hasPermission` method - renamed config file from `laravel-permission` to `permission`. - ## 1.17.0 - 2018-08-24 + - added support for Laravel 5.7 ## 1.16.0 - 2018-02-07 + - added support for Laravel 5.6 ## 1.15 - 2017-12-08 + - allow `hasAnyPermission` to take an array of permissions ## 1.14.1 - 2017-10-26 + - fixed `Gate::before` for custom gate callbacks ## 1.14.0 - 2017-10-18 -- refactor `PermissionRegistrar` to use `$gate->before()` + +- refactor `PermissionRegistrar` to use `$gate->before()` - removed `log_registration_exception` as it is no longer relevant ## 1.13.0 - 2017-08-31 + - added compatibility for Laravel 5.5 ## 1.12.0 + - made foreign key name to users table configurable ## 1.11.1 + - `hasPermissionTo` uses the cache to avoid extra queries when it is called multiple times ## 1.11.0 + - add `getDirectPermissions`, `getPermissionsViaRoles`, `getAllPermissions` ## 1.10.0 - 2017-02-22 + - add `hasAnyPermission` ## 1.9.0 - 2017-02-20 + - add `log_registration_exception` in settings file -- fix for ambiguous column name `id` when using the role scope +- fix for ambiguous column name `id` when using the role scope ## 1.8.0 - 2017-02-09 + - `hasDirectPermission` method is now public ## 1.7.0 - 2016-01-23 + - added support for Laravel 5.4 ## 1.6.1 - 2016-01-19 + - make exception logging more verbose ## 1.6.0 - 2016-12-27 + - added `Role` scope ## 1.5.3 - 2016-12-15 + - moved some things to `boot` method in SP to solve some compatibility problems with other packages ## 1.5.2 - 2016-08-26 + - make compatible with L5.3 ## 1.5.1 - 2016-07-23 + - fixes `givePermissionTo` and `assignRole` in Laravel 5.1 ## 1.5.0 - 2016-07-23 + ** this version does not work in Laravel 5.1, please upgrade to version 1.5.1 of this package - allowed `givePermissonTo` to accept multiple permissions @@ -514,10 +654,12 @@ The 403 response is backward compatible - dropped support for PHP 5.5 and HHVM ## 1.4.0 - 2016-05-08 -- added `hasPermissionTo` function to the `Role` model + +- added `hasPermissionTo` function to the `Role` model ## 1.3.4 - 2016-02-27 -- `hasAnyRole` can now properly process an array + +- `hasAnyRole` can now properly process an array ## 1.3.3 - 2016-02-24 @@ -542,25 +684,30 @@ The 403 response is backward compatible ## 1.2.0 - 2015-10-28 ###Added + - support for custom models ## 1.1.0 - 2015-10-12 ### Added -- Blade directives + +- Blade directives - `hasAllRoles()`- and `hasAnyRole()`-functions ## 1.0.2 - 2015-10-11 ### Fixed + - Fix for running phpunit locally ## 1.0.1 - 2015-09-30 ### Fixed + - Fixed the inconsistent naming of the `hasPermission`-method. ## 1.0.0 - 2015-09-16 ### Added + - Everything, initial release From e54f376517f698e058c518f73703a0ee59b26521 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Wed, 17 Nov 2021 12:47:22 +0100 Subject: [PATCH 0479/1013] Add support for PHP 8.1 (#1926) * wip * wip * wip * wip * drop support for PHP 7.2 * wip * wip * drop support for Laravel 6 * wip * wip * wip --- .github/workflows/run-tests-L7.yml | 14 +++----------- .github/workflows/run-tests-L8.yml | 10 ++-------- composer.json | 14 +++++++------- 3 files changed, 12 insertions(+), 26 deletions(-) diff --git a/.github/workflows/run-tests-L7.yml b/.github/workflows/run-tests-L7.yml index bde4ee667..4f347b351 100644 --- a/.github/workflows/run-tests-L7.yml +++ b/.github/workflows/run-tests-L7.yml @@ -9,14 +9,12 @@ jobs: strategy: fail-fast: false matrix: - php: [8.0, 7.4, 7.3, 7.2] - laravel: [7.*, 6.*] + php: [8.0, 7.4, 7.3] + laravel: [7.*] dependency-version: [prefer-lowest, prefer-stable] include: - laravel: 7.* - testbench: 5.* - - laravel: 6.* - testbench: 4.* + testbench: 5.20 name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} @@ -24,12 +22,6 @@ jobs: - name: Checkout code uses: actions/checkout@v2 - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: ~/.composer/cache/files - key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} - - name: Setup PHP uses: shivammathur/setup-php@v2 with: diff --git a/.github/workflows/run-tests-L8.yml b/.github/workflows/run-tests-L8.yml index de738eb00..14b15a76d 100644 --- a/.github/workflows/run-tests-L8.yml +++ b/.github/workflows/run-tests-L8.yml @@ -9,12 +9,12 @@ jobs: strategy: fail-fast: false matrix: - php: [8.0, 7.4, 7.3] + php: [8.1, 8.0, 7.4, 7.3] laravel: [8.*] dependency-version: [prefer-lowest, prefer-stable] include: - laravel: 8.* - testbench: 6.* + testbench: 6.23 name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} @@ -22,12 +22,6 @@ jobs: - name: Checkout code uses: actions/checkout@v2 - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: ~/.composer/cache/files - key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} - - name: Setup PHP uses: shivammathur/setup-php@v2 with: diff --git a/composer.json b/composer.json index 2810431cc..7382f27d7 100644 --- a/composer.json +++ b/composer.json @@ -22,15 +22,15 @@ } ], "require": { - "php" : "^7.2.5|^8.0", - "illuminate/auth": "^6.0|^7.0|^8.0", - "illuminate/container": "^6.0|^7.0|^8.0", - "illuminate/contracts": "^6.0|^7.0|^8.0", - "illuminate/database": "^6.0|^7.0|^8.0" + "php" : "^7.3|^8.0|^8.1", + "illuminate/auth": "^7.0|^8.0", + "illuminate/container": "^7.0|^8.0", + "illuminate/contracts": "^7.0|^8.0", + "illuminate/database": "^7.0|^8.0" }, "require-dev": { - "orchestra/testbench": "^4.0|^5.0|^6.0", - "phpunit/phpunit": "^8.0|^9.0", + "orchestra/testbench": "^5.0|^6.0", + "phpunit/phpunit": "^9.4", "predis/predis": "^1.1" }, "autoload": { From 08cc9bdbf89640cb1d64fb01eb9513c1cbb2549d Mon Sep 17 00:00:00 2001 From: freekmurze Date: Wed, 17 Nov 2021 11:48:05 +0000 Subject: [PATCH 0480/1013] Update CHANGELOG --- CHANGELOG.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80a6bb6d6..980ea5ecd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.4.0 - 2021-11-17 + +## What's Changed + +- Add support for PHP 8.1 by @freekmurze in https://github.com/spatie/laravel-permission/pull/1926 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.3.2...5.4.0 + ## 5.3.2 - 2021-11-17 ## What's Changed @@ -289,12 +297,13 @@ The following changes are not "breaking", but worth making the updates to your a - app()['cache']->forget('spatie.permission.cache'); + $this->app->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions(); + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. ## 2.25.0 - 2018-11-07 -- A model's `roles` and `permissions` relations (respectively) are now automatically reloaded after an Assign/Remove role or Grant/Revoke of permissions. This means there's no longer a need to call `->fresh()` on the model if the only reason is to reload the role/permission relations. (That said, you may want to call it for other reasons.) +- A model's `roles` and `permissions` relations (respectively) are now automatically reloaded after an Assign/Remove role or Grant/Revoke of permissions. This means there's no longer a need to call `-&gt;fresh()` on the model if the only reason is to reload the role/permission relations. (That said, you may want to call it for other reasons.) - Added support for passing id to HasRole() ## 2.24.0 - 2018-11-06 @@ -337,6 +346,7 @@ The following changes are not "breaking", but worth making the updates to your a // user hasRole 'roleB' but not 'roleA' @endrole + ``` ## 2.19.1 - 2018-09-14 @@ -468,7 +478,7 @@ BEST NOT TO USE v2.7.7 if you've changed tablenames in the config file. ## 2.7.2 - 2017-10-18 -- refactor `PermissionRegistrar` to use `$gate->before()` +- refactor `PermissionRegistrar` to use `$gate-&gt;before()` - removed `log_registration_exception` as it is no longer relevant ## 2.7.1 - 2017-10-12 @@ -587,7 +597,7 @@ BEST NOT TO USE v2.7.7 if you've changed tablenames in the config file. ## 1.14.0 - 2017-10-18 -- refactor `PermissionRegistrar` to use `$gate->before()` +- refactor `PermissionRegistrar` to use `$gate-&gt;before()` - removed `log_registration_exception` as it is no longer relevant ## 1.13.0 - 2017-08-31 From a93244a9027be38584855042b3a223086b81444a Mon Sep 17 00:00:00 2001 From: Luke Downing Date: Fri, 17 Dec 2021 10:06:14 +0000 Subject: [PATCH 0481/1013] Adds documentation for `LazilyRefreshDatabase` (#1952) * Adds documentation for `LazilyRefreshDatabase` Howdy! When using `LazilyRefreshDatabase`, you ideally want to avoid seeding before each and every test, because that makes the trait useless. The approach described in this doc PR shows how to get around this, and also shows the user how to avoid a caveat with cached permissions when lazily refreshing the database. Thanks for all the hard work! Kind Regards, Luke * Update testing.md --- docs/advanced-usage/testing.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/advanced-usage/testing.md b/docs/advanced-usage/testing.md index bd9e64c50..d0cd3e7fe 100644 --- a/docs/advanced-usage/testing.md +++ b/docs/advanced-usage/testing.md @@ -20,6 +20,17 @@ In your tests simply add a `setUp()` instruction to re-register the permissions, } ``` +If you are using Laravel's `LazilyRefreshDatabase` trait, you most likely want to avoid seeding permissions before every test, because that would negate the use of the `LazilyRefreshDatabase` trait. To overcome this, you should wrap your seeder in an event listener for the `MigrationsEnded` event: + +```php +Event::listen(MigrationsEnded::class, function () { + $this->artisan('db:seed', ['--class' => RoleAndPermissionSeeder::class]); + $this->app->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions(); +}); +``` + +Note that we call `PermissionRegistrar::forgetCachedPermissions` after seeding. This is to prevent a caching issue that can occur when the database is set up after permissions have already been registered and cached. + ## Factories Many applications do not require using factories to create fake roles/permissions for testing, because they use a Seeder to create specific roles and permissions that the application uses; thus tests are performed using the declared roles and permissions. From 51cef5b4a2cc593d5b1db22f7e568c08a7488b2c Mon Sep 17 00:00:00 2001 From: PaolaRuby <79208489+PaolaRuby@users.noreply.github.com> Date: Fri, 17 Dec 2021 05:06:33 -0500 Subject: [PATCH 0482/1013] Fix .gitattributes (#1945) --- .gitattributes | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitattributes b/.gitattributes index 7742c9ae4..5a0aa162e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,11 +2,15 @@ # https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html # Ignore all test and documentation with "export-ignore". +/.github export-ignore /.gitattributes export-ignore /.gitignore export-ignore /.travis.yml export-ignore /phpunit.xml.dist export-ignore /.scrutinizer.yml export-ignore +/art export-ignore +/docs export-ignore /tests export-ignore /.editorconfig export-ignore +/.php_cs.dist.php export-ignore /.styleci.yml export-ignore From 1fdb51702f5176469f2fc38d25c18fd373e11d84 Mon Sep 17 00:00:00 2001 From: Ali Ghasemzadeh Date: Fri, 17 Dec 2021 13:36:56 +0330 Subject: [PATCH 0483/1013] Livewire Role and Permissions. (#1928) Livewire Role and Permissions. --- docs/advanced-usage/ui-options.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/advanced-usage/ui-options.md b/docs/advanced-usage/ui-options.md index 787804ec4..e8029ff22 100644 --- a/docs/advanced-usage/ui-options.md +++ b/docs/advanced-usage/ui-options.md @@ -18,3 +18,6 @@ The package doesn't come with any screens out of the box, you should build that - [Laravel User Management for managing users, roles, permissions, departments and authorization](https://github.com/Mekaeil/LaravelUserManagement) by [Mekaeil](https://github.com/Mekaeil) - [Generating UI boilerplate using InfyOm](https://youtu.be/hlGu2pa1bdU) video tutorial by [Shailesh](https://github.com/shailesh-ladumor) + + +- [LiveWire Base Admin Panel](https://github.com/alighasemzadeh/bap) User management by [AliGhasemzadeh](https://github.com/alighasemzadeh) From 3f3d8742d2be29b35ed41a0c593f2e41a3f1dfe3 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Fri, 17 Dec 2021 05:15:20 -0500 Subject: [PATCH 0484/1013] Fix teams upgrade migration (#1959) Co-authored-by: Erik Niebla --- database/migrations/add_teams_fields.php.stub | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/database/migrations/add_teams_fields.php.stub b/database/migrations/add_teams_fields.php.stub index 727104f17..55d2afc37 100644 --- a/database/migrations/add_teams_fields.php.stub +++ b/database/migrations/add_teams_fields.php.stub @@ -1,8 +1,10 @@ unsignedBigInteger($columnNames['team_foreign_key'])->nullable(); $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index'); - } - }); + }); + } - Schema::table($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) { - if (! Schema::hasColumn($tableNames['model_has_permissions'], $columnNames['team_foreign_key'])) { + if (! Schema::hasColumn($tableNames['model_has_permissions'], $columnNames['team_foreign_key'])) { + Schema::table($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) { $table->unsignedBigInteger($columnNames['team_foreign_key'])->default('1');; $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index'); + if (DB::getDriverName() !== 'sqlite') { + $table->dropForeign([PermissionRegistrar::$pivotPermission]); + } $table->dropPrimary(); - $table->primary([$columnNames['team_foreign_key'], 'permission_id', $columnNames['model_morph_key'], 'model_type'], + + $table->primary([$columnNames['team_foreign_key'], PermissionRegistrar::$pivotPermission, $columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_permission_model_type_primary'); - } - }); + if (DB::getDriverName() !== 'sqlite') { + $table->foreign(PermissionRegistrar::$pivotPermission) + ->references('id')->on($tableNames['permissions'])->onDelete('cascade'); + } + }); + } - Schema::table($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames) { - if (! Schema::hasColumn($tableNames['model_has_roles'], $columnNames['team_foreign_key'])) { + if (! Schema::hasColumn($tableNames['model_has_roles'], $columnNames['team_foreign_key'])) { + Schema::table($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames) { $table->unsignedBigInteger($columnNames['team_foreign_key'])->default('1');; $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index'); + if (DB::getDriverName() !== 'sqlite') { + $table->dropForeign([PermissionRegistrar::$pivotRole]); + } $table->dropPrimary(); - $table->primary([$columnNames['team_foreign_key'], 'role_id', $columnNames['model_morph_key'], 'model_type'], + + $table->primary([$columnNames['team_foreign_key'], PermissionRegistrar::$pivotRole, $columnNames['model_morph_key'], 'model_type'], 'model_has_roles_role_model_type_primary'); - } - }); + if (DB::getDriverName() !== 'sqlite') { + $table->foreign(PermissionRegistrar::$pivotRole) + ->references('id')->on($tableNames['roles'])->onDelete('cascade'); + } + }); + } app('cache') ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null) From 79f7dbf2b6b21d81209774f5726a0e9f4222dd92 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Fri, 17 Dec 2021 05:18:05 -0500 Subject: [PATCH 0485/1013] [V5] WherePivot instead of only Where on team relation pivot, better readability (#1944) * WherePivot instead of only Where on team relation pivot * Fix detach, sync after wherePivot on relations Co-authored-by: Erik Niebla --- src/Traits/HasPermissions.php | 65 +++++++++++------------------- src/Traits/HasRoles.php | 76 +++++++++++++---------------------- src/helpers.php | 2 +- 3 files changed, 53 insertions(+), 90 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 6baecb5dc..00e31ce74 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -44,18 +44,19 @@ public function getPermissionClass() */ public function permissions(): BelongsToMany { - return $this->morphToMany( + $relation = $this->morphToMany( config('permission.models.permission'), 'model', config('permission.table_names.model_has_permissions'), config('permission.column_names.model_morph_key'), PermissionRegistrar::$pivotPermission - ) - ->where(function ($q) { - $q->when(PermissionRegistrar::$teams, function ($q) { - $q->where(PermissionRegistrar::$teamsKey, app(PermissionRegistrar::class)->getPermissionsTeamId()); - }); - }); + ); + + if (! PermissionRegistrar::$teams) { + return $relation; + } + + return $relation->wherePivot(PermissionRegistrar::$teamsKey, getPermissionsTeamId()); } /** @@ -314,21 +315,6 @@ public function getAllPermissions(): Collection return $permissions->sort()->values(); } - /** - * Add teams pivot if teams are enabled - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany - */ - protected function getPermissionsRelation() - { - $relation = $this->permissions(); - if (PermissionRegistrar::$teams && ! is_a($this, Role::class)) { - $relation->wherePivot(PermissionRegistrar::$teamsKey, app(PermissionRegistrar::class)->getPermissionsTeamId()); - } - - return $relation; - } - /** * Grant the given permission(s) to a role. * @@ -338,33 +324,30 @@ protected function getPermissionsRelation() */ public function givePermissionTo(...$permissions) { - $permissionClass = $this->getPermissionClass(); $permissions = collect($permissions) ->flatten() - ->map(function ($permission) { + ->reduce(function ($array, $permission) { if (empty($permission)) { - return false; + return $array; + } + + $permission = $this->getStoredPermission($permission); + if (! $permission instanceof Permission) { + return $array; } - return $this->getStoredPermission($permission); - }) - ->filter(function ($permission) { - return $permission instanceof Permission; - }) - ->each(function ($permission) { $this->ensureModelSharesGuard($permission); - }) - ->map(function ($permission) { - return [$permission->getKeyName() => $permission->getKey(), 'values' => PermissionRegistrar::$teams && ! is_a($this, Role::class) ? - [PermissionRegistrar::$teamsKey => app(PermissionRegistrar::class)->getPermissionsTeamId()] : [], - ]; - }) - ->pluck('values', (new $permissionClass())->getKeyName())->toArray(); + + $array[$permission->getKey()] = PermissionRegistrar::$teams && ! is_a($this, Role::class) ? + [PermissionRegistrar::$teamsKey => getPermissionsTeamId()] : []; + + return $array; + }, []); $model = $this->getModel(); if ($model->exists) { - $this->getPermissionsRelation()->sync($permissions, false); + $this->permissions()->sync($permissions, false); $model->load('permissions'); } else { $class = \get_class($model); @@ -396,7 +379,7 @@ function ($object) use ($permissions, $model) { */ public function syncPermissions(...$permissions) { - $this->getPermissionsRelation()->detach(); + $this->permissions()->detach(); return $this->givePermissionTo($permissions); } @@ -410,7 +393,7 @@ public function syncPermissions(...$permissions) */ public function revokePermissionTo($permission) { - $this->getPermissionsRelation()->detach($this->getStoredPermission($permission)); + $this->permissions()->detach($this->getStoredPermission($permission)); if (is_a($this, get_class(app(PermissionRegistrar::class)->getRoleClass()))) { $this->forgetCachedPermissions(); diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 54378b236..926381850 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -41,25 +41,23 @@ public function getRoleClass() */ public function roles(): BelongsToMany { - $model_has_roles = config('permission.table_names.model_has_roles'); - - return $this->morphToMany( + $relation = $this->morphToMany( config('permission.models.role'), 'model', - $model_has_roles, + config('permission.table_names.model_has_roles'), config('permission.column_names.model_morph_key'), PermissionRegistrar::$pivotRole - ) - ->where(function ($q) use ($model_has_roles) { - $q->when(PermissionRegistrar::$teams, function ($q) use ($model_has_roles) { - $teamId = app(PermissionRegistrar::class)->getPermissionsTeamId(); - $q->where($model_has_roles.'.'.PermissionRegistrar::$teamsKey, $teamId) - ->where(function ($q) use ($teamId) { - $teamField = config('permission.table_names.roles').'.'.PermissionRegistrar::$teamsKey; - $q->whereNull($teamField)->orWhere($teamField, $teamId); - }); + ); + + if (! PermissionRegistrar::$teams) { + return $relation; + } + + return $relation->wherePivot(PermissionRegistrar::$teamsKey, getPermissionsTeamId()) + ->where(function ($q) { + $teamField = config('permission.table_names.roles').'.'.PermissionRegistrar::$teamsKey; + $q->whereNull($teamField)->orWhere($teamField, getPermissionsTeamId()); }); - }); } /** @@ -98,21 +96,6 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder }); } - /** - * Add teams pivot if teams are enabled - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany - */ - protected function getRolesRelation() - { - $relation = $this->roles(); - if (PermissionRegistrar::$teams && ! is_a($this, Permission::class)) { - $relation->wherePivot(PermissionRegistrar::$teamsKey, app(PermissionRegistrar::class)->getPermissionsTeamId()); - } - - return $relation; - } - /** * Assign the given role to the model. * @@ -122,33 +105,30 @@ protected function getRolesRelation() */ public function assignRole(...$roles) { - $roleClass = $this->getRoleClass(); $roles = collect($roles) ->flatten() - ->map(function ($role) { + ->reduce(function ($array, $role) { if (empty($role)) { - return false; + return $array; + } + + $role = $this->getStoredRole($role); + if (! $role instanceof Role) { + return $array; } - return $this->getStoredRole($role); - }) - ->filter(function ($role) { - return $role instanceof Role; - }) - ->each(function ($role) { $this->ensureModelSharesGuard($role); - }) - ->map(function ($role) { - return [$role->getKeyName() => $role->getKey(), 'values' => PermissionRegistrar::$teams && ! is_a($this, Permission::class) ? - [PermissionRegistrar::$teamsKey => app(PermissionRegistrar::class)->getPermissionsTeamId()] : [], - ]; - }) - ->pluck('values', (new $roleClass())->getKeyName())->toArray(); + + $array[$role->getKey()] = PermissionRegistrar::$teams && ! is_a($this, Permission::class) ? + [PermissionRegistrar::$teamsKey => getPermissionsTeamId()] : []; + + return $array; + }, []); $model = $this->getModel(); if ($model->exists) { - $this->getRolesRelation()->sync($roles, false); + $this->roles()->sync($roles, false); $model->load('roles'); } else { $class = \get_class($model); @@ -178,7 +158,7 @@ function ($object) use ($roles, $model) { */ public function removeRole($role) { - $this->getRolesRelation()->detach($this->getStoredRole($role)); + $this->roles()->detach($this->getStoredRole($role)); $this->load('roles'); @@ -198,7 +178,7 @@ public function removeRole($role) */ public function syncRoles(...$roles) { - $this->getRolesRelation()->detach(); + $this->roles()->detach(); return $this->assignRole($roles); } diff --git a/src/helpers.php b/src/helpers.php index f4df0a7cc..25116d0ff 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -36,6 +36,6 @@ function setPermissionsTeamId($id) */ function getPermissionsTeamId() { - app(\Spatie\Permission\PermissionRegistrar::class)->getPermissionsTeamId(); + return app(\Spatie\Permission\PermissionRegistrar::class)->getPermissionsTeamId(); } } From 94443e59d64c90186c65000ac0859c8120f76379 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Thu, 23 Dec 2021 22:16:13 +0100 Subject: [PATCH 0486/1013] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index df0e89b52..141fb33de 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ ## Documentation, Installation, and Usage Instructions -See the [DOCUMENTATION](https://docs.spatie.be/laravel-permission/) for detailed installation and usage instructions. +See the [documentation](https://spatie.be/docs/laravel-permission/) for detailed installation and usage instructions. ## What It Does This package allows you to manage user permissions and roles in a database. From 46fc8f201fa4bc771ba8be11d47cc00df3933a10 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Mon, 3 Jan 2022 06:23:42 -0500 Subject: [PATCH 0487/1013] Fix links (#1973) Co-authored-by: Erik Niebla --- docs/introduction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/introduction.md b/docs/introduction.md index 466402858..604fa8cac 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -17,7 +17,7 @@ $user->assignRole('writer'); $role->givePermissionTo('edit articles'); ``` -If you're using multiple guards we've got you covered as well. Every guard will have its own set of permissions and roles that can be assigned to the guard's users. Read about it in the [using multiple guards](https://docs.spatie.be/laravel-permission/v5/basic-usage/multiple-guards/) section. +If you're using multiple guards we've got you covered as well. Every guard will have its own set of permissions and roles that can be assigned to the guard's users. Read about it in the [using multiple guards](./basic-usage/multiple-guards/) section. Because all permissions will be registered on [Laravel's gate](https://laravel.com/docs/authorization), you can check if a user has a permission with Laravel's default `can` function: From 626b3ae8c8fc0670fb479e8d7ff9c2e4a210e9ed Mon Sep 17 00:00:00 2001 From: erikn69 Date: Mon, 3 Jan 2022 06:24:35 -0500 Subject: [PATCH 0488/1013] Fix #1966 `Duplicate entry 'roles_name_guard_name_unique'` (#1970) Co-authored-by: Erik Niebla --- database/migrations/add_teams_fields.php.stub | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/database/migrations/add_teams_fields.php.stub b/database/migrations/add_teams_fields.php.stub index 55d2afc37..6df2378bf 100644 --- a/database/migrations/add_teams_fields.php.stub +++ b/database/migrations/add_teams_fields.php.stub @@ -31,8 +31,11 @@ class AddTeamsFields extends Migration if (! Schema::hasColumn($tableNames['roles'], $columnNames['team_foreign_key'])) { Schema::table($tableNames['roles'], function (Blueprint $table) use ($columnNames) { - $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable(); + $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable()->after('id'); $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index'); + + $table->dropUnique('roles_name_guard_name_unique'); + $table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']); }); } From 0da24dbdfe4611be1f56b4310e789b5ae4de4d1a Mon Sep 17 00:00:00 2001 From: Ahmed Kamel Date: Mon, 3 Jan 2022 13:24:50 +0200 Subject: [PATCH 0489/1013] fix use multiple guards (#1965) --- docs/basic-usage/basic-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/basic-usage.md b/docs/basic-usage/basic-usage.md index f1508ea5a..28c30efd2 100644 --- a/docs/basic-usage/basic-usage.md +++ b/docs/basic-usage/basic-usage.md @@ -50,7 +50,7 @@ $role->revokePermissionTo($permission); $permission->removeRole($role); ``` -If you're using multiple guards the `guard_name` attribute needs to be set as well. Read about it in the [using multiple guards](../multiple-guards) section of the readme. +If you're using multiple guards the `guard_name` attribute needs to be set as well. Read about it in the [using multiple guards](./multiple-guards) section of the readme. The `HasRoles` trait adds Eloquent relationships to your models, which can be accessed directly or used as a base query: From 68d3ef3e179314fdac468eb230ca003b4c1fc3af Mon Sep 17 00:00:00 2001 From: erikn69 Date: Mon, 3 Jan 2022 06:26:22 -0500 Subject: [PATCH 0490/1013] Teams test fixes, global helper setPermissionsTeamId on tests (#1911) Co-authored-by: Erik Niebla --- docs/basic-usage/teams-permissions.md | 4 +- src/PermissionRegistrar.php | 4 +- tests/HasPermissionsTest.php | 17 +++---- tests/HasPermissionsWithCustomModelsTest.php | 5 +- tests/HasRolesTest.php | 15 +++--- tests/HasRolesWithCustomModelsTest.php | 15 ++++++ tests/TeamHasPermissionsTest.php | 53 +++++++++++++++----- tests/TeamHasRolesTest.php | 44 +++++++++++++--- tests/TestCase.php | 33 +++--------- 9 files changed, 120 insertions(+), 70 deletions(-) create mode 100644 tests/HasRolesWithCustomModelsTest.php diff --git a/docs/basic-usage/teams-permissions.md b/docs/basic-usage/teams-permissions.md index a9152ee71..9c57ffc15 100644 --- a/docs/basic-usage/teams-permissions.md +++ b/docs/basic-usage/teams-permissions.md @@ -34,12 +34,12 @@ class TeamsPermission{ public function handle($request, \Closure $next){ if(!empty(auth()->user())){ // session value set on login - app(\Spatie\Permission\PermissionRegistrar::class)->setPermissionsTeamId(session('team_id')); + setPermissionsTeamId(session('team_id')); } // other custom ways to get team_id /*if(!empty(auth('api')->user())){ // `getTeamIdFromToken()` example of custom method for getting the set team_id - app(\Spatie\Permission\PermissionRegistrar::class)->setPermissionsTeamId(auth('api')->user()->getTeamIdFromToken()); + setPermissionsTeamId(auth('api')->user()->getTeamIdFromToken()); }*/ return $next($request); diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 0326be646..6d228440b 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -41,7 +41,7 @@ class PermissionRegistrar /** @var string */ public static $teamsKey; - /** @var int */ + /** @var int|string */ protected $teamId = null; /** @var string */ @@ -163,7 +163,7 @@ public function clearClassPermissions() */ private function loadPermissions() { - if ($this->permissions !== null) { + if ($this->permissions) { return; } diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index 11dd4b0f2..df793e451 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -2,6 +2,7 @@ namespace Spatie\Permission\Test; +use DB; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Contracts\Role; use Spatie\Permission\Exceptions\GuardDoesNotMatch; @@ -381,7 +382,7 @@ public function it_can_list_all_the_permissions_via_roles_of_user() $this->assertEquals( collect(['edit-articles', 'edit-news']), - $this->testUser->getPermissionsViaRoles()->pluck('name') + $this->testUser->getPermissionsViaRoles()->pluck('name')->sort()->values() ); } @@ -491,17 +492,16 @@ public function calling_givePermissionTo_before_saving_object_doesnt_interfere_w $user2 = new User(['email' => 'test2@user.com']); $user2->givePermissionTo('edit-articles'); - \DB::enableQueryLog(); + DB::enableQueryLog(); $user2->save(); - $querys = \DB::getQueryLog(); - \DB::disableQueryLog(); + DB::disableQueryLog(); $this->assertTrue($user->fresh()->hasPermissionTo('edit-news')); $this->assertFalse($user->fresh()->hasPermissionTo('edit-articles')); $this->assertTrue($user2->fresh()->hasPermissionTo('edit-articles')); $this->assertFalse($user2->fresh()->hasPermissionTo('edit-news')); - $this->assertSame(4, count($querys)); //avoid unnecessary sync + $this->assertSame(4, count(DB::getQueryLog())); //avoid unnecessary sync } /** @test */ @@ -514,17 +514,16 @@ public function calling_syncPermissions_before_saving_object_doesnt_interfere_wi $user2 = new User(['email' => 'test2@user.com']); $user2->syncPermissions('edit-articles'); - \DB::enableQueryLog(); + DB::enableQueryLog(); $user2->save(); - $querys = \DB::getQueryLog(); - \DB::disableQueryLog(); + DB::disableQueryLog(); $this->assertTrue($user->fresh()->hasPermissionTo('edit-news')); $this->assertFalse($user->fresh()->hasPermissionTo('edit-articles')); $this->assertTrue($user2->fresh()->hasPermissionTo('edit-articles')); $this->assertFalse($user2->fresh()->hasPermissionTo('edit-news')); - $this->assertSame(4, count($querys)); //avoid unnecessary sync + $this->assertSame(4, count(DB::getQueryLog())); //avoid unnecessary sync } /** @test */ diff --git a/tests/HasPermissionsWithCustomModelsTest.php b/tests/HasPermissionsWithCustomModelsTest.php index 92970121a..bdb237a5c 100644 --- a/tests/HasPermissionsWithCustomModelsTest.php +++ b/tests/HasPermissionsWithCustomModelsTest.php @@ -8,9 +8,8 @@ class HasPermissionsWithCustomModelsTest extends HasPermissionsTest protected $useCustomModels = true; /** @test */ - public function it_can_use_custom_models() + public function it_can_use_custom_model_permission() { - $this->assertSame(get_class($this->testUserPermission), \Spatie\Permission\Test\Permission::class); - $this->assertSame(get_class($this->testUserRole), \Spatie\Permission\Test\Role::class); + $this->assertSame(get_class($this->testUserPermission), Permission::class); } } diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index bd7415070..0031b58da 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -2,6 +2,7 @@ namespace Spatie\Permission\Test; +use DB; use Spatie\Permission\Contracts\Role; use Spatie\Permission\Exceptions\GuardDoesNotMatch; use Spatie\Permission\Exceptions\RoleDoesNotExist; @@ -246,17 +247,16 @@ public function calling_syncRoles_before_saving_object_doesnt_interfere_with_oth $user2 = new User(['email' => 'admin@user.com']); $user2->syncRoles('testRole2'); - \DB::enableQueryLog(); + DB::enableQueryLog(); $user2->save(); - $querys = \DB::getQueryLog(); - \DB::disableQueryLog(); + DB::disableQueryLog(); $this->assertTrue($user->fresh()->hasRole('testRole')); $this->assertFalse($user->fresh()->hasRole('testRole2')); $this->assertTrue($user2->fresh()->hasRole('testRole2')); $this->assertFalse($user2->fresh()->hasRole('testRole')); - $this->assertSame(4, count($querys)); //avoid unnecessary sync + $this->assertSame(4, count(DB::getQueryLog())); //avoid unnecessary sync } /** @test */ @@ -269,17 +269,16 @@ public function calling_assignRole_before_saving_object_doesnt_interfere_with_ot $admin_user = new User(['email' => 'admin@user.com']); $admin_user->assignRole('testRole2'); - \DB::enableQueryLog(); + DB::enableQueryLog(); $admin_user->save(); - $querys = \DB::getQueryLog(); - \DB::disableQueryLog(); + DB::disableQueryLog(); $this->assertTrue($user->fresh()->hasRole('testRole')); $this->assertFalse($user->fresh()->hasRole('testRole2')); $this->assertTrue($admin_user->fresh()->hasRole('testRole2')); $this->assertFalse($admin_user->fresh()->hasRole('testRole')); - $this->assertSame(4, count($querys)); //avoid unnecessary sync + $this->assertSame(4, count(DB::getQueryLog())); //avoid unnecessary sync } /** @test */ diff --git a/tests/HasRolesWithCustomModelsTest.php b/tests/HasRolesWithCustomModelsTest.php new file mode 100644 index 000000000..d39739205 --- /dev/null +++ b/tests/HasRolesWithCustomModelsTest.php @@ -0,0 +1,15 @@ +assertSame(get_class($this->testUserRole), Role::class); + } +} diff --git a/tests/TeamHasPermissionsTest.php b/tests/TeamHasPermissionsTest.php index 4fc345225..ccc3de93a 100644 --- a/tests/TeamHasPermissionsTest.php +++ b/tests/TeamHasPermissionsTest.php @@ -10,15 +10,15 @@ class TeamHasPermissionsTest extends HasPermissionsTest /** @test */ public function it_can_assign_same_and_different_permission_on_same_user_on_different_teams() { - $this->setPermissionsTeamId(1); + setPermissionsTeamId(1); $this->testUser->load('permissions'); $this->testUser->givePermissionTo('edit-articles', 'edit-news'); - $this->setPermissionsTeamId(2); + setPermissionsTeamId(2); $this->testUser->load('permissions'); $this->testUser->givePermissionTo('edit-articles', 'edit-blog'); - $this->setPermissionsTeamId(1); + setPermissionsTeamId(1); $this->testUser->load('permissions'); $this->assertEquals( collect(['edit-articles', 'edit-news']), @@ -27,7 +27,7 @@ public function it_can_assign_same_and_different_permission_on_same_user_on_diff $this->assertTrue($this->testUser->hasAllDirectPermissions(['edit-articles', 'edit-news'])); $this->assertFalse($this->testUser->hasAllDirectPermissions(['edit-articles', 'edit-blog'])); - $this->setPermissionsTeamId(2); + setPermissionsTeamId(2); $this->testUser->load('permissions'); $this->assertEquals( collect(['edit-articles', 'edit-blog']), @@ -42,17 +42,17 @@ public function it_can_list_all_the_coupled_permissions_both_directly_and_via_ro { $this->testUserRole->givePermissionTo('edit-articles'); - $this->setPermissionsTeamId(1); + setPermissionsTeamId(1); $this->testUser->load('permissions'); $this->testUser->assignRole('testRole'); $this->testUser->givePermissionTo('edit-news'); - $this->setPermissionsTeamId(2); + setPermissionsTeamId(2); $this->testUser->load('permissions'); $this->testUser->assignRole('testRole'); $this->testUser->givePermissionTo('edit-blog'); - $this->setPermissionsTeamId(1); + setPermissionsTeamId(1); $this->testUser->load('roles'); $this->testUser->load('permissions'); @@ -61,7 +61,7 @@ public function it_can_list_all_the_coupled_permissions_both_directly_and_via_ro $this->testUser->getAllPermissions()->pluck('name')->sort()->values() ); - $this->setPermissionsTeamId(2); + setPermissionsTeamId(2); $this->testUser->load('roles'); $this->testUser->load('permissions'); @@ -74,15 +74,15 @@ public function it_can_list_all_the_coupled_permissions_both_directly_and_via_ro /** @test */ public function it_can_sync_or_remove_permission_without_detach_on_different_teams() { - $this->setPermissionsTeamId(1); + setPermissionsTeamId(1); $this->testUser->load('permissions'); $this->testUser->syncPermissions('edit-articles', 'edit-news'); - $this->setPermissionsTeamId(2); + setPermissionsTeamId(2); $this->testUser->load('permissions'); $this->testUser->syncPermissions('edit-articles', 'edit-blog'); - $this->setPermissionsTeamId(1); + setPermissionsTeamId(1); $this->testUser->load('permissions'); $this->assertEquals( @@ -96,11 +96,40 @@ public function it_can_sync_or_remove_permission_without_detach_on_different_tea $this->testUser->getPermissionNames()->sort()->values() ); - $this->setPermissionsTeamId(2); + setPermissionsTeamId(2); $this->testUser->load('permissions'); $this->assertEquals( collect(['edit-articles', 'edit-blog']), $this->testUser->getPermissionNames()->sort()->values() ); } + + /** @test */ + public function it_can_scope_users_on_different_teams() + { + $user1 = User::create(['email' => 'user1@test.com']); + $user2 = User::create(['email' => 'user2@test.com']); + + setPermissionsTeamId(2); + $user1->givePermissionTo(['edit-articles', 'edit-news']); + $this->testUserRole->givePermissionTo('edit-articles'); + $user2->assignRole('testRole'); + + setPermissionsTeamId(1); + $user1->givePermissionTo(['edit-articles']); + + setPermissionsTeamId(2); + $scopedUsers1Team2 = User::permission(['edit-articles', 'edit-news'])->get(); + $scopedUsers2Team2 = User::permission('edit-news')->get(); + + $this->assertEquals(2, $scopedUsers1Team2->count()); + $this->assertEquals(1, $scopedUsers2Team2->count()); + + setPermissionsTeamId(1); + $scopedUsers1Team1 = User::permission(['edit-articles', 'edit-news'])->get(); + $scopedUsers2Team1 = User::permission('edit-news')->get(); + + $this->assertEquals(1, $scopedUsers1Team1->count()); + $this->assertEquals(0, $scopedUsers2Team1->count()); + } } diff --git a/tests/TeamHasRolesTest.php b/tests/TeamHasRolesTest.php index b07614d8e..05393194a 100644 --- a/tests/TeamHasRolesTest.php +++ b/tests/TeamHasRolesTest.php @@ -22,15 +22,15 @@ public function it_can_assign_same_and_different_roles_on_same_user_different_te $this->assertNotNull($testRole3Team1); $this->assertNotNull($testRole4NoTeam); - $this->setPermissionsTeamId(1); + setPermissionsTeamId(1); $this->testUser->load('roles'); $this->testUser->assignRole('testRole', 'testRole2'); - $this->setPermissionsTeamId(2); + setPermissionsTeamId(2); $this->testUser->load('roles'); $this->testUser->assignRole('testRole', 'testRole3'); - $this->setPermissionsTeamId(1); + setPermissionsTeamId(1); $this->testUser->load('roles'); $this->assertEquals( @@ -44,7 +44,7 @@ public function it_can_assign_same_and_different_roles_on_same_user_different_te $this->assertTrue($this->testUser->hasRole($testRole3Team1)); //testRole3 team=1 $this->assertTrue($this->testUser->hasRole($testRole4NoTeam)); // global role team=null - $this->setPermissionsTeamId(2); + setPermissionsTeamId(2); $this->testUser->load('roles'); $this->assertEquals( @@ -63,15 +63,15 @@ public function it_can_sync_or_remove_roles_without_detach_on_different_teams() { app(Role::class)->create(['name' => 'testRole3', 'team_test_id' => 2]); - $this->setPermissionsTeamId(1); + setPermissionsTeamId(1); $this->testUser->load('roles'); $this->testUser->syncRoles('testRole', 'testRole2'); - $this->setPermissionsTeamId(2); + setPermissionsTeamId(2); $this->testUser->load('roles'); $this->testUser->syncRoles('testRole', 'testRole3'); - $this->setPermissionsTeamId(1); + setPermissionsTeamId(1); $this->testUser->load('roles'); $this->assertEquals( @@ -85,7 +85,7 @@ public function it_can_sync_or_remove_roles_without_detach_on_different_teams() $this->testUser->getRoleNames()->sort()->values() ); - $this->setPermissionsTeamId(2); + setPermissionsTeamId(2); $this->testUser->load('roles'); $this->assertEquals( @@ -93,4 +93,32 @@ public function it_can_sync_or_remove_roles_without_detach_on_different_teams() $this->testUser->getRoleNames()->sort()->values() ); } + + /** @test */ + public function it_can_scope_users_on_different_teams() + { + $user1 = User::create(['email' => 'user1@test.com']); + $user2 = User::create(['email' => 'user2@test.com']); + + setPermissionsTeamId(2); + $user1->assignRole($this->testUserRole); + $user2->assignRole('testRole2'); + + setPermissionsTeamId(1); + $user1->assignRole('testRole'); + + setPermissionsTeamId(2); + $scopedUsers1Team1 = User::role($this->testUserRole)->get(); + $scopedUsers2Team1 = User::role(['testRole', 'testRole2'])->get(); + + $this->assertEquals(1, $scopedUsers1Team1->count()); + $this->assertEquals(2, $scopedUsers2Team1->count()); + + setPermissionsTeamId(1); + $scopedUsers1Team2 = User::role($this->testUserRole)->get(); + $scopedUsers2Team2 = User::role('testRole2')->get(); + + $this->assertEquals(1, $scopedUsers1Team2->count()); + $this->assertEquals(0, $scopedUsers2Team2->count()); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 72c527df8..80a0c3c0b 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -46,17 +46,9 @@ public function setUp(): void // Note: this also flushes the cache from within the migration $this->setUpDatabase($this->app); if ($this->hasTeams) { - $this->setPermissionsTeamId(1); + setPermissionsTeamId(1); } - $this->testUser = User::first(); - $this->testUserRole = app(Role::class)->find(1); - $this->testUserPermission = app(Permission::class)->find(1); - - $this->testAdmin = Admin::first(); - $this->testAdminRole = app(Role::class)->find(3); - $this->testAdminPermission = app(Permission::class)->find(4); - $this->setUpRoutes(); } @@ -137,34 +129,23 @@ protected function setUpDatabase($app) (new \CreatePermissionTables())->up(); - User::create(['email' => 'test@user.com']); - Admin::create(['email' => 'admin@user.com']); - $app[Role::class]->create(['name' => 'testRole']); + $this->testUser = User::create(['email' => 'test@user.com']); + $this->testAdmin = Admin::create(['email' => 'admin@user.com']); + $this->testUserRole = $app[Role::class]->create(['name' => 'testRole']); $app[Role::class]->create(['name' => 'testRole2']); - $app[Role::class]->create(['name' => 'testAdminRole', 'guard_name' => 'admin']); - $app[Permission::class]->create(['name' => 'edit-articles']); + $this->testAdminRole = $app[Role::class]->create(['name' => 'testAdminRole', 'guard_name' => 'admin']); + $this->testUserPermission = $app[Permission::class]->create(['name' => 'edit-articles']); $app[Permission::class]->create(['name' => 'edit-news']); $app[Permission::class]->create(['name' => 'edit-blog']); - $app[Permission::class]->create(['name' => 'admin-permission', 'guard_name' => 'admin']); + $this->testAdminPermission = $app[Permission::class]->create(['name' => 'admin-permission', 'guard_name' => 'admin']); $app[Permission::class]->create(['name' => 'Edit News']); } - /** - * Reload the permissions. - */ protected function reloadPermissions() { app(PermissionRegistrar::class)->forgetCachedPermissions(); } - /** - * Change the team_id - */ - protected function setPermissionsTeamId(int $id) - { - app(PermissionRegistrar::class)->setPermissionsTeamId($id); - } - public function createCacheTable() { Schema::create('cache', function ($table) { From 10c7b8657e566bb8c01ec77c8cf1f4f192b073dc Mon Sep 17 00:00:00 2001 From: angeljqv <79208641+angeljqv@users.noreply.github.com> Date: Mon, 3 Jan 2022 06:27:49 -0500 Subject: [PATCH 0491/1013] Use global helpers (#1963) --- src/Commands/CreateRole.php | 6 +++--- src/Models/Role.php | 17 ++++++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Commands/CreateRole.php b/src/Commands/CreateRole.php index fdd76ad10..e4701f014 100644 --- a/src/Commands/CreateRole.php +++ b/src/Commands/CreateRole.php @@ -21,8 +21,8 @@ public function handle() { $roleClass = app(RoleContract::class); - $teamIdAux = app(PermissionRegistrar::class)->getPermissionsTeamId(); - app(PermissionRegistrar::class)->setPermissionsTeamId($this->option('team-id') ?: null); + $teamIdAux = getPermissionsTeamId(); + setPermissionsTeamId($this->option('team-id') ?: null); if (! PermissionRegistrar::$teams && $this->option('team-id')) { $this->warn("Teams feature disabled, argument --team-id has no effect. Either enable it in permissions config file or remove --team-id parameter"); @@ -31,7 +31,7 @@ public function handle() } $role = $roleClass::findOrCreate($this->argument('name'), $this->argument('guard')); - app(PermissionRegistrar::class)->setPermissionsTeamId($teamIdAux); + setPermissionsTeamId($teamIdAux); $teams_key = PermissionRegistrar::$teamsKey; if (PermissionRegistrar::$teams && $this->option('team-id') && is_null($role->$teams_key)) { diff --git a/src/Models/Role.php b/src/Models/Role.php index 2c6fcfd25..b1da140b7 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -43,7 +43,7 @@ public static function create(array $attributes = []) if (array_key_exists(PermissionRegistrar::$teamsKey, $attributes)) { $params[PermissionRegistrar::$teamsKey] = $attributes[PermissionRegistrar::$teamsKey]; } else { - $attributes[PermissionRegistrar::$teamsKey] = app(PermissionRegistrar::class)->getPermissionsTeamId(); + $attributes[PermissionRegistrar::$teamsKey] = getPermissionsTeamId(); } } if (static::findByParam($params)) { @@ -131,7 +131,7 @@ public static function findOrCreate(string $name, $guardName = null): RoleContra $role = static::findByParam(['name' => $name, 'guard_name' => $guardName]); if (! $role) { - return static::query()->create(['name' => $name, 'guard_name' => $guardName] + (PermissionRegistrar::$teams ? [PermissionRegistrar::$teamsKey => app(PermissionRegistrar::class)->getPermissionsTeamId()] : [])); + return static::query()->create(['name' => $name, 'guard_name' => $guardName] + (PermissionRegistrar::$teams ? [PermissionRegistrar::$teamsKey => getPermissionsTeamId()] : [])); } return $role; @@ -139,13 +139,16 @@ public static function findOrCreate(string $name, $guardName = null): RoleContra protected static function findByParam(array $params = []) { - $query = static::when(PermissionRegistrar::$teams, function ($q) use ($params) { - $q->where(function ($q) use ($params) { + $query = static::query(); + + if (PermissionRegistrar::$teams) { + $query->where(function ($q) use ($params) { $q->whereNull(PermissionRegistrar::$teamsKey) - ->orWhere(PermissionRegistrar::$teamsKey, $params[PermissionRegistrar::$teamsKey] ?? app(PermissionRegistrar::class)->getPermissionsTeamId()); + ->orWhere(PermissionRegistrar::$teamsKey, $params[PermissionRegistrar::$teamsKey] ?? getPermissionsTeamId()); }); - }); - unset($params[PermissionRegistrar::$teamsKey]); + unset($params[PermissionRegistrar::$teamsKey]); + } + foreach ($params as $key => $value) { $query->where($key, $value); } From 2017a9853320e2a235a8317ee20a161f1dc46616 Mon Sep 17 00:00:00 2001 From: angeljqv <79208641+angeljqv@users.noreply.github.com> Date: Mon, 3 Jan 2022 06:28:14 -0500 Subject: [PATCH 0492/1013] Replace is_array with Arr::wrap (#1962) --- src/PermissionServiceProvider.php | 13 +++---------- src/Traits/HasPermissions.php | 5 ++--- src/Traits/HasRoles.php | 7 ++----- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index c185022e6..db7bb6c9e 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -4,6 +4,7 @@ use Illuminate\Filesystem\Filesystem; use Illuminate\Routing\Route; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\ServiceProvider; use Illuminate\View\Compilers\BladeCompiler; @@ -152,11 +153,7 @@ protected function registerMacroHelpers() } Route::macro('role', function ($roles = []) { - if (! is_array($roles)) { - $roles = [$roles]; - } - - $roles = implode('|', $roles); + $roles = implode('|', Arr::wrap($roles)); $this->middleware("role:$roles"); @@ -164,11 +161,7 @@ protected function registerMacroHelpers() }); Route::macro('permission', function ($permissions = []) { - if (! is_array($permissions)) { - $permissions = [$permissions]; - } - - $permissions = implode('|', $permissions); + $permissions = implode('|', Arr::wrap($permissions)); $this->middleware("permission:$permissions"); diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 00e31ce74..ecf07414f 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Contracts\Role; @@ -103,8 +104,6 @@ protected function convertToPermissionModels($permissions): array $permissions = $permissions->all(); } - $permissions = is_array($permissions) ? $permissions : [$permissions]; - return array_map(function ($permission) { if ($permission instanceof Permission) { return $permission; @@ -112,7 +111,7 @@ protected function convertToPermissionModels($permissions): array $method = is_string($permission) ? 'findByName' : 'findById'; return $this->getPermissionClass()->{$method}($permission, $this->getDefaultGuardName()); - }, $permissions); + }, Arr::wrap($permissions)); } /** diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 926381850..f70f1daff 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Contracts\Role; @@ -75,10 +76,6 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder $roles = $roles->all(); } - if (! is_array($roles)) { - $roles = [$roles]; - } - $roles = array_map(function ($role) use ($guard) { if ($role instanceof Role) { return $role; @@ -87,7 +84,7 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder $method = is_numeric($role) ? 'findById' : 'findByName'; return $this->getRoleClass()->{$method}($role, $guard ?: $this->getDefaultGuardName()); - }, $roles); + }, Arr::wrap($roles)); return $query->whereHas('roles', function (Builder $subQuery) use ($roles) { $roleClass = $this->getRoleClass(); From 3aab62f59fa23b8f88ff464fe46bf939aa976ae2 Mon Sep 17 00:00:00 2001 From: angeljqv <79208641+angeljqv@users.noreply.github.com> Date: Mon, 3 Jan 2022 06:28:35 -0500 Subject: [PATCH 0493/1013] Add CacheRepository getter (#1946) --- src/PermissionRegistrar.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 6d228440b..a878bcb34 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -5,6 +5,8 @@ use Illuminate\Cache\CacheManager; use Illuminate\Contracts\Auth\Access\Authorizable; use Illuminate\Contracts\Auth\Access\Gate; +use Illuminate\Contracts\Cache\Repository; +use Illuminate\Contracts\Cache\Store; use Illuminate\Database\Eloquent\Collection; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Contracts\Role; @@ -79,7 +81,7 @@ public function initializeCache() $this->cache = $this->getCacheStoreFromConfig(); } - protected function getCacheStoreFromConfig(): \Illuminate\Contracts\Cache\Repository + protected function getCacheStoreFromConfig(): Repository { // the 'default' fallback here is from the permission.php config file, // where 'default' means to use config(cache.default) @@ -265,12 +267,12 @@ public function setRoleClass($roleClass) return $this; } - /** - * Get the instance of the Cache Store. - * - * @return \Illuminate\Contracts\Cache\Store - */ - public function getCacheStore(): \Illuminate\Contracts\Cache\Store + public function getCacheRepository(): Repository + { + return $this->cache; + } + + public function getCacheStore(): Store { return $this->cache->getStore(); } From 6f4764c0cafe1cf508b7b9801a81b2caa858150a Mon Sep 17 00:00:00 2001 From: freekmurze Date: Mon, 3 Jan 2022 11:29:06 +0000 Subject: [PATCH 0494/1013] Fix styling --- src/Models/Role.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Models/Role.php b/src/Models/Role.php index b1da140b7..58ff8c8a7 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -148,7 +148,7 @@ protected static function findByParam(array $params = []) }); unset($params[PermissionRegistrar::$teamsKey]); } - + foreach ($params as $key => $value) { $query->where($key, $value); } From 45a4af9bf7754eeb692f07041091229c3ea92234 Mon Sep 17 00:00:00 2001 From: freek Date: Tue, 11 Jan 2022 16:03:06 +0100 Subject: [PATCH 0495/1013] wip --- .github/workflows/run-tests-L8.yml | 9 ++++++++- composer.json | 10 +++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/run-tests-L8.yml b/.github/workflows/run-tests-L8.yml index 14b15a76d..5843a98c9 100644 --- a/.github/workflows/run-tests-L8.yml +++ b/.github/workflows/run-tests-L8.yml @@ -10,11 +10,18 @@ jobs: fail-fast: false matrix: php: [8.1, 8.0, 7.4, 7.3] - laravel: [8.*] + laravel: [9.*, 8.*] dependency-version: [prefer-lowest, prefer-stable] include: + - laravel: 9.* + testbench: 7.* - laravel: 8.* testbench: 6.23 + exclude: + - laravel: 9.* + php: 7.4 + - laravel: 9.* + php: 7.3 name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} diff --git a/composer.json b/composer.json index 7382f27d7..c1a09b24d 100644 --- a/composer.json +++ b/composer.json @@ -23,13 +23,13 @@ ], "require": { "php" : "^7.3|^8.0|^8.1", - "illuminate/auth": "^7.0|^8.0", - "illuminate/container": "^7.0|^8.0", - "illuminate/contracts": "^7.0|^8.0", - "illuminate/database": "^7.0|^8.0" + "illuminate/auth": "^7.0|^8.0|^9.0", + "illuminate/container": "^7.0|^8.0|^9.0", + "illuminate/contracts": "^7.0|^8.0|^9.0", + "illuminate/database": "^7.0|^8.0|^9.0" }, "require-dev": { - "orchestra/testbench": "^5.0|^6.0", + "orchestra/testbench": "^5.0|^6.0|^7.0", "phpunit/phpunit": "^9.4", "predis/predis": "^1.1" }, From 6a3ed627cee28a552b5176c172ae0abc5eb30925 Mon Sep 17 00:00:00 2001 From: freek Date: Tue, 11 Jan 2022 16:06:21 +0100 Subject: [PATCH 0496/1013] wip --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 980ea5ecd..e164c37a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.5.0 - 2021-01-11 + +- add support for Laravel 9 + ## 5.4.0 - 2021-11-17 ## What's Changed From 54acbaea6876a13169244c1b122c1c8aa17fd656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Gerg=C5=91?= Date: Tue, 1 Mar 2022 20:26:37 +0100 Subject: [PATCH 0497/1013] Spelling correction (#2024) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e164c37a3..7a8680965 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to `laravel-permission` will be documented in this file -## 5.5.0 - 2021-01-11 +## 5.5.0 - 2022-01-11 - add support for Laravel 9 From 20dd894eb64e5b02de9dfa28aa13af0643b2e456 Mon Sep 17 00:00:00 2001 From: Samson Adesanoye Date: Tue, 1 Mar 2022 20:26:55 +0100 Subject: [PATCH 0498/1013] update broken link to laravel exception (#2023) --- docs/advanced-usage/exceptions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-usage/exceptions.md b/docs/advanced-usage/exceptions.md index b71939a3d..6134ed59f 100644 --- a/docs/advanced-usage/exceptions.md +++ b/docs/advanced-usage/exceptions.md @@ -7,7 +7,7 @@ If you need to override exceptions thrown by this package, you can simply use no An example is shown below for your convenience, but nothing here is specific to this package other than the name of the exception. -You can find all the exceptions added by this package in the code here: https://github.com/spatie/laravel-permission/tree/main/src/Exceptions +You can find all the exceptions added by this package in the code here: [https://github.com/spatie/laravel-permission/tree/main/src/Exceptions](https://github.com/spatie/laravel-permission/tree/main/src/Exceptions) **app/Exceptions/Handler.php** From 6c46b4e7dc5445ebb36a44b00ee08d0838f580b0 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Thu, 3 Mar 2022 12:57:16 -0500 Subject: [PATCH 0499/1013] Fix Blade Directives incompatibility with renderers (#2039) * Fix Blade Directives incompatibility with renderers * Fix styling Co-authored-by: erikn69 --- src/PermissionServiceProvider.php | 119 +++++++++++++++--------------- 1 file changed, 59 insertions(+), 60 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index db7bb6c9e..924d576a4 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -7,7 +7,6 @@ use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\ServiceProvider; -use Illuminate\View\Compilers\BladeCompiler; use Spatie\Permission\Contracts\Permission as PermissionContract; use Spatie\Permission\Contracts\Role as RoleContract; @@ -84,65 +83,65 @@ protected function registerModelBindings() protected function registerBladeExtensions() { - $this->app->afterResolving('blade.compiler', function (BladeCompiler $bladeCompiler) { - $bladeCompiler->directive('role', function ($arguments) { - list($role, $guard) = explode(',', $arguments.','); - - return "check() && auth({$guard})->user()->hasRole({$role})): ?>"; - }); - $bladeCompiler->directive('elserole', function ($arguments) { - list($role, $guard) = explode(',', $arguments.','); - - return "check() && auth({$guard})->user()->hasRole({$role})): ?>"; - }); - $bladeCompiler->directive('endrole', function () { - return ''; - }); - - $bladeCompiler->directive('hasrole', function ($arguments) { - list($role, $guard) = explode(',', $arguments.','); - - return "check() && auth({$guard})->user()->hasRole({$role})): ?>"; - }); - $bladeCompiler->directive('endhasrole', function () { - return ''; - }); - - $bladeCompiler->directive('hasanyrole', function ($arguments) { - list($roles, $guard) = explode(',', $arguments.','); - - return "check() && auth({$guard})->user()->hasAnyRole({$roles})): ?>"; - }); - $bladeCompiler->directive('endhasanyrole', function () { - return ''; - }); - - $bladeCompiler->directive('hasallroles', function ($arguments) { - list($roles, $guard) = explode(',', $arguments.','); - - return "check() && auth({$guard})->user()->hasAllRoles({$roles})): ?>"; - }); - $bladeCompiler->directive('endhasallroles', function () { - return ''; - }); - - $bladeCompiler->directive('unlessrole', function ($arguments) { - list($role, $guard) = explode(',', $arguments.','); - - return "check() || ! auth({$guard})->user()->hasRole({$role})): ?>"; - }); - $bladeCompiler->directive('endunlessrole', function () { - return ''; - }); - - $bladeCompiler->directive('hasexactroles', function ($arguments) { - list($roles, $guard) = explode(',', $arguments.','); - - return "check() && auth({$guard})->user()->hasExactRoles({$roles})): ?>"; - }); - $bladeCompiler->directive('endhasexactroles', function () { - return ''; - }); + $bladeCompiler = $this->app['blade.compiler']; + + $bladeCompiler->directive('role', function ($arguments) { + list($role, $guard) = explode(',', $arguments.','); + + return "check() && auth({$guard})->user()->hasRole({$role})): ?>"; + }); + $bladeCompiler->directive('elserole', function ($arguments) { + list($role, $guard) = explode(',', $arguments.','); + + return "check() && auth({$guard})->user()->hasRole({$role})): ?>"; + }); + $bladeCompiler->directive('endrole', function () { + return ''; + }); + + $bladeCompiler->directive('hasrole', function ($arguments) { + list($role, $guard) = explode(',', $arguments.','); + + return "check() && auth({$guard})->user()->hasRole({$role})): ?>"; + }); + $bladeCompiler->directive('endhasrole', function () { + return ''; + }); + + $bladeCompiler->directive('hasanyrole', function ($arguments) { + list($roles, $guard) = explode(',', $arguments.','); + + return "check() && auth({$guard})->user()->hasAnyRole({$roles})): ?>"; + }); + $bladeCompiler->directive('endhasanyrole', function () { + return ''; + }); + + $bladeCompiler->directive('hasallroles', function ($arguments) { + list($roles, $guard) = explode(',', $arguments.','); + + return "check() && auth({$guard})->user()->hasAllRoles({$roles})): ?>"; + }); + $bladeCompiler->directive('endhasallroles', function () { + return ''; + }); + + $bladeCompiler->directive('unlessrole', function ($arguments) { + list($role, $guard) = explode(',', $arguments.','); + + return "check() || ! auth({$guard})->user()->hasRole({$role})): ?>"; + }); + $bladeCompiler->directive('endunlessrole', function () { + return ''; + }); + + $bladeCompiler->directive('hasexactroles', function ($arguments) { + list($roles, $guard) = explode(',', $arguments.','); + + return "check() && auth({$guard})->user()->hasExactRoles({$roles})): ?>"; + }); + $bladeCompiler->directive('endhasexactroles', function () { + return ''; }); } From 677903c69460a1867da833979a0dfdffd163767d Mon Sep 17 00:00:00 2001 From: freekmurze Date: Thu, 3 Mar 2022 17:58:20 +0000 Subject: [PATCH 0500/1013] Update CHANGELOG --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a8680965..0093dbf08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.5.1 - 2022-03-03 + +## What's Changed + +- Spelling correction by @gergo85 in https://github.com/spatie/laravel-permission/pull/2024 +- update broken link to laravel exception by @kingzamzon in https://github.com/spatie/laravel-permission/pull/2023 +- Fix Blade Directives incompatibility with renderers by @erikn69 in https://github.com/spatie/laravel-permission/pull/2039 + +## New Contributors + +- @gergo85 made their first contribution in https://github.com/spatie/laravel-permission/pull/2024 +- @kingzamzon made their first contribution in https://github.com/spatie/laravel-permission/pull/2023 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.0...5.5.1 + ## 5.5.0 - 2022-01-11 - add support for Laravel 9 @@ -302,6 +317,7 @@ The following changes are not "breaking", but worth making the updates to your a + $this->app->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions(); + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -351,6 +367,7 @@ The following changes are not "breaking", but worth making the updates to your a @endrole + ``` ## 2.19.1 - 2018-09-14 From eb838cd2c539d1c97f8c997e67479cb68c2d0e36 Mon Sep 17 00:00:00 2001 From: Cristian Tabacitu Date: Wed, 9 Mar 2022 12:21:20 +0200 Subject: [PATCH 0501/1013] [Fixes BIG bug] register blade directives after resolving blade compiler (#2048) * register blade directives after resolving blade compiler Fixes #2038, #2044, #2045 * use callAfterResolving instead of afterResolving --- src/PermissionServiceProvider.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 924d576a4..61fc1321b 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -7,6 +7,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\ServiceProvider; +use Illuminate\View\Compilers\BladeCompiler; use Spatie\Permission\Contracts\Permission as PermissionContract; use Spatie\Permission\Contracts\Role as RoleContract; @@ -39,7 +40,9 @@ public function register() 'permission' ); - $this->registerBladeExtensions(); + $this->callAfterResolving('blade.compiler', function (BladeCompiler $bladeCompiler) { + $this->registerBladeExtensions($bladeCompiler); + }); } protected function offerPublishing() @@ -81,10 +84,8 @@ protected function registerModelBindings() $this->app->bind(RoleContract::class, $config['role']); } - protected function registerBladeExtensions() + protected function registerBladeExtensions($bladeCompiler) { - $bladeCompiler = $this->app['blade.compiler']; - $bladeCompiler->directive('role', function ($arguments) { list($role, $guard) = explode(',', $arguments.','); From 377e2308d2099f65207ea12e3b587d761b69c41f Mon Sep 17 00:00:00 2001 From: freekmurze Date: Wed, 9 Mar 2022 10:22:08 +0000 Subject: [PATCH 0502/1013] Update CHANGELOG --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0093dbf08..727ace7b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.5.2 - 2022-03-09 + +## What's Changed + +- [Fixes BIG bug] register blade directives after resolving blade compiler by @tabacitu in https://github.com/spatie/laravel-permission/pull/2048 + +## New Contributors + +- @tabacitu made their first contribution in https://github.com/spatie/laravel-permission/pull/2048 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.1...5.5.2 + ## 5.5.1 - 2022-03-03 ## What's Changed @@ -318,6 +330,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -368,6 +381,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From b731394245a0b88ed255b62bcad34b1fc163b625 Mon Sep 17 00:00:00 2001 From: Adriaan Marain Date: Wed, 9 Mar 2022 17:20:14 +0100 Subject: [PATCH 0503/1013] Add banner --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 141fb33de..b65b58048 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ + +[](https://supportukrainenow.org) +

Social Card of Laravel Permission

# Associate users with permissions and roles From 500afd4ce9770934e0c2e4a8fdfab4a18a56b41e Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Fri, 18 Mar 2022 16:40:42 +0100 Subject: [PATCH 0504/1013] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b65b58048..c03f87ec9 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ Please see [CONTRIBUTING](CONTRIBUTING.md) for details. ### Security -If you discover any security-related issues, please email [freek@spatie.be](mailto:freek@spatie.be) instead of using the issue tracker. +If you discover any security-related issues, please email [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. ## Postcardware From 50c363888fb90c0404e945eab5db262ffcd0ff00 Mon Sep 17 00:00:00 2001 From: Adriaan Marain Date: Mon, 21 Mar 2022 13:33:29 +0100 Subject: [PATCH 0505/1013] Change copy --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c03f87ec9..d090b8b24 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recen ## Contributing -Please see [CONTRIBUTING](CONTRIBUTING.md) for details. +Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. ### Security From 16e37de3cdf963677b50cd9ceec18968f3e6bd21 Mon Sep 17 00:00:00 2001 From: Adriaan Marain Date: Mon, 21 Mar 2022 13:51:34 +0100 Subject: [PATCH 0506/1013] Use organisation-wide community health files --- .github/SECURITY.md | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .github/SECURITY.md diff --git a/.github/SECURITY.md b/.github/SECURITY.md deleted file mode 100644 index ca9134343..000000000 --- a/.github/SECURITY.md +++ /dev/null @@ -1,3 +0,0 @@ -# Security Policy - -If you discover any security related issues, please email freek@spatie.be instead of using the issue tracker. From 15cb00fab6280c0e08955f9ec3f4c736a36f0173 Mon Sep 17 00:00:00 2001 From: Adriaan Marain Date: Mon, 21 Mar 2022 13:54:43 +0100 Subject: [PATCH 0507/1013] Use organisation-wide community health files --- CONTRIBUTING.md | 32 -------------------------------- 1 file changed, 32 deletions(-) delete mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 2ceb084d4..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,32 +0,0 @@ -# Contributing - -Contributions are **welcome** and will be fully **credited**. - -We accept contributions via Pull Requests on [Github](https://github.com/spatie/laravel-permission). - - -## Pull Requests - -- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). - -- **Add tests!** - Your patch won't be accepted if it doesn't have tests. - -- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. - -- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. - -- **Create feature branches** - Don't ask us to pull from your master branch. - -- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. - -- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. - - -## Running Tests - -``` bash -$ phpunit -``` - - -**Happy coding**! From aec8d121ccb9cf2b96bb2792488f90648f9d4d65 Mon Sep 17 00:00:00 2001 From: angeljqv <79208641+angeljqv@users.noreply.github.com> Date: Thu, 5 May 2022 16:18:15 -0500 Subject: [PATCH 0508/1013] Update .gitattributes (#2065) --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitattributes b/.gitattributes index 5a0aa162e..993e0d23c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -14,3 +14,5 @@ /.editorconfig export-ignore /.php_cs.dist.php export-ignore /.styleci.yml export-ignore +/CHANGELOG.md export-ignore +/CONTRIBUTING.md export-ignore From 77baa1b9024a0ef4acf5a0101b4ce6e5cda9dde9 Mon Sep 17 00:00:00 2001 From: Morgan Arnel <84181964+morganarnel@users.noreply.github.com> Date: Thu, 5 May 2022 22:19:11 +0100 Subject: [PATCH 0509/1013] Update add_teams_fields.php.stub (#2067) Double semicolon in teams_field migration ;) --- database/migrations/add_teams_fields.php.stub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/add_teams_fields.php.stub b/database/migrations/add_teams_fields.php.stub index 6df2378bf..6abdf8d8f 100644 --- a/database/migrations/add_teams_fields.php.stub +++ b/database/migrations/add_teams_fields.php.stub @@ -41,7 +41,7 @@ class AddTeamsFields extends Migration if (! Schema::hasColumn($tableNames['model_has_permissions'], $columnNames['team_foreign_key'])) { Schema::table($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) { - $table->unsignedBigInteger($columnNames['team_foreign_key'])->default('1');; + $table->unsignedBigInteger($columnNames['team_foreign_key'])->default('1'); $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index'); if (DB::getDriverName() !== 'sqlite') { From 96466b07c9a416fcc095a9097e87131e082e60af Mon Sep 17 00:00:00 2001 From: erikn69 Date: Thu, 5 May 2022 16:43:01 -0500 Subject: [PATCH 0510/1013] Allow revokePermissionTo to accept Permission[] (#2014) Co-authored-by: Erik Niebla --- src/Traits/HasPermissions.php | 6 +++++- tests/HasPermissionsTest.php | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index ecf07414f..d116e8ac6 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -384,7 +384,7 @@ public function syncPermissions(...$permissions) } /** - * Revoke the given permission. + * Revoke the given permission(s). * * @param \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Permission[]|string|string[] $permission * @@ -426,6 +426,10 @@ protected function getStoredPermission($permissions) } if (is_array($permissions)) { + $permissions = array_map(function ($permission) use ($permissionClass) { + return is_a($permission, get_class($permissionClass)) ? $permission->name : $permission; + }, $permissions); + return $permissionClass ->whereIn('name', $permissions) ->whereIn('guard_name', $this->getGuardNames()) diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index df793e451..40151b67e 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -240,6 +240,34 @@ public function it_can_give_and_revoke_multiple_permissions() $this->assertEquals(0, $this->testUserRole->permissions()->count()); } + /** @test */ + public function it_can_give_and_revoke_permissions_models_array() + { + $models = [app(Permission::class)::where('name', 'edit-articles')->first(), app(Permission::class)::where('name', 'edit-news')->first()]; + + $this->testUserRole->givePermissionTo($models); + + $this->assertEquals(2, $this->testUserRole->permissions()->count()); + + $this->testUserRole->revokePermissionTo($models); + + $this->assertEquals(0, $this->testUserRole->permissions()->count()); + } + + /** @test */ + public function it_can_give_and_revoke_permissions_models_collection() + { + $models = app(Permission::class)::whereIn('name', ['edit-articles', 'edit-news'])->get(); + + $this->testUserRole->givePermissionTo($models); + + $this->assertEquals(2, $this->testUserRole->permissions()->count()); + + $this->testUserRole->revokePermissionTo($models); + + $this->assertEquals(0, $this->testUserRole->permissions()->count()); + } + /** @test */ public function it_can_determine_that_the_user_does_not_have_a_permission() { From 67c4df75d5692cfa2c91b317e57df9e670625d25 Mon Sep 17 00:00:00 2001 From: Faqih Muntashir <48067039+itsfaqih@users.noreply.github.com> Date: Fri, 6 May 2022 04:45:18 +0700 Subject: [PATCH 0511/1013] Improve typing in role's findById and findOrCreate method (#2022) --- src/Models/Role.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Models/Role.php b/src/Models/Role.php index 58ff8c8a7..76bec3095 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -103,6 +103,14 @@ public static function findByName(string $name, $guardName = null): RoleContract return $role; } + /** + * Find a role by its id (and optionally guardName). + * + * @param int $id + * @param string|null $guardName + * + * @return \Spatie\Permission\Contracts\Role|\Spatie\Permission\Models\Role + */ public static function findById(int $id, $guardName = null): RoleContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); @@ -122,7 +130,7 @@ public static function findById(int $id, $guardName = null): RoleContract * @param string $name * @param string|null $guardName * - * @return \Spatie\Permission\Contracts\Role + * @return \Spatie\Permission\Contracts\Role|\Spatie\Permission\Models\Role */ public static function findOrCreate(string $name, $guardName = null): RoleContract { From 4e3ad0aed6e3a8a4b5df9424aa0a721902bc7841 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Thu, 5 May 2022 16:50:47 -0500 Subject: [PATCH 0512/1013] [V5] Cache loader improvements (#1912) * Cache loader improvements * Cache even smaller Co-authored-by: Erik Niebla --- src/PermissionRegistrar.php | 115 ++++++++++++++++++++++++++---------- 1 file changed, 83 insertions(+), 32 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index a878bcb34..ce10bafa9 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -170,32 +170,22 @@ private function loadPermissions() } $this->permissions = $this->cache->remember(self::$cacheKey, self::$cacheExpirationTime, function () { - // make the cache smaller using an array with only required fields - return $this->getPermissionClass()->select('id', 'id as i', 'name as n', 'guard_name as g') - ->with('roles:id,id as i,name as n,guard_name as g')->get() - ->map(function ($permission) { - return $permission->only('i', 'n', 'g') + - ['r' => $permission->roles->map->only('i', 'n', 'g')->all()]; - })->all(); + return $this->getSerializedPermissionsForCache(); }); - if (is_array($this->permissions)) { - $this->permissions = $this->getPermissionClass()::hydrate( - collect($this->permissions)->map(function ($item) { - return ['id' => $item['i'] ?? $item['id'], 'name' => $item['n'] ?? $item['name'], 'guard_name' => $item['g'] ?? $item['guard_name']]; - })->all() - ) - ->each(function ($permission, $i) { - $roles = Collection::make($this->permissions[$i]['r'] ?? $this->permissions[$i]['roles'] ?? []) - ->map(function ($item) { - return $this->getHydratedRole($item); - }); - - $permission->setRelation('roles', $roles); - }); - - $this->cachedRoles = []; + // fallback for old cache method, must be removed on next mayor version + if (! isset($this->permissions['permissions'])) { + $this->forgetCachedPermissions(); + $this->loadPermissions(); + + return; } + + $this->hydrateRolesCache(); + + $this->permissions = $this->getHydratedPermissionCollection(); + + $this->cachedRoles = []; } /** @@ -277,21 +267,82 @@ public function getCacheStore(): Store return $this->cache->getStore(); } - private function getHydratedRole(array $item) + /* + * Make the cache smaller using an array with only required fields + */ + private function getSerializedPermissionsForCache() { - $roleId = $item['i'] ?? $item['id']; + $roleClass = $this->getRoleClass(); + $roleKey = (new $roleClass())->getKeyName(); + + $permissionClass = $this->getPermissionClass(); + $permissionKey = (new $permissionClass())->getKeyName(); + + $permissions = $permissionClass + ->select($permissionKey, "$permissionKey as i", 'name as n', 'guard_name as g') + ->with("roles:$roleKey,$roleKey as i,name as n,guard_name as g")->get() + ->map(function ($permission) { + return $permission->only('i', 'n', 'g') + $this->getSerializedRoleRelation($permission); + })->all(); + $roles = array_values($this->cachedRoles); + $this->cachedRoles = []; - if (isset($this->cachedRoles[$roleId])) { - return $this->cachedRoles[$roleId]; + return compact('permissions', 'roles'); + } + + private function getSerializedRoleRelation($permission) + { + if (! $permission->roles->count()) { + return []; } + return [ + 'r' => $permission->roles->map(function ($role) { + if (! isset($this->cachedRoles[$role->i])) { + $this->cachedRoles[$role->i] = $role->only('i', 'n', 'g'); + } + + return $role->i; + })->all(), + ]; + } + + private function getHydratedPermissionCollection() + { + $permissionClass = $this->getPermissionClass(); + $permissionInstance = new $permissionClass(); + + return Collection::make( + array_map(function ($item) use ($permissionInstance) { + return $permissionInstance + ->newFromBuilder([ + $permissionInstance->getKeyName() => $item['i'], + 'name' => $item['n'], + 'guard_name' => $item['g'], + ]) + ->setRelation('roles', $this->getHydratedRoleCollection($item['r'] ?? [])); + }, $this->permissions['permissions']) + ); + } + + private function getHydratedRoleCollection(array $roles) + { + return Collection::make(array_values( + array_intersect_key($this->cachedRoles, array_flip($roles)) + )); + } + + private function hydrateRolesCache() + { $roleClass = $this->getRoleClass(); $roleInstance = new $roleClass(); - return $this->cachedRoles[$roleId] = $roleInstance->newFromBuilder([ - 'id' => $roleId, - 'name' => $item['n'] ?? $item['name'], - 'guard_name' => $item['g'] ?? $item['guard_name'], - ]); + array_map(function ($item) use ($roleInstance) { + $this->cachedRoles[$item['i']] = $roleInstance->newFromBuilder([ + $roleInstance->getKeyName() => $item['i'], + 'name' => $item['n'], + 'guard_name' => $item['g'], + ]); + }, $this->permissions['roles']); } } From b71df07f3b5db4886657b4c17244d66ba6796c1a Mon Sep 17 00:00:00 2001 From: freekmurze Date: Thu, 5 May 2022 21:55:54 +0000 Subject: [PATCH 0513/1013] Update CHANGELOG --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 727ace7b1..dcb503e1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.5.3 - 2022-05-05 + +## What's Changed + +- Update .gitattributes by @angeljqv in https://github.com/spatie/laravel-permission/pull/2065 +- Remove double semicolon from add_teams_fields.php.stub by @morganarnel in https://github.com/spatie/laravel-permission/pull/2067 +- [V5] Allow revokePermissionTo to accept Permission[] by @erikn69 in https://github.com/spatie/laravel-permission/pull/2014 +- [V5] Improve typing in role's findById and findOrCreate method by @itsfaqih in https://github.com/spatie/laravel-permission/pull/2022 +- [V5] Cache loader improvements by @erikn69 in https://github.com/spatie/laravel-permission/pull/1912 + +## New Contributors + +- @morganarnel made their first contribution in https://github.com/spatie/laravel-permission/pull/2067 +- @itsfaqih made their first contribution in https://github.com/spatie/laravel-permission/pull/2022 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.2...5.5.3 + ## 5.5.2 - 2022-03-09 ## What's Changed @@ -331,6 +348,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -382,6 +400,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From 8236acb68d5596d857b81ced2a3ee5fcf773650a Mon Sep 17 00:00:00 2001 From: erikn69 Date: Mon, 16 May 2022 07:07:35 -0500 Subject: [PATCH 0514/1013] Support custom primary keys on models (#2092) --- src/Models/Permission.php | 2 +- src/Models/Role.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 2b253d6f6..1834d72e2 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -108,7 +108,7 @@ public static function findByName(string $name, $guardName = null): PermissionCo public static function findById(int $id, $guardName = null): PermissionContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); - $permission = static::getPermission(['id' => $id, 'guard_name' => $guardName]); + $permission = static::getPermission([(new static())->getKeyName() => $id, 'guard_name' => $guardName]); if (! $permission) { throw PermissionDoesNotExist::withId($id, $guardName); diff --git a/src/Models/Role.php b/src/Models/Role.php index 76bec3095..da6f269e0 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -115,7 +115,7 @@ public static function findById(int $id, $guardName = null): RoleContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); - $role = static::findByParam(['id' => $id, 'guard_name' => $guardName]); + $role = static::findByParam([(new static())->getKeyName() => $id, 'guard_name' => $guardName]); if (! $role) { throw RoleDoesNotExist::withId($id); From 55f96fbd47f72d55aa92440fb65f5eafa83f7fcd Mon Sep 17 00:00:00 2001 From: Abhishek Paul Date: Mon, 16 May 2022 17:37:56 +0530 Subject: [PATCH 0515/1013] Fix UuidTrait (#2094) Boot method having protected it should be public and no need to call parent boot method --- docs/advanced-usage/uuid.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index bebbeef73..299f83c64 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -96,10 +96,8 @@ It is common to use a trait to handle the $keyType and $incrementing settings, a trait UuidTrait { - protected static function bootUuidTrait() + public static function bootUuidTrait() { - parent::boot(); - static::creating(function ($model) { $model->keyType = 'string'; $model->incrementing = false; From cb86fd87b43fcfc493c3f2b1de6fad100c078146 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Mon, 16 May 2022 07:09:59 -0500 Subject: [PATCH 0516/1013] Support custom fields on cache (#2091) --- src/PermissionRegistrar.php | 84 ++++++++++++++------ tests/HasPermissionsWithCustomModelsTest.php | 24 ++++++ 2 files changed, 83 insertions(+), 25 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index ce10bafa9..38beec605 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -52,6 +52,12 @@ class PermissionRegistrar /** @var array */ private $cachedRoles = []; + /** @var array */ + private $alias = []; + + /** @var array */ + private $except = []; + /** * PermissionRegistrar constructor. * @@ -174,18 +180,20 @@ private function loadPermissions() }); // fallback for old cache method, must be removed on next mayor version - if (! isset($this->permissions['permissions'])) { + if (! isset($this->permissions['alias'])) { $this->forgetCachedPermissions(); $this->loadPermissions(); return; } + $this->alias = $this->permissions['alias']; + $this->hydrateRolesCache(); $this->permissions = $this->getHydratedPermissionCollection(); - $this->cachedRoles = []; + $this->cachedRoles = $this->alias = $this->except = []; } /** @@ -267,27 +275,55 @@ public function getCacheStore(): Store return $this->cache->getStore(); } + /** + * Changes array keys with alias + * + * @return array + */ + private function aliasedArray($model): array + { + return collect(is_array($model) ? $model : $model->getAttributes())->except($this->except) + ->keyBy(function ($value, $key) { + return $this->alias[$key] ?? $key; + })->all(); + } + + /** + * Array for cache alias + */ + private function aliasModelFields($newKeys = []): void + { + $i = 0; + $alphas = ! count($this->alias) ? range('a', 'h') : range('j', 'p'); + + foreach (array_keys($newKeys->getAttributes()) as $value) { + if (! isset($this->alias[$value])) { + $this->alias[$value] = $alphas[$i++] ?? $value; + } + } + + $this->alias = array_diff_key($this->alias, array_flip($this->except)); + } + /* * Make the cache smaller using an array with only required fields */ private function getSerializedPermissionsForCache() { - $roleClass = $this->getRoleClass(); - $roleKey = (new $roleClass())->getKeyName(); - - $permissionClass = $this->getPermissionClass(); - $permissionKey = (new $permissionClass())->getKeyName(); + $this->except = config('permission.cache.column_names_except', ['created_at','updated_at', 'deleted_at']); - $permissions = $permissionClass - ->select($permissionKey, "$permissionKey as i", 'name as n', 'guard_name as g') - ->with("roles:$roleKey,$roleKey as i,name as n,guard_name as g")->get() + $permissions = $this->getPermissionClass()->select()->with('roles')->get() ->map(function ($permission) { - return $permission->only('i', 'n', 'g') + $this->getSerializedRoleRelation($permission); + if (! $this->alias) { + $this->aliasModelFields($permission); + } + + return $this->aliasedArray($permission) + $this->getSerializedRoleRelation($permission); })->all(); $roles = array_values($this->cachedRoles); $this->cachedRoles = []; - return compact('permissions', 'roles'); + return ['alias' => array_flip($this->alias)] + compact('permissions', 'roles'); } private function getSerializedRoleRelation($permission) @@ -296,13 +332,18 @@ private function getSerializedRoleRelation($permission) return []; } + if (! isset($this->alias['roles'])) { + $this->alias['roles'] = 'r'; + $this->aliasModelFields($permission->roles[0]); + } + return [ 'r' => $permission->roles->map(function ($role) { - if (! isset($this->cachedRoles[$role->i])) { - $this->cachedRoles[$role->i] = $role->only('i', 'n', 'g'); + if (! isset($this->cachedRoles[$role->getKey()])) { + $this->cachedRoles[$role->getKey()] = $this->aliasedArray($role); } - return $role->i; + return $role->getKey(); })->all(), ]; } @@ -315,11 +356,7 @@ private function getHydratedPermissionCollection() return Collection::make( array_map(function ($item) use ($permissionInstance) { return $permissionInstance - ->newFromBuilder([ - $permissionInstance->getKeyName() => $item['i'], - 'name' => $item['n'], - 'guard_name' => $item['g'], - ]) + ->newFromBuilder($this->aliasedArray(array_diff_key($item, ['r' => 0]))) ->setRelation('roles', $this->getHydratedRoleCollection($item['r'] ?? [])); }, $this->permissions['permissions']) ); @@ -338,11 +375,8 @@ private function hydrateRolesCache() $roleInstance = new $roleClass(); array_map(function ($item) use ($roleInstance) { - $this->cachedRoles[$item['i']] = $roleInstance->newFromBuilder([ - $roleInstance->getKeyName() => $item['i'], - 'name' => $item['n'], - 'guard_name' => $item['g'], - ]); + $role = $roleInstance->newFromBuilder($this->aliasedArray($item)); + $this->cachedRoles[$role->getKey()] = $role; }, $this->permissions['roles']); } } diff --git a/tests/HasPermissionsWithCustomModelsTest.php b/tests/HasPermissionsWithCustomModelsTest.php index bdb237a5c..6b9fa34ca 100644 --- a/tests/HasPermissionsWithCustomModelsTest.php +++ b/tests/HasPermissionsWithCustomModelsTest.php @@ -2,6 +2,9 @@ namespace Spatie\Permission\Test; +use DB; +use Spatie\Permission\PermissionRegistrar; + class HasPermissionsWithCustomModelsTest extends HasPermissionsTest { /** @var bool */ @@ -12,4 +15,25 @@ public function it_can_use_custom_model_permission() { $this->assertSame(get_class($this->testUserPermission), Permission::class); } + + /** @test */ + public function it_can_use_custom_fields_from_cache() + { + DB::connection()->getSchemaBuilder()->table(config('permission.table_names.roles'), function ($table) { + $table->string('type')->default('R'); + }); + DB::connection()->getSchemaBuilder()->table(config('permission.table_names.permissions'), function ($table) { + $table->string('type')->default('P'); + }); + + $this->testUserRole->givePermissionTo($this->testUserPermission); + app(PermissionRegistrar::class)->getPermissions(); + + DB::enableQueryLog(); + $this->assertSame('P', Permission::findByName('edit-articles')->type); + $this->assertSame('R', Permission::findByName('edit-articles')->roles[0]->type); + DB::disableQueryLog(); + + $this->assertSame(0, count(DB::getQueryLog())); + } } From 8a69aaddbf91810b0175b05a1acb9b3410bff68e Mon Sep 17 00:00:00 2001 From: freekmurze Date: Mon, 16 May 2022 12:11:28 +0000 Subject: [PATCH 0517/1013] Update CHANGELOG --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcb503e1f..755ea17f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.5.4 - 2022-05-16 + +## What's Changed + +- Support custom primary key names on models by @erikn69 in https://github.com/spatie/laravel-permission/pull/2092 +- Fix UuidTrait on uuid doc page by @abhishekpaul in https://github.com/spatie/laravel-permission/pull/2094 +- Support custom fields on cache by @erikn69 in https://github.com/spatie/laravel-permission/pull/2091 + +## New Contributors + +- @abhishekpaul made their first contribution in https://github.com/spatie/laravel-permission/pull/2094 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.3...5.5.4 + ## 5.5.3 - 2022-05-05 ## What's Changed @@ -349,6 +363,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -401,6 +416,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From 411eecaef445aa37ae40d58468671ff8e9c45b56 Mon Sep 17 00:00:00 2001 From: Sergei Zhidkov Date: Tue, 24 May 2022 08:41:19 +0400 Subject: [PATCH 0518/1013] config() in Role and Permission models is deleted --- src/Models/Permission.php | 2 -- src/Models/Role.php | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 1834d72e2..9ef30ed17 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -22,8 +22,6 @@ class Permission extends Model implements PermissionContract public function __construct(array $attributes = []) { - $attributes['guard_name'] = $attributes['guard_name'] ?? config('auth.defaults.guard'); - parent::__construct($attributes); $this->guarded[] = $this->primaryKey; diff --git a/src/Models/Role.php b/src/Models/Role.php index da6f269e0..99d6479af 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -22,8 +22,6 @@ class Role extends Model implements RoleContract public function __construct(array $attributes = []) { - $attributes['guard_name'] = $attributes['guard_name'] ?? config('auth.defaults.guard'); - parent::__construct($attributes); $this->guarded[] = $this->primaryKey; From fc841f86190f558658c072c2f2107fdf67308c8f Mon Sep 17 00:00:00 2001 From: erikn69 Date: Mon, 30 May 2022 09:48:59 -0500 Subject: [PATCH 0519/1013] Custom primary keys tests (#2096) --- .gitignore | 2 +- .../create_permission_tables.php.stub | 12 ++--- tests/Permission.php | 4 +- tests/Role.php | 4 +- tests/TestCase.php | 44 +++++++++++++++++-- 5 files changed, 54 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index ba274b9c4..da5ec4b46 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ tests/temp .idea .phpunit.result.cache .php-cs-fixer.cache - +tests/CreatePermissionCustomTables.php diff --git a/database/migrations/create_permission_tables.php.stub b/database/migrations/create_permission_tables.php.stub index f20ef752b..04c3278b9 100644 --- a/database/migrations/create_permission_tables.php.stub +++ b/database/migrations/create_permission_tables.php.stub @@ -26,7 +26,7 @@ class CreatePermissionTables extends Migration } Schema::create($tableNames['permissions'], function (Blueprint $table) { - $table->bigIncrements('id'); + $table->bigIncrements('id'); // permission id $table->string('name'); // For MySQL 8.0 use string('name', 125); $table->string('guard_name'); // For MySQL 8.0 use string('guard_name', 125); $table->timestamps(); @@ -35,7 +35,7 @@ class CreatePermissionTables extends Migration }); Schema::create($tableNames['roles'], function (Blueprint $table) use ($teams, $columnNames) { - $table->bigIncrements('id'); + $table->bigIncrements('id'); // role id if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable(); $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index'); @@ -58,7 +58,7 @@ class CreatePermissionTables extends Migration $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index'); $table->foreign(PermissionRegistrar::$pivotPermission) - ->references('id') + ->references('id') // permission id ->on($tableNames['permissions']) ->onDelete('cascade'); if ($teams) { @@ -82,7 +82,7 @@ class CreatePermissionTables extends Migration $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index'); $table->foreign(PermissionRegistrar::$pivotRole) - ->references('id') + ->references('id') // role id ->on($tableNames['roles']) ->onDelete('cascade'); if ($teams) { @@ -102,12 +102,12 @@ class CreatePermissionTables extends Migration $table->unsignedBigInteger(PermissionRegistrar::$pivotRole); $table->foreign(PermissionRegistrar::$pivotPermission) - ->references('id') + ->references('id') // permission id ->on($tableNames['permissions']) ->onDelete('cascade'); $table->foreign(PermissionRegistrar::$pivotRole) - ->references('id') + ->references('id') // role id ->on($tableNames['roles']) ->onDelete('cascade'); diff --git a/tests/Permission.php b/tests/Permission.php index a621c53fc..d7529ed69 100644 --- a/tests/Permission.php +++ b/tests/Permission.php @@ -4,8 +4,10 @@ class Permission extends \Spatie\Permission\Models\Permission { + protected $primaryKey = 'permission_test_id'; + protected $visible = [ - 'id', + 'permission_test_id', 'name', ]; } diff --git a/tests/Role.php b/tests/Role.php index 1977b00c2..bbe13ece7 100644 --- a/tests/Role.php +++ b/tests/Role.php @@ -4,8 +4,10 @@ class Role extends \Spatie\Permission\Models\Role { + protected $primaryKey = 'role_test_id'; + protected $visible = [ - 'id', + 'role_test_id', 'name', ]; } diff --git a/tests/TestCase.php b/tests/TestCase.php index 80a0c3c0b..8f913c53e 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -39,10 +39,17 @@ abstract class TestCase extends Orchestra /** @var bool */ protected $hasTeams = false; + protected static $migration; + protected static $customMigration; + public function setUp(): void { parent::setUp(); + if (! self::$migration) { + $this->prepareMigration(); + } + // Note: this also flushes the cache from within the migration $this->setUpDatabase($this->app); if ($this->hasTeams) { @@ -125,9 +132,11 @@ protected function setUpDatabase($app) $this->createCacheTable(); } - include_once __DIR__.'/../database/migrations/create_permission_tables.php.stub'; - - (new \CreatePermissionTables())->up(); + if (! $this->useCustomModels) { + self::$migration->up(); + } else { + self::$customMigration->up(); + } $this->testUser = User::create(['email' => 'test@user.com']); $this->testAdmin = Admin::create(['email' => 'admin@user.com']); @@ -141,6 +150,35 @@ protected function setUpDatabase($app) $app[Permission::class]->create(['name' => 'Edit News']); } + private function prepareMigration() + { + $migration = str_replace( + [ + 'CreatePermissionTables', + '(\'id\'); // permission id', + '(\'id\'); // role id', + 'references(\'id\') // permission id', + 'references(\'id\') // role id', + ], + [ + 'CreatePermissionCustomTables', + '(\'permission_test_id\');', + '(\'role_test_id\');', + 'references(\'permission_test_id\')', + 'references(\'role_test_id\')', + ], + file_get_contents(__DIR__.'/../database/migrations/create_permission_tables.php.stub') + ); + + file_put_contents(__DIR__.'/CreatePermissionCustomTables.php', $migration); + + include_once __DIR__.'/../database/migrations/create_permission_tables.php.stub'; + self::$migration = new \CreatePermissionTables(); + + include_once __DIR__.'/CreatePermissionCustomTables.php'; + self::$customMigration = new \CreatePermissionCustomTables(); + } + protected function reloadPermissions() { app(PermissionRegistrar::class)->forgetCachedPermissions(); From f98e34ad47bfbc66ec8e3dd7f62e9f837e5d3daa Mon Sep 17 00:00:00 2001 From: Ayesh Karunaratne Date: Thu, 30 Jun 2022 04:39:24 +0530 Subject: [PATCH 0520/1013] [PHP 8.2] Fix `${var}` string interpolation deprecation (#2117) PHP 8.2 deprecates `"${var}"` string interpolation pattern. This fixes the only such occurrence in `spatie/laravel-permission` package. - [PHP 8.2: `${var}` string interpolation deprecated](https://php.watch/versions/8.2/${var}-string-interpolation-deprecated) - [RFC](https://wiki.php.net/rfc/deprecate_dollar_brace_string_interpolation) --- src/Commands/UpgradeForTeams.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/UpgradeForTeams.php b/src/Commands/UpgradeForTeams.php index 39dd1376c..9923ca19d 100644 --- a/src/Commands/UpgradeForTeams.php +++ b/src/Commands/UpgradeForTeams.php @@ -122,6 +122,6 @@ protected function getMigrationPath($date = null) { $date = $date ?: date('Y_m_d_His'); - return database_path("migrations/${date}_{$this->migrationSuffix}"); + return database_path("migrations/{$date}_{$this->migrationSuffix}"); } } From 738333537f639ab4cabc7367235f93f73fc231b8 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Wed, 29 Jun 2022 18:09:46 -0500 Subject: [PATCH 0521/1013] Use `getKey`, `getKeyName` instead of `id` (#2116) --- src/Models/Role.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Models/Role.php b/src/Models/Role.php index da6f269e0..dfabc0b68 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -193,6 +193,6 @@ public function hasPermissionTo($permission): bool throw GuardDoesNotMatch::create($permission->guard_name, $this->getGuardNames()); } - return $this->permissions->contains('id', $permission->id); + return $this->permissions->contains($permission->getKeyName(), $permission->getKey()); } } From d26cb3d7c63bdc29c77a76c5007cf4813f18173d Mon Sep 17 00:00:00 2001 From: erikn69 Date: Wed, 29 Jun 2022 18:10:45 -0500 Subject: [PATCH 0522/1013] Use static instead of self for extending (#2111) --- src/WildcardPermission.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/WildcardPermission.php b/src/WildcardPermission.php index a1b20a707..884b1c17c 100644 --- a/src/WildcardPermission.php +++ b/src/WildcardPermission.php @@ -41,7 +41,7 @@ public function __construct(string $permission) public function implies($permission): bool { if (is_string($permission)) { - $permission = new self($permission); + $permission = new static($permission); } $otherParts = $permission->getParts(); @@ -52,7 +52,7 @@ public function implies($permission): bool return true; } - if (! $this->parts->get($i)->contains(self::WILDCARD_TOKEN) + if (! $this->parts->get($i)->contains(static::WILDCARD_TOKEN) && ! $this->containsAll($this->parts->get($i), $otherPart)) { return false; } @@ -61,7 +61,7 @@ public function implies($permission): bool } for ($i; $i < $this->parts->count(); $i++) { - if (! $this->parts->get($i)->contains(self::WILDCARD_TOKEN)) { + if (! $this->parts->get($i)->contains(static::WILDCARD_TOKEN)) { return false; } } @@ -105,10 +105,10 @@ protected function setParts(): void throw WildcardPermissionNotProperlyFormatted::create($this->permission); } - $parts = collect(explode(self::PART_DELIMITER, $this->permission)); + $parts = collect(explode(static::PART_DELIMITER, $this->permission)); $parts->each(function ($item, $key) { - $subParts = collect(explode(self::SUBPART_DELIMITER, $item)); + $subParts = collect(explode(static::SUBPART_DELIMITER, $item)); if ($subParts->isEmpty() || $subParts->contains('')) { throw WildcardPermissionNotProperlyFormatted::create($this->permission); From f2303a70be60919811ca8afc313e8244fda00974 Mon Sep 17 00:00:00 2001 From: angeljqv <79208641+angeljqv@users.noreply.github.com> Date: Wed, 29 Jun 2022 18:11:42 -0500 Subject: [PATCH 0523/1013] Clear roles array after hydrate from cache (#2099) --- src/PermissionRegistrar.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 38beec605..e886c4961 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -378,5 +378,7 @@ private function hydrateRolesCache() $role = $roleInstance->newFromBuilder($this->aliasedArray($item)); $this->cachedRoles[$role->getKey()] = $role; }, $this->permissions['roles']); + + $this->permissions['roles'] = []; } } From dbc6c18ef4b2d097350a5faf82382564748e6c97 Mon Sep 17 00:00:00 2001 From: freekmurze Date: Wed, 29 Jun 2022 23:15:44 +0000 Subject: [PATCH 0524/1013] Update CHANGELOG --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 755ea17f1..37ae05db5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.5.5 - 2022-06-29 + +### What's Changed + +- Custom primary keys tests(Only tests) by @erikn69 in https://github.com/spatie/laravel-permission/pull/2096 +- [PHP 8.2] Fix `${var}` string interpolation deprecation by @Ayesh in https://github.com/spatie/laravel-permission/pull/2117 +- Use `getKey`, `getKeyName` instead of `id` by @erikn69 in https://github.com/spatie/laravel-permission/pull/2116 +- On WildcardPermission class use static instead of self for extending by @erikn69 in https://github.com/spatie/laravel-permission/pull/2111 +- Clear roles array after hydrate from cache by @angeljqv in https://github.com/spatie/laravel-permission/pull/2099 + +### New Contributors + +- @Ayesh made their first contribution in https://github.com/spatie/laravel-permission/pull/2117 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.4...5.5.5 + ## 5.5.4 - 2022-05-16 ## What's Changed @@ -364,6 +380,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -417,6 +434,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From 6e1b1122db35b8d779beb9be54a30b0e2e6c2f25 Mon Sep 17 00:00:00 2001 From: drdan18 Date: Thu, 14 Jul 2022 08:32:50 -0400 Subject: [PATCH 0525/1013] Update role-permission.md The explanation of the expected results of hasAllDirectPermissions was incorrect. --- docs/basic-usage/role-permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/role-permissions.md b/docs/basic-usage/role-permissions.md index c2c8fb764..113ab980a 100644 --- a/docs/basic-usage/role-permissions.md +++ b/docs/basic-usage/role-permissions.md @@ -143,7 +143,7 @@ $user->hasAllDirectPermissions(['edit articles', 'delete articles']); $user->hasAnyDirectPermission(['create articles', 'delete articles']); ``` By following the previous example, when we call `$user->hasAllDirectPermissions(['edit articles', 'delete articles'])` -it returns `true`, because the user has all these direct permissions. +it returns `false`, because the user does not have `edit articles` as a direct permission. When we call `$user->hasAnyDirectPermission('edit articles')`, it returns `true` because the user has one of the provided permissions. From 849f5b9432c9cc53b791670b9c9b91c5446cee85 Mon Sep 17 00:00:00 2001 From: Glen Solsberry Date: Tue, 6 Sep 2022 10:29:22 -0400 Subject: [PATCH 0526/1013] Update multiple-guards.md The verbiage provided is specific to a logged-in user, when in reality, it's for any loaded model. --- docs/basic-usage/multiple-guards.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/multiple-guards.md b/docs/basic-usage/multiple-guards.md index 564c232ea..27f453cb1 100644 --- a/docs/basic-usage/multiple-guards.md +++ b/docs/basic-usage/multiple-guards.md @@ -38,7 +38,7 @@ $user->hasPermissionTo('publish articles', 'admin'); > **Note**: When determining whether a role/permission is valid on a given model, it checks against the first matching guard in this order (it does NOT check role/permission for EACH possibility, just the first match): - first the guardName() method if it exists on the model; - then the `$guard_name` property if it exists on the model; -- then the first-defined guard/provider combination in the `auth.guards` config array that matches the logged-in user's guard; +- then the first-defined guard/provider combination in the `auth.guards` config array that matches the loaded model's guard; - then the `auth.defaults.guard` config (which is the user's guard if they are logged in, else the default in the file). From 0a11eec80b2882b068a1bf1d66e75d8a60d216e4 Mon Sep 17 00:00:00 2001 From: Miten Patel Date: Wed, 7 Sep 2022 09:48:20 +0530 Subject: [PATCH 0527/1013] Update teams-permissions.md --- docs/basic-usage/teams-permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/teams-permissions.md b/docs/basic-usage/teams-permissions.md index 9c57ffc15..7c2bf2f54 100644 --- a/docs/basic-usage/teams-permissions.md +++ b/docs/basic-usage/teams-permissions.md @@ -91,7 +91,7 @@ class YourTeamModel extends \Illuminate\Database\Eloquent\Model User::find('your_user_id')->assignRole('Super Admin'); // restore session team_id to package instance setPermissionsTeamId($session_team_id); - } + }); } // ... } From 54addd6e28c01caf5155310fc97b3441d611fd5e Mon Sep 17 00:00:00 2001 From: Androidacy Service Account Date: Fri, 16 Sep 2022 21:05:11 -0400 Subject: [PATCH 0528/1013] Add note about non-standard User models This may lead to some confusing results for users who didn't delve into the advanced usage section. Put a note in Prerequisites about the caveat. --- docs/prerequisites.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/prerequisites.md b/docs/prerequisites.md index 4508c6cdd..cac1e87ac 100644 --- a/docs/prerequisites.md +++ b/docs/prerequisites.md @@ -47,3 +47,7 @@ MySQL 8.0 limits index keys to 1000 characters. This package publishes a migrati Thus in your AppServiceProvider you will need to set `Schema::defaultStringLength(125)`. [See the Laravel Docs for instructions](https://laravel.com/docs/migrations#index-lengths-mysql-mariadb). +## Note for apps using UUIDs/GUIDs + +This package expects the primary key of your `User` model to be an auto-incrementing `int`. If it is not, you may need to modifiy the `create_permission_tables` migration and/or modify the default configuration. See [https://spatie.be/docs/laravel-permission/v5/advanced-usage/uuid](https://spatie.be/docs/laravel-permission/v5/advanced-usage/uuid) for more information. + From e7e78bb73f3b13ab37e8054086b7c0efc5862462 Mon Sep 17 00:00:00 2001 From: angeljqv <79208641+angeljqv@users.noreply.github.com> Date: Wed, 21 Sep 2022 09:50:02 -0500 Subject: [PATCH 0529/1013] Delegate permission collection filter to another method --- src/Traits/HasPermissions.php | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index d116e8ac6..3f89e802d 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -315,15 +315,15 @@ public function getAllPermissions(): Collection } /** - * Grant the given permission(s) to a role. + * Returns permissions ids as array keys * * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions * - * @return $this + * @return array */ - public function givePermissionTo(...$permissions) + public function collectPermissions(...$permissions) { - $permissions = collect($permissions) + return collect($permissions) ->flatten() ->reduce(function ($array, $permission) { if (empty($permission)) { @@ -342,6 +342,18 @@ public function givePermissionTo(...$permissions) return $array; }, []); + } + + /** + * Grant the given permission(s) to a role. + * + * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions + * + * @return $this + */ + public function givePermissionTo(...$permissions) + { + $permissions = $this->collectPermissions(...$permissions); $model = $this->getModel(); From a096b9d03cec1593376a83b7c036af7254e14f50 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Fri, 30 Sep 2022 09:51:31 -0500 Subject: [PATCH 0530/1013] Fix returning all roles instead of the assigned --- src/Traits/HasRoles.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index f70f1daff..6e4d7e5fe 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -189,6 +189,8 @@ public function syncRoles(...$roles) */ public function hasRole($roles, string $guard = null): bool { + $this->loadMissing('roles'); + if (is_string($roles) && false !== strpos($roles, '|')) { $roles = $this->convertPipeToArray($roles); } @@ -248,6 +250,8 @@ public function hasAnyRole(...$roles): bool */ public function hasAllRoles($roles, string $guard = null): bool { + $this->loadMissing('roles'); + if (is_string($roles) && false !== strpos($roles, '|')) { $roles = $this->convertPipeToArray($roles); } @@ -282,6 +286,8 @@ public function hasAllRoles($roles, string $guard = null): bool */ public function hasExactRoles($roles, string $guard = null): bool { + $this->loadMissing('roles'); + if (is_string($roles) && false !== strpos($roles, '|')) { $roles = $this->convertPipeToArray($roles); } @@ -311,6 +317,8 @@ public function getDirectPermissions(): Collection public function getRoleNames(): Collection { + $this->loadMissing('roles'); + return $this->roles->pluck('name'); } From dd47fdde95596252a8147b73e56c446663ab73cc Mon Sep 17 00:00:00 2001 From: Maarten Paauw Date: Wed, 5 Oct 2022 10:13:39 +0200 Subject: [PATCH 0531/1013] Make Writing Policies link clickable The link wasn't clickable at the documentation website. --- docs/basic-usage/role-permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/role-permissions.md b/docs/basic-usage/role-permissions.md index c2c8fb764..259a521c1 100644 --- a/docs/basic-usage/role-permissions.md +++ b/docs/basic-usage/role-permissions.md @@ -170,4 +170,4 @@ the second will be a collection with the `edit article` permission and the third ### NOTE about using permission names in policies -When calling `authorize()` for a policy method, if you have a permission named the same as one of those policy methods, your permission "name" will take precedence and not fire the policy. For this reason it may be wise to avoid naming your permissions the same as the methods in your policy. While you can define your own method names, you can read more about the defaults Laravel offers in Laravel's documentation at https://laravel.com/docs/authorization#writing-policies +When calling `authorize()` for a policy method, if you have a permission named the same as one of those policy methods, your permission "name" will take precedence and not fire the policy. For this reason it may be wise to avoid naming your permissions the same as the methods in your policy. While you can define your own method names, you can read more about the defaults Laravel offers in Laravel's documentation at [Writing Policies](https://laravel.com/docs/authorization#writing-policies). From 5d239d87ab001466274c3fd2fe04936c6a77e4b3 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 12 Oct 2022 11:46:10 -0400 Subject: [PATCH 0532/1013] Fix typo --- docs/prerequisites.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/prerequisites.md b/docs/prerequisites.md index cac1e87ac..069667cdb 100644 --- a/docs/prerequisites.md +++ b/docs/prerequisites.md @@ -49,5 +49,5 @@ Thus in your AppServiceProvider you will need to set `Schema::defaultStringLengt ## Note for apps using UUIDs/GUIDs -This package expects the primary key of your `User` model to be an auto-incrementing `int`. If it is not, you may need to modifiy the `create_permission_tables` migration and/or modify the default configuration. See [https://spatie.be/docs/laravel-permission/v5/advanced-usage/uuid](https://spatie.be/docs/laravel-permission/v5/advanced-usage/uuid) for more information. +This package expects the primary key of your `User` model to be an auto-incrementing `int`. If it is not, you may need to modify the `create_permission_tables` migration and/or modify the default configuration. See [https://spatie.be/docs/laravel-permission/v5/advanced-usage/uuid](https://spatie.be/docs/laravel-permission/v5/advanced-usage/uuid) for more information. From 309ec2d5eee1ea71f6bcdf77f6eddd4972f316a5 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Fri, 14 Oct 2022 11:08:06 -0500 Subject: [PATCH 0533/1013] Add ULIDs reference --- docs/prerequisites.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/prerequisites.md b/docs/prerequisites.md index 069667cdb..fc64ed7a8 100644 --- a/docs/prerequisites.md +++ b/docs/prerequisites.md @@ -47,7 +47,7 @@ MySQL 8.0 limits index keys to 1000 characters. This package publishes a migrati Thus in your AppServiceProvider you will need to set `Schema::defaultStringLength(125)`. [See the Laravel Docs for instructions](https://laravel.com/docs/migrations#index-lengths-mysql-mariadb). -## Note for apps using UUIDs/GUIDs +## Note for apps using UUIDs/ULIDs/GUIDs This package expects the primary key of your `User` model to be an auto-incrementing `int`. If it is not, you may need to modify the `create_permission_tables` migration and/or modify the default configuration. See [https://spatie.be/docs/laravel-permission/v5/advanced-usage/uuid](https://spatie.be/docs/laravel-permission/v5/advanced-usage/uuid) for more information. From fc9ea8586cc24b09f04f1a966cb7c943149b6f5e Mon Sep 17 00:00:00 2001 From: erikn69 Date: Fri, 14 Oct 2022 12:55:12 -0500 Subject: [PATCH 0534/1013] PHP 8.2 Build --- .github/workflows/run-tests-L8.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-tests-L8.yml b/.github/workflows/run-tests-L8.yml index 5843a98c9..11c4fcbae 100644 --- a/.github/workflows/run-tests-L8.yml +++ b/.github/workflows/run-tests-L8.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - php: [8.1, 8.0, 7.4, 7.3] + php: [8.2, 8.1, 8.0, 7.4, 7.3] laravel: [9.*, 8.*] dependency-version: [prefer-lowest, prefer-stable] include: @@ -38,7 +38,7 @@ jobs: - name: Install dependencies run: | - composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:>=4.3.4" "mockery/mockery:^1.3.2" --no-interaction --no-update + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:>=4.3.4" "mockery/mockery:^1.3.2" "nesbot/carbon:>=2.62.1" --no-interaction --no-update composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction - name: Execute tests From ecf27c5f0d62210a38f9edef59352ec872b1d241 Mon Sep 17 00:00:00 2001 From: Eric Junker Date: Mon, 17 Oct 2022 11:35:00 -0500 Subject: [PATCH 0535/1013] Prevent MissingAttributeException for guard_name When new Laravel feature `Model::preventAccessingMissingAttributes()` is enabled you may get a `MissingAttributeException` for `guard_name`. This change uses `getAttributeValue()` to prevent the exception. Fixes #2215 --- src/Guard.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Guard.php b/src/Guard.php index 41d802c10..c0630392b 100644 --- a/src/Guard.php +++ b/src/Guard.php @@ -22,7 +22,7 @@ public static function getNames($model): Collection if (\method_exists($model, 'guardName')) { $guardName = $model->guardName(); } else { - $guardName = $model->guard_name ?? null; + $guardName = $model->getAttributeValue('guard_name'); } } From 5e8b0695f4e1ac8e9a09edb7b06a0aa92547e05e Mon Sep 17 00:00:00 2001 From: angeljqv <79208641+angeljqv@users.noreply.github.com> Date: Tue, 18 Oct 2022 20:37:09 -0500 Subject: [PATCH 0536/1013] Delegate permission filter to another method (#2183) * Delegate permission filter to another method --- src/Traits/HasPermissions.php | 45 +++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 3f89e802d..7e89ec7a2 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -115,20 +115,15 @@ protected function convertToPermissionModels($permissions): array } /** - * Determine if the model may perform the given permission. + * Find a permission. * * @param string|int|\Spatie\Permission\Contracts\Permission $permission - * @param string|null $guardName * - * @return bool + * @return \Spatie\Permission\Contracts\Permission * @throws PermissionDoesNotExist */ - public function hasPermissionTo($permission, $guardName = null): bool + public function filterPermission($permission, $guardName = null) { - if (config('permission.enable_wildcard_permission', false)) { - return $this->hasWildcardPermission($permission, $guardName); - } - $permissionClass = $this->getPermissionClass(); if (is_string($permission)) { @@ -149,6 +144,26 @@ public function hasPermissionTo($permission, $guardName = null): bool throw new PermissionDoesNotExist(); } + return $permission; + } + + /** + * Determine if the model may perform the given permission. + * + * @param string|int|\Spatie\Permission\Contracts\Permission $permission + * @param string|null $guardName + * + * @return bool + * @throws PermissionDoesNotExist + */ + public function hasPermissionTo($permission, $guardName = null): bool + { + if (config('permission.enable_wildcard_permission', false)) { + return $this->hasWildcardPermission($permission, $guardName); + } + + $permission = $this->filterPermission($permission, $guardName); + return $this->hasDirectPermission($permission) || $this->hasPermissionViaRole($permission); } @@ -271,19 +286,7 @@ protected function hasPermissionViaRole(Permission $permission): bool */ public function hasDirectPermission($permission): bool { - $permissionClass = $this->getPermissionClass(); - - if (is_string($permission)) { - $permission = $permissionClass->findByName($permission, $this->getDefaultGuardName()); - } - - if (is_int($permission)) { - $permission = $permissionClass->findById($permission, $this->getDefaultGuardName()); - } - - if (! $permission instanceof Permission) { - throw new PermissionDoesNotExist(); - } + $permission = $this->filterPermission($permission); return $this->permissions->contains($permission->getKeyName(), $permission->getKey()); } From cca6226273787be4674aa23f552c8426495e6f1f Mon Sep 17 00:00:00 2001 From: drbyte Date: Wed, 19 Oct 2022 03:01:56 +0000 Subject: [PATCH 0537/1013] Update CHANGELOG --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37ae05db5..76c98176f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,30 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.5.6 - 2022-10-19 + +Just a maintenance release. + +### What's Changed + +- Actions: add PHP 8.2 Build by @erikn69 in https://github.com/spatie/laravel-permission/pull/2214 +- Docs: Fix small syntax error in teams-permissions.md by @miten5 in https://github.com/spatie/laravel-permission/pull/2171 +- Docs: Update documentation for multiple guards by @gms8994 in https://github.com/spatie/laravel-permission/pull/2169 +- Docs: Make Writing Policies link clickable by @maartenpaauw in https://github.com/spatie/laravel-permission/pull/2202 +- Docs: Add note about non-standard User models by @androidacy-user in https://github.com/spatie/laravel-permission/pull/2179 +- Docs: Fix explanation of results for hasAllDirectPermissions in role-permission.md by @drdan18 in https://github.com/spatie/laravel-permission/pull/2139 +- Docs: Add ULIDs reference by @erikn69 in https://github.com/spatie/laravel-permission/pull/2213 + +### New Contributors + +- @miten5 made their first contribution in https://github.com/spatie/laravel-permission/pull/2171 +- @gms8994 made their first contribution in https://github.com/spatie/laravel-permission/pull/2169 +- @maartenpaauw made their first contribution in https://github.com/spatie/laravel-permission/pull/2202 +- @androidacy-user made their first contribution in https://github.com/spatie/laravel-permission/pull/2179 +- @drdan18 made their first contribution in https://github.com/spatie/laravel-permission/pull/2139 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.5...5.5.6 + ## 5.5.5 - 2022-06-29 ### What's Changed @@ -381,6 +405,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -435,6 +460,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From 5bd7b0c5a4765864e8fcbc36237b1ca5274255f6 Mon Sep 17 00:00:00 2001 From: drbyte Date: Wed, 19 Oct 2022 03:04:02 +0000 Subject: [PATCH 0538/1013] Update CHANGELOG --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76c98176f..17258ca8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.5.7 - 2022-10-19 + +Optimize HasPermissions trait + +### What's Changed + +- Delegate permission collection filter to another method by @angeljqv in https://github.com/spatie/laravel-permission/pull/2182 +- Delegate permission filter to another method by @angeljqv in https://github.com/spatie/laravel-permission/pull/2183 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.6...5.5.7 + ## 5.5.6 - 2022-10-19 Just a maintenance release. @@ -406,6 +417,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -461,6 +473,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From 647d6e629eb7434ad57724c74fadd833456007f7 Mon Sep 17 00:00:00 2001 From: drbyte Date: Wed, 19 Oct 2022 03:05:52 +0000 Subject: [PATCH 0539/1013] Update CHANGELOG --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17258ca8e..cbd4c04c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.5.8 - 2022-10-19 + +`HasRoles` trait + +### What's Changed + +- Fix returning all roles instead of the assigned by @erikn69 in https://github.com/spatie/laravel-permission/pull/2194 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.7...5.5.8 + ## 5.5.7 - 2022-10-19 Optimize HasPermissions trait @@ -418,6 +428,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -474,6 +485,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From 2f266d80d6b0b5c5eeb29b190e45456a96769b86 Mon Sep 17 00:00:00 2001 From: drbyte Date: Wed, 19 Oct 2022 03:07:39 +0000 Subject: [PATCH 0540/1013] Update CHANGELOG --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbd4c04c3..ccafa27ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.5.9 - 2022-10-19 + +Compatibility Bugfix + +### What's Changed + +- Prevent `MissingAttributeException` for `guard_name` by @ejunker in https://github.com/spatie/laravel-permission/pull/2216 + +### New Contributors + +- @ejunker made their first contribution in https://github.com/spatie/laravel-permission/pull/2216 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.8...5.5.9 + ## 5.5.8 - 2022-10-19 `HasRoles` trait @@ -429,6 +443,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -486,6 +501,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From 9e742069966b3540f5642f54f8c1149d214ab504 Mon Sep 17 00:00:00 2001 From: drbyte Date: Wed, 19 Oct 2022 03:12:43 +0000 Subject: [PATCH 0541/1013] Update CHANGELOG --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccafa27ab..eaed58a41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.5.10 - 2022-10-19 + +### What's Changed + +- Avoid calling the config helper in the role/perm model constructor by @adiafora in https://github.com/spatie/laravel-permission/pull/2098 as discussed in https://github.com/spatie/laravel-permission/issues/2097 regarding `DI` + +### New Contributors + +- @adiafora made their first contribution in https://github.com/spatie/laravel-permission/pull/2098 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.9...5.5.10 + ## 5.5.9 - 2022-10-19 Compatibility Bugfix @@ -444,6 +456,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -502,6 +515,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From b8e717b2248dfbc078e932e5487683a09f69ffbc Mon Sep 17 00:00:00 2001 From: erikn69 Date: Mon, 5 Sep 2022 11:35:59 -0500 Subject: [PATCH 0542/1013] Support static arrays on blade directives --- src/PermissionServiceProvider.php | 33 +++++++------------ tests/BladeTest.php | 31 +++++++++++++++-- .../views/guardHasAllRolesArray.blade.php | 5 +++ 3 files changed, 46 insertions(+), 23 deletions(-) create mode 100644 tests/resources/views/guardHasAllRolesArray.blade.php diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 61fc1321b..e408064a9 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -84,62 +84,53 @@ protected function registerModelBindings() $this->app->bind(RoleContract::class, $config['role']); } + public static function bladeMethodWrapper($method, $role, $guard = null) + { + return auth($guard)->check() && auth($guard)->user()->{$method}($role); + } + protected function registerBladeExtensions($bladeCompiler) { $bladeCompiler->directive('role', function ($arguments) { - list($role, $guard) = explode(',', $arguments.','); - - return "check() && auth({$guard})->user()->hasRole({$role})): ?>"; + return ""; }); $bladeCompiler->directive('elserole', function ($arguments) { - list($role, $guard) = explode(',', $arguments.','); - - return "check() && auth({$guard})->user()->hasRole({$role})): ?>"; + return ""; }); $bladeCompiler->directive('endrole', function () { return ''; }); $bladeCompiler->directive('hasrole', function ($arguments) { - list($role, $guard) = explode(',', $arguments.','); - - return "check() && auth({$guard})->user()->hasRole({$role})): ?>"; + return ""; }); $bladeCompiler->directive('endhasrole', function () { return ''; }); $bladeCompiler->directive('hasanyrole', function ($arguments) { - list($roles, $guard) = explode(',', $arguments.','); - - return "check() && auth({$guard})->user()->hasAnyRole({$roles})): ?>"; + return ""; }); $bladeCompiler->directive('endhasanyrole', function () { return ''; }); $bladeCompiler->directive('hasallroles', function ($arguments) { - list($roles, $guard) = explode(',', $arguments.','); - - return "check() && auth({$guard})->user()->hasAllRoles({$roles})): ?>"; + return ""; }); $bladeCompiler->directive('endhasallroles', function () { return ''; }); $bladeCompiler->directive('unlessrole', function ($arguments) { - list($role, $guard) = explode(',', $arguments.','); - - return "check() || ! auth({$guard})->user()->hasRole({$role})): ?>"; + return ""; }); $bladeCompiler->directive('endunlessrole', function () { return ''; }); $bladeCompiler->directive('hasexactroles', function ($arguments) { - list($roles, $guard) = explode(',', $arguments.','); - - return "check() && auth({$guard})->user()->hasExactRoles({$roles})): ?>"; + return ""; }); $bladeCompiler->directive('endhasexactroles', function () { return ''; diff --git a/tests/BladeTest.php b/tests/BladeTest.php index 8558f5092..21d66437f 100644 --- a/tests/BladeTest.php +++ b/tests/BladeTest.php @@ -31,9 +31,9 @@ public function all_blade_directives_will_evaluate_false_when_there_is_nobody_lo $this->assertEquals('does not have permission', $this->renderView('can', ['permission' => $permission])); $this->assertEquals('does not have role', $this->renderView('role', compact('role', 'elserole'))); $this->assertEquals('does not have role', $this->renderView('hasRole', compact('role', 'elserole'))); - $this->assertEquals('does not have all of the given roles', $this->renderView('hasAllRoles', $roles)); + $this->assertEquals('does not have all of the given roles', $this->renderView('hasAllRoles', compact('roles'))); $this->assertEquals('does not have all of the given roles', $this->renderView('hasAllRoles', ['roles' => implode('|', $roles)])); - $this->assertEquals('does not have any of the given roles', $this->renderView('hasAnyRole', $roles)); + $this->assertEquals('does not have any of the given roles', $this->renderView('hasAnyRole', compact('roles'))); $this->assertEquals('does not have any of the given roles', $this->renderView('hasAnyRole', ['roles' => implode('|', $roles)])); } @@ -262,6 +262,33 @@ public function the_hasallroles_directive_will_evaluate_false_when_the_logged_in $this->assertEquals('does not have all of the given roles', $this->renderView('guardHasAllRolesPipe', compact('guard'))); } + /** @test */ + public function the_hasallroles_directive_will_evaluate_true_when_the_logged_in_user_does_have_all_required_roles_in_array() + { + $guard = 'admin'; + + $admin = $this->getSuperAdmin(); + + $admin->assignRole('moderator'); + + auth('admin')->setUser($admin); + + $this->assertEquals('does have all of the given roles', $this->renderView('guardHasAllRolesArray', compact('guard'))); + } + + /** @test */ + public function the_hasallroles_directive_will_evaluate_false_when_the_logged_in_user_doesnt_have_all_required_roles_in_array() + { + $guard = ''; + $user = $this->getMember(); + + $user->assignRole('writer'); + + auth()->setUser($user); + + $this->assertEquals('does not have all of the given roles', $this->renderView('guardHasAllRolesArray', compact('guard'))); + } + protected function getWriter() { $this->testUser->assignRole('writer'); diff --git a/tests/resources/views/guardHasAllRolesArray.blade.php b/tests/resources/views/guardHasAllRolesArray.blade.php new file mode 100644 index 000000000..6ba51c1d8 --- /dev/null +++ b/tests/resources/views/guardHasAllRolesArray.blade.php @@ -0,0 +1,5 @@ +@hasallroles(['super-admin', 'moderator'], $guard) +does have all of the given roles +@else +does not have all of the given roles +@endhasallroles From 1cb0a5ca72a04f3a2e34ecfe94340b30a6f92e82 Mon Sep 17 00:00:00 2001 From: drbyte Date: Wed, 19 Oct 2022 16:01:19 +0000 Subject: [PATCH 0543/1013] Update CHANGELOG --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eaed58a41..01a4a7ac9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.5.11 - 2022-10-19 + +### What's Changed + +- Support static arrays on blade directives by @erikn69 in https://github.com/spatie/laravel-permission/pull/2168 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.10...5.5.11 + ## 5.5.10 - 2022-10-19 ### What's Changed @@ -457,6 +465,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -516,6 +525,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From e4493daedcab9aac91b9d1549ebcbad44f3f7326 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Wed, 19 Oct 2022 13:38:41 -0500 Subject: [PATCH 0544/1013] Fix undefined index guard_name --- src/Models/Permission.php | 2 +- src/Models/Role.php | 2 +- tests/PermissionTest.php | 14 ++++++++++++++ tests/RoleTest.php | 13 +++++++++++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 9ef30ed17..066a37840 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -64,7 +64,7 @@ public function roles(): BelongsToMany public function users(): BelongsToMany { return $this->morphedByMany( - getModelForGuard($this->attributes['guard_name']), + getModelForGuard($this->attributes['guard_name'] ?? config('auth.defaults.guard')), 'model', config('permission.table_names.model_has_permissions'), PermissionRegistrar::$pivotPermission, diff --git a/src/Models/Role.php b/src/Models/Role.php index 731e99988..84f913618 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -70,7 +70,7 @@ public function permissions(): BelongsToMany public function users(): BelongsToMany { return $this->morphedByMany( - getModelForGuard($this->attributes['guard_name']), + getModelForGuard($this->attributes['guard_name'] ?? config('auth.defaults.guard')), 'model', config('permission.table_names.model_has_roles'), PermissionRegistrar::$pivotRole, diff --git a/tests/PermissionTest.php b/tests/PermissionTest.php index 35f02d0b0..b157d247d 100644 --- a/tests/PermissionTest.php +++ b/tests/PermissionTest.php @@ -7,6 +7,20 @@ class PermissionTest extends TestCase { + /** @test */ + public function it_get_user_models_using_with() + { + $this->testUser->givePermissionTo($this->testUserPermission); + + $permission = app(Permission::class)::with('users') + ->where($this->testUserPermission->getKeyName(), $this->testUserPermission->getKey()) + ->first(); + + $this->assertEquals($permission->getKey(), $this->testUserPermission->getKey()); + $this->assertCount(1, $permission->users); + $this->assertEquals($permission->users[0]->id, $this->testUser->id); + } + /** @test */ public function it_throws_an_exception_when_the_permission_already_exists() { diff --git a/tests/RoleTest.php b/tests/RoleTest.php index 61a958b8d..35e287d50 100644 --- a/tests/RoleTest.php +++ b/tests/RoleTest.php @@ -21,6 +21,19 @@ public function setUp(): void Permission::create(['name' => 'wrong-guard-permission', 'guard_name' => 'admin']); } + /** @test */ + public function it_get_user_models_using_with() + { + $this->testUser->assignRole($this->testUserRole); + + $role = app(Role::class)::with('users') + ->where($this->testUserRole->getKeyName(), $this->testUserRole->getKey())->first(); + + $this->assertEquals($role->getKey(), $this->testUserRole->getKey()); + $this->assertCount(1, $role->users); + $this->assertEquals($role->users[0]->id, $this->testUser->id); + } + /** @test */ public function it_has_user_models_of_the_right_class() { From 3cbed36eb0524f518bb2cbf1bf69e7df489c3424 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Wed, 19 Oct 2022 14:24:57 -0500 Subject: [PATCH 0545/1013] Fix detaching user models on teams feature #2220 --- src/Traits/HasPermissions.php | 3 +++ src/Traits/HasRoles.php | 3 +++ tests/TeamHasRolesTest.php | 26 ++++++++++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 7e89ec7a2..8234dd666 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -27,7 +27,10 @@ public static function bootHasPermissions() return; } + $teams = PermissionRegistrar::$teams; + PermissionRegistrar::$teams = false; $model->permissions()->detach(); + PermissionRegistrar::$teams = $teams; }); } diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 6e4d7e5fe..59faeec79 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -24,7 +24,10 @@ public static function bootHasRoles() return; } + $teams = PermissionRegistrar::$teams; + PermissionRegistrar::$teams = false; $model->roles()->detach(); + PermissionRegistrar::$teams = $teams; }); } diff --git a/tests/TeamHasRolesTest.php b/tests/TeamHasRolesTest.php index 05393194a..d3f35d899 100644 --- a/tests/TeamHasRolesTest.php +++ b/tests/TeamHasRolesTest.php @@ -9,6 +9,32 @@ class TeamHasRolesTest extends HasRolesTest /** @var bool */ protected $hasTeams = true; + /** @test */ + public function it_deletes_pivot_table_entries_when_deleting_models() + { + $user1 = User::create(['email' => 'user2@test.com']); + $user2 = User::create(['email' => 'user2@test.com']); + + setPermissionsTeamId(1); + $user1->assignRole('testRole'); + $user1->givePermissionTo('edit-articles'); + $user2->assignRole('testRole'); + $user2->givePermissionTo('edit-articles'); + setPermissionsTeamId(2); + $user1->givePermissionTo('edit-news'); + + $this->assertDatabaseHas('model_has_permissions', [config('permission.column_names.model_morph_key') => $user1->id]); + $this->assertDatabaseHas('model_has_roles', [config('permission.column_names.model_morph_key') => $user1->id]); + + $user1->delete(); + + setPermissionsTeamId(1); + $this->assertDatabaseMissing('model_has_permissions', [config('permission.column_names.model_morph_key') => $user1->id]); + $this->assertDatabaseMissing('model_has_roles', [config('permission.column_names.model_morph_key') => $user1->id]); + $this->assertDatabaseHas('model_has_permissions', [config('permission.column_names.model_morph_key') => $user2->id]); + $this->assertDatabaseHas('model_has_roles', [config('permission.column_names.model_morph_key') => $user2->id]); + } + /** @test */ public function it_can_assign_same_and_different_roles_on_same_user_different_teams() { From 9adc4b2189e216d1770ffaa8783e5c7cf2ff4174 Mon Sep 17 00:00:00 2001 From: drbyte Date: Wed, 19 Oct 2022 20:28:08 +0000 Subject: [PATCH 0546/1013] Update CHANGELOG --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01a4a7ac9..100c68091 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.5.12 - 2022-10-19 + +Fix regression introduced in `5.5.10` + +### What's Changed + +- Fix undefined index guard_name by @erikn69 in https://github.com/spatie/laravel-permission/pull/2219 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.11...5.5.12 + ## 5.5.11 - 2022-10-19 ### What's Changed @@ -466,6 +476,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -526,6 +537,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From f1ab1775bf68e57f8eeca262ef7294559968a45b Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 19 Oct 2022 17:13:51 -0400 Subject: [PATCH 0547/1013] Add clarity about coding for permissions --- docs/best-practices/roles-vs-permissions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/best-practices/roles-vs-permissions.md b/docs/best-practices/roles-vs-permissions.md index 50be7b117..b7d99b01f 100644 --- a/docs/best-practices/roles-vs-permissions.md +++ b/docs/best-practices/roles-vs-permissions.md @@ -3,9 +3,9 @@ title: Roles vs Permissions weight: 1 --- -It is generally best to code your app around `permissions` only. That way you can always use the native Laravel `@can` and `can()` directives everywhere in your app. +It is generally best to code your app around testing against `permissions` only. (ie: when testing whether to grant access to something, in most cases it's wisest to check against a `permission`, not a `role`). That way you can always use the native Laravel `@can` and `can()` directives everywhere in your app. -Roles can still be used to group permissions for easy assignment, and you can still use the role-based helper methods if truly necessary. But most app-related logic can usually be best controlled using the `can` methods, which allows Laravel's Gate layer to do all the heavy lifting. +Roles can still be used to group permissions for easy assignment to a user/model, and you can still use the role-based helper methods if truly necessary. But most app-related logic can usually be best controlled using the `can` methods, which allows Laravel's Gate layer to do all the heavy lifting. Sometimes certain groups of `route` rules may make best sense to group them around a `role`, but still, whenever possible, there is less overhead used if you can check against a specific `permission` instead. eg: `users` have `roles`, and `roles` have `permissions`, and your app always checks for `permissions`, not `roles`. From 4807005e6c299f0e0f8165c18a4c23d0d357d8d4 Mon Sep 17 00:00:00 2001 From: Julio Motol Date: Fri, 21 Oct 2022 08:41:12 +0800 Subject: [PATCH 0548/1013] =?UTF-8?q?=F0=9F=90=9B=20Override=20`getAllPerm?= =?UTF-8?q?issions()`=20in=20`Role`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julio Motol --- src/Models/Role.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Models/Role.php b/src/Models/Role.php index bb4173d82..b26a9af38 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Support\Collection; use Spatie\Permission\Contracts\Role as RoleContract; use Spatie\Permission\Exceptions\GuardDoesNotMatch; use Spatie\Permission\Exceptions\RoleAlreadyExists; @@ -195,4 +196,12 @@ public function hasPermissionTo($permission): bool return $this->permissions->contains($permission->getKeyName(), $permission->getKey()); } + + /** + * Return all the permissions the model has, both directly and via roles. + */ + public function getAllPermissions(): Collection + { + return $this->permissions->sort()->values(); + } } From eaaeb69113c5878f9d452bc2fe4876363e415380 Mon Sep 17 00:00:00 2001 From: Yao Date: Fri, 21 Oct 2022 10:12:53 +0800 Subject: [PATCH 0549/1013] fix: A bad configuration was used in forRoles --- src/Exceptions/UnauthorizedException.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Exceptions/UnauthorizedException.php b/src/Exceptions/UnauthorizedException.php index 373f426cc..da6ab32b3 100644 --- a/src/Exceptions/UnauthorizedException.php +++ b/src/Exceptions/UnauthorizedException.php @@ -14,7 +14,7 @@ public static function forRoles(array $roles): self { $message = 'User does not have the right roles.'; - if (config('permission.display_permission_in_exception')) { + if (config('permission.display_role_in_exception')) { $permStr = implode(', ', $roles); $message = 'User does not have the right roles. Necessary roles are '.$permStr; } From 6a4747d79b2118581a109d8082db09ebadbff5fe Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 21 Oct 2022 00:46:41 -0400 Subject: [PATCH 0550/1013] Revert "Avoid calling the config helper in the role/perm model constructor" --- src/Models/Permission.php | 2 ++ src/Models/Role.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 066a37840..1a044e694 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -22,6 +22,8 @@ class Permission extends Model implements PermissionContract public function __construct(array $attributes = []) { + $attributes['guard_name'] = $attributes['guard_name'] ?? config('auth.defaults.guard'); + parent::__construct($attributes); $this->guarded[] = $this->primaryKey; diff --git a/src/Models/Role.php b/src/Models/Role.php index 84f913618..bb4173d82 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -22,6 +22,8 @@ class Role extends Model implements RoleContract public function __construct(array $attributes = []) { + $attributes['guard_name'] = $attributes['guard_name'] ?? config('auth.defaults.guard'); + parent::__construct($attributes); $this->guarded[] = $this->primaryKey; From 87850d72185278b65a33b58b6016dd3564d3a583 Mon Sep 17 00:00:00 2001 From: drbyte Date: Fri, 21 Oct 2022 04:50:01 +0000 Subject: [PATCH 0551/1013] Update CHANGELOG --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 100c68091..4e21a691d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.5.13 - 2022-10-21 + +### What's Changed + +- fix UnauthorizedException: Wrong configuration was used in forRoles by @Sy-Dante in https://github.com/spatie/laravel-permission/pull/2224 + +### New Contributors + +- @Sy-Dante made their first contribution in https://github.com/spatie/laravel-permission/pull/2224 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.12...5.5.13 + ## 5.5.12 - 2022-10-19 Fix regression introduced in `5.5.10` @@ -477,6 +489,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -538,6 +551,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From dc9c48cbff08dcc13fbe3b07d81fa6f0be3de6e3 Mon Sep 17 00:00:00 2001 From: drbyte Date: Fri, 21 Oct 2022 04:50:57 +0000 Subject: [PATCH 0552/1013] Update CHANGELOG --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e21a691d..acdc7ed3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.5.14 - 2022-10-21 + +FIXED BREAKING CHANGE. (Sorry about that!) + +### What's Changed + +- Revert "Avoid calling the config helper in the role/perm model constructor" by @drbyte in https://github.com/spatie/laravel-permission/pull/2225 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.13...5.5.14 + ## 5.5.13 - 2022-10-21 ### What's Changed @@ -490,6 +500,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -552,6 +563,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From 446c39f0658f6f0606eaf2dcb4f51b79aafdd913 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Fri, 21 Oct 2022 11:52:37 -0500 Subject: [PATCH 0553/1013] Add tests for display roles/permissions on UnauthorizedException --- src/Exceptions/UnauthorizedException.php | 9 ++--- tests/PermissionMiddlewareTest.php | 23 +++++++++++++ tests/RoleMiddlewareTest.php | 23 +++++++++++++ tests/RoleOrPermissionMiddlewareTest.php | 42 ++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 6 deletions(-) diff --git a/src/Exceptions/UnauthorizedException.php b/src/Exceptions/UnauthorizedException.php index da6ab32b3..2a270faf8 100644 --- a/src/Exceptions/UnauthorizedException.php +++ b/src/Exceptions/UnauthorizedException.php @@ -15,8 +15,7 @@ public static function forRoles(array $roles): self $message = 'User does not have the right roles.'; if (config('permission.display_role_in_exception')) { - $permStr = implode(', ', $roles); - $message = 'User does not have the right roles. Necessary roles are '.$permStr; + $message .= ' Necessary roles are '.implode(', ', $roles); } $exception = new static(403, $message, null, []); @@ -30,8 +29,7 @@ public static function forPermissions(array $permissions): self $message = 'User does not have the right permissions.'; if (config('permission.display_permission_in_exception')) { - $permStr = implode(', ', $permissions); - $message = 'User does not have the right permissions. Necessary permissions are '.$permStr; + $message .= ' Necessary permissions are '.implode(', ', $permissions); } $exception = new static(403, $message, null, []); @@ -45,8 +43,7 @@ public static function forRolesOrPermissions(array $rolesOrPermissions): self $message = 'User does not have any of the necessary access rights.'; if (config('permission.display_permission_in_exception') && config('permission.display_role_in_exception')) { - $permStr = implode(', ', $rolesOrPermissions); - $message = 'User does not have the right permissions. Necessary permissions are '.$permStr; + $message .= ' Necessary roles or permissions are '.implode(', ', $rolesOrPermissions); } $exception = new static(403, $message, null, []); diff --git a/tests/PermissionMiddlewareTest.php b/tests/PermissionMiddlewareTest.php index fb49073d4..8d294fd48 100644 --- a/tests/PermissionMiddlewareTest.php +++ b/tests/PermissionMiddlewareTest.php @@ -5,6 +5,7 @@ use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Config; use InvalidArgumentException; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Exceptions\UnauthorizedException; @@ -147,6 +148,7 @@ public function the_required_permissions_can_be_fetched_from_the_exception() { Auth::login($this->testUser); + $message = null; $requiredPermissions = []; try { @@ -154,12 +156,33 @@ public function the_required_permissions_can_be_fetched_from_the_exception() return (new Response())->setContent(''); }, 'some-permission'); } catch (UnauthorizedException $e) { + $message = $e->getMessage(); $requiredPermissions = $e->getRequiredPermissions(); } + $this->assertEquals('User does not have the right permissions.', $message); $this->assertEquals(['some-permission'], $requiredPermissions); } + /** @test */ + public function the_required_permissions_can_be_displayed_in_the_exception() + { + Auth::login($this->testUser); + Config::set(['permission.display_permission_in_exception' => true]); + + $message = null; + + try { + $this->permissionMiddleware->handle(new Request(), function () { + return (new Response())->setContent(''); + }, 'some-permission'); + } catch (UnauthorizedException $e) { + $message = $e->getMessage(); + } + + $this->assertStringEndsWith('Necessary permissions are some-permission', $message); + } + /** @test */ public function use_not_existing_custom_guard_in_permission() { diff --git a/tests/RoleMiddlewareTest.php b/tests/RoleMiddlewareTest.php index 70fd27e86..e100af32c 100644 --- a/tests/RoleMiddlewareTest.php +++ b/tests/RoleMiddlewareTest.php @@ -5,6 +5,7 @@ use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Config; use InvalidArgumentException; use Spatie\Permission\Exceptions\UnauthorizedException; use Spatie\Permission\Middlewares\RoleMiddleware; @@ -113,6 +114,7 @@ public function the_required_roles_can_be_fetched_from_the_exception() { Auth::login($this->testUser); + $message = null; $requiredRoles = []; try { @@ -120,12 +122,33 @@ public function the_required_roles_can_be_fetched_from_the_exception() return (new Response())->setContent(''); }, 'some-role'); } catch (UnauthorizedException $e) { + $message = $e->getMessage(); $requiredRoles = $e->getRequiredRoles(); } + $this->assertEquals('User does not have the right roles.', $message); $this->assertEquals(['some-role'], $requiredRoles); } + /** @test */ + public function the_required_roles_can_be_displayed_in_the_exception() + { + Auth::login($this->testUser); + Config::set(['permission.display_role_in_exception' => true]); + + $message = null; + + try { + $this->roleMiddleware->handle(new Request(), function () { + return (new Response())->setContent(''); + }, 'some-role'); + } catch (UnauthorizedException $e) { + $message = $e->getMessage(); + } + + $this->assertStringEndsWith('Necessary roles are some-role', $message); + } + /** @test */ public function use_not_existing_custom_guard_in_role() { diff --git a/tests/RoleOrPermissionMiddlewareTest.php b/tests/RoleOrPermissionMiddlewareTest.php index 00e81b290..345f0209a 100644 --- a/tests/RoleOrPermissionMiddlewareTest.php +++ b/tests/RoleOrPermissionMiddlewareTest.php @@ -5,6 +5,7 @@ use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Config; use InvalidArgumentException; use Spatie\Permission\Exceptions\UnauthorizedException; use Spatie\Permission\Middlewares\RoleOrPermissionMiddleware; @@ -123,6 +124,47 @@ public function user_can_access_permission_or_role_with_guard_admin_while_login_ ); } + /** @test */ + public function the_required_permissions_or_roles_can_be_fetched_from_the_exception() + { + Auth::login($this->testUser); + + $message = null; + $requiredRolesOrPermissions = []; + + try { + $this->roleOrPermissionMiddleware->handle(new Request(), function () { + return (new Response())->setContent(''); + }, 'some-permission|some-role'); + } catch (UnauthorizedException $e) { + $message = $e->getMessage(); + $requiredRolesOrPermissions = $e->getRequiredPermissions(); + } + + $this->assertEquals('User does not have any of the necessary access rights.', $message); + $this->assertEquals(['some-permission', 'some-role'], $requiredRolesOrPermissions); + } + + /** @test */ + public function the_required_permissions_or_roles_can_be_displayed_in_the_exception() + { + Auth::login($this->testUser); + Config::set(['permission.display_permission_in_exception' => true]); + Config::set(['permission.display_role_in_exception' => true]); + + $message = null; + + try { + $this->roleOrPermissionMiddleware->handle(new Request(), function () { + return (new Response())->setContent(''); + }, 'some-permission|some-role'); + } catch (UnauthorizedException $e) { + $message = $e->getMessage(); + } + + $this->assertStringEndsWith('Necessary roles or permissions are some-permission, some-role', $message); + } + protected function runMiddleware($middleware, $name, $guard = null) { try { From cdf73026b686ad41323c48b5f79812d01c1b6b2a Mon Sep 17 00:00:00 2001 From: Maarten Paauw Date: Thu, 13 Oct 2022 19:15:59 +0200 Subject: [PATCH 0554/1013] Autocomplete all Blade directives via Laravel Idea plugin --- docs/advanced-usage/phpstorm.md | 43 +++++++++++++++----- ide.json | 72 +++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 10 deletions(-) create mode 100644 ide.json diff --git a/docs/advanced-usage/phpstorm.md b/docs/advanced-usage/phpstorm.md index 205d6a0b3..23e1a7574 100644 --- a/docs/advanced-usage/phpstorm.md +++ b/docs/advanced-usage/phpstorm.md @@ -5,6 +5,9 @@ weight: 8 # Extending PhpStorm +> **Note** +> When using Laravel Idea plugin all directives are automatically added. + You may wish to extend PhpStorm to support Blade Directives of this package. 1. In PhpStorm, open Preferences, and navigate to **Languages and Frameworks -> PHP -> Blade** @@ -16,11 +19,17 @@ You may wish to extend PhpStorm to support Blade Directives of this package. **role** - has parameter = YES -- Prefix: `check() && auth()->user()->hasRole(` -- Suffix: `)); ?>` +- Prefix: `` -- +**elserole** + +- has parameter = YES +- Prefix: `` + **endrole** - has parameter = NO @@ -32,8 +41,8 @@ You may wish to extend PhpStorm to support Blade Directives of this package. **hasrole** - has parameter = YES -- Prefix: `check() && auth()->user()->hasRole(` -- Suffix: `)); ?>` +- Prefix: `` -- @@ -48,8 +57,8 @@ You may wish to extend PhpStorm to support Blade Directives of this package. **hasanyrole** - has parameter = YES -- Prefix: `check() && auth()->user()->hasAnyRole(` -- Suffix: `)); ?>` +- Prefix: `` -- @@ -64,8 +73,8 @@ You may wish to extend PhpStorm to support Blade Directives of this package. **hasallroles** - has parameter = YES -- Prefix: `check() && auth()->user()->hasAllRoles(` -- Suffix: `)); ?>` +- Prefix: `` -- @@ -80,8 +89,8 @@ You may wish to extend PhpStorm to support Blade Directives of this package. **unlessrole** - has parameter = YES -- Prefix: `check() && !auth()->user()->hasRole(` -- Suffix: `)); ?>` +- Prefix: `` -- @@ -92,3 +101,17 @@ You may wish to extend PhpStorm to support Blade Directives of this package. - Suffix: blank -- + +**hasexactroles** + +- has parameter = YES +- Prefix: `` + +-- + +**endhasexactroles** + +- has parameter = NO +- Prefix: blank +- Suffix: blank diff --git a/ide.json b/ide.json new file mode 100644 index 000000000..7afcd2a45 --- /dev/null +++ b/ide.json @@ -0,0 +1,72 @@ +{ + "$schema": "https://laravel-ide.com/schema/laravel-ide-v2.json", + "blade": { + "directives": [ + { + "name": "role", + "prefix": "" + }, + { + "name": "elserole", + "prefix": "" + }, + { + "name": "endrole", + "prefix": "", + "suffix": "" + }, + { + "name": "hasrole", + "prefix": "" + }, + { + "name": "endhasrole", + "prefix": "", + "suffix": "" + }, + { + "name": "hasanyrole", + "prefix": "" + }, + { + "name": "endhasanyrole", + "prefix": "", + "suffix": "" + }, + { + "name": "hasallroles", + "prefix": "" + }, + { + "name": "endhasallroles", + "prefix": "", + "suffix": "" + }, + { + "name": "unlessrole", + "prefix": "" + }, + { + "name": "endunlessrole", + "prefix": "", + "suffix": "" + }, + { + "name": "hasexactroles", + "prefix": "" + }, + { + "name": "endhasexactroles", + "prefix": "", + "suffix": "" + } + ] + } +} From 074382c9639e06c4722f5a180a99a584d281a22f Mon Sep 17 00:00:00 2001 From: drbyte Date: Sun, 23 Oct 2022 02:35:44 +0000 Subject: [PATCH 0555/1013] Update CHANGELOG --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index acdc7ed3c..58b36b5ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.5.15 - 2022-10-23 + +Autocomplete all Blade directives via Laravel Idea plugin + +### What's Changed + +- Autocomplete all Blade directives via Laravel Idea plugin by @maartenpaauw in https://github.com/spatie/laravel-permission/pull/2210 +- Add tests for display roles/permissions on UnauthorizedException by @erikn69 in https://github.com/spatie/laravel-permission/pull/2228 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.14...5.5.15 + ## 5.5.14 - 2022-10-21 FIXED BREAKING CHANGE. (Sorry about that!) @@ -501,6 +512,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -564,6 +576,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From e57a67b51a02cbfcd074ed68786897db1dbbf2fa Mon Sep 17 00:00:00 2001 From: Subhan Shamsoddini Date: Sun, 23 Oct 2022 06:19:28 +0330 Subject: [PATCH 0556/1013] optimize `for` loop in WildcardPermission (#2113) --- src/WildcardPermission.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/WildcardPermission.php b/src/WildcardPermission.php index 884b1c17c..f3bf631bd 100644 --- a/src/WildcardPermission.php +++ b/src/WildcardPermission.php @@ -47,8 +47,9 @@ public function implies($permission): bool $otherParts = $permission->getParts(); $i = 0; + $partsCount = $this->getParts()->count(); foreach ($otherParts as $otherPart) { - if ($this->getParts()->count() - 1 < $i) { + if ($partsCount - 1 < $i) { return true; } @@ -60,7 +61,7 @@ public function implies($permission): bool $i++; } - for ($i; $i < $this->parts->count(); $i++) { + for ($i; $i < $partsCount; $i++) { if (! $this->parts->get($i)->contains(static::WILDCARD_TOKEN)) { return false; } From e5d989cf4fb93e8b7b431ede417b96d6f776779e Mon Sep 17 00:00:00 2001 From: drbyte Date: Sun, 23 Oct 2022 02:49:55 +0000 Subject: [PATCH 0557/1013] Fix styling --- src/WildcardPermission.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WildcardPermission.php b/src/WildcardPermission.php index f3bf631bd..73797a854 100644 --- a/src/WildcardPermission.php +++ b/src/WildcardPermission.php @@ -47,7 +47,7 @@ public function implies($permission): bool $otherParts = $permission->getParts(); $i = 0; - $partsCount = $this->getParts()->count(); + $partsCount = $this->getParts()->count(); foreach ($otherParts as $otherPart) { if ($partsCount - 1 < $i) { return true; From 15b3e2c7c8c6ed508ed873dc91173ae6a9ab985b Mon Sep 17 00:00:00 2001 From: drbyte Date: Sun, 23 Oct 2022 02:50:10 +0000 Subject: [PATCH 0558/1013] Update CHANGELOG --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58b36b5ea..d1c3f597d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.5.16 - 2022-10-23 + +### What's Changed + +- optimize `for` loop in WildcardPermission by @SubhanSh in https://github.com/spatie/laravel-permission/pull/2113 + +### New Contributors + +- @SubhanSh made their first contribution in https://github.com/spatie/laravel-permission/pull/2113 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.15...5.5.16 + ## 5.5.15 - 2022-10-23 Autocomplete all Blade directives via Laravel Idea plugin @@ -513,6 +525,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -577,6 +590,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From 2ec9befb33d2110d9d354bb65a41b3b962280eb1 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 22 Oct 2022 22:55:43 -0400 Subject: [PATCH 0559/1013] Remove stray whitespace --- CHANGELOG.md | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1c3f597d..44b30be50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -509,23 +509,6 @@ The following changes are not "breaking", but worth making the updates to your a - app()['cache']->forget('spatie.permission.cache'); + $this->app->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions(); - - - - - - - - - - - - - - - - - ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -574,23 +557,6 @@ The following changes are not "breaking", but worth making the updates to your a // user hasRole 'roleB' but not 'roleA' @endrole - - - - - - - - - - - - - - - - - ``` ## 2.19.1 - 2018-09-14 From 1176b9f7a1121a7f18cdee7d2b07875c6e83ec59 Mon Sep 17 00:00:00 2001 From: Julio Motol Date: Wed, 26 Oct 2022 23:40:54 +0800 Subject: [PATCH 0560/1013] =?UTF-8?q?Revert=20"=F0=9F=90=9B=20Override=20`?= =?UTF-8?q?getAllPermissions()`=20in=20`Role`"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 4807005e6c299f0e0f8165c18a4c23d0d357d8d4. --- src/Models/Role.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/Models/Role.php b/src/Models/Role.php index b26a9af38..bb4173d82 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -4,7 +4,6 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; -use Illuminate\Support\Collection; use Spatie\Permission\Contracts\Role as RoleContract; use Spatie\Permission\Exceptions\GuardDoesNotMatch; use Spatie\Permission\Exceptions\RoleAlreadyExists; @@ -196,12 +195,4 @@ public function hasPermissionTo($permission): bool return $this->permissions->contains($permission->getKeyName(), $permission->getKey()); } - - /** - * Return all the permissions the model has, both directly and via roles. - */ - public function getAllPermissions(): Collection - { - return $this->permissions->sort()->values(); - } } From 4835ff7941c1f918eba95bed1ddc98fa6f1c6628 Mon Sep 17 00:00:00 2001 From: Julio Motol Date: Wed, 26 Oct 2022 23:41:57 +0800 Subject: [PATCH 0561/1013] =?UTF-8?q?=F0=9F=90=9B=20Check=20for=20role=20r?= =?UTF-8?q?elationship=20method=20before=20merging=20permissions=20from=20?= =?UTF-8?q?roles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julio Motol --- src/Traits/HasPermissions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 7e89ec7a2..69ccccd9d 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -310,7 +310,7 @@ public function getAllPermissions(): Collection /** @var Collection $permissions */ $permissions = $this->permissions; - if ($this->roles) { + if (method_exists($this, 'roles')) { $permissions = $permissions->merge($this->getPermissionsViaRoles()); } From c19742a031ccbaab2eae0363270b944ea26e17ad Mon Sep 17 00:00:00 2001 From: Ching Cheng Kang Date: Sat, 5 Nov 2022 15:41:49 +0800 Subject: [PATCH 0562/1013] Fix broken Link that link to freek's blog post --- docs/basic-usage/super-admin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/super-admin.md b/docs/basic-usage/super-admin.md index d48ad254f..426bbc036 100644 --- a/docs/basic-usage/super-admin.md +++ b/docs/basic-usage/super-admin.md @@ -38,7 +38,7 @@ Jeffrey Way explains the concept of a super-admin (and a model owner, and model Alternatively you might want to move the Super Admin check to the `Gate::after` phase instead, particularly if your Super Admin shouldn't be allowed to do things your app doesn't want "anyone" to do, such as writing more than 1 review, or bypassing unsubscribe rules, etc. -The following code snippet is inspired from [Freek's blog article](https://murze.be/when-to-use-gateafter-in-laravel) where this topic is discussed further. +The following code snippet is inspired from [Freek's blog article](https://freek.dev/1325-when-to-use-gateafter-in-laravel) where this topic is discussed further. ```php // somewhere in a service provider From 61692e9a7e5f8427ed03a9f40d9f76a0a02b5b0f Mon Sep 17 00:00:00 2001 From: Jorin Vermeulen <4212335+xorinzor@users.noreply.github.com> Date: Sun, 6 Nov 2022 11:21:14 +0100 Subject: [PATCH 0563/1013] Update role-permissions.md the `syncPermissions` method on the `Role` object was missing from this page --- docs/basic-usage/role-permissions.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/basic-usage/role-permissions.md b/docs/basic-usage/role-permissions.md index 1a24fde8c..476610957 100644 --- a/docs/basic-usage/role-permissions.md +++ b/docs/basic-usage/role-permissions.md @@ -84,6 +84,12 @@ A permission can be revoked from a role: $role->revokePermissionTo('edit articles'); ``` +Or revoke & add new permissions in one go: + +```php +$role->syncPermissions(['edit articles', 'delete articles']); +``` + The `givePermissionTo` and `revokePermissionTo` functions can accept a string or a `Spatie\Permission\Models\Permission` object. From a1e4463d7a074565182915f472b10445d18c93e2 Mon Sep 17 00:00:00 2001 From: Mohammad ALTAWEEL Date: Fri, 18 Nov 2022 07:45:37 +0800 Subject: [PATCH 0564/1013] Don't throw an exception when checking permission --- src/Traits/HasPermissions.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 7e89ec7a2..d55450bfc 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -249,14 +249,13 @@ public function hasAnyPermission(...$permissions): bool * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions * * @return bool - * @throws \Exception */ public function hasAllPermissions(...$permissions): bool { $permissions = collect($permissions)->flatten(); foreach ($permissions as $permission) { - if (! $this->hasPermissionTo($permission)) { + if (! $this->checkPermissionTo($permission)) { return false; } } @@ -486,6 +485,7 @@ public function forgetCachedPermissions() /** * Check if the model has All of the requested Direct permissions. + * * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions * @return bool */ @@ -504,6 +504,7 @@ public function hasAllDirectPermissions(...$permissions): bool /** * Check if the model has Any of the requested Direct permissions. + * * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions * @return bool */ From baa6d35a5d64f522e39f4652c00cd53c4219d3fa Mon Sep 17 00:00:00 2001 From: drbyte Date: Sat, 19 Nov 2022 02:10:48 +0000 Subject: [PATCH 0565/1013] Update CHANGELOG --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44b30be50..d5ee15a1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.6.0 - 2022-11-19 + +### What's Changed + +- No longer throws an exception when checking `hasAllPermissions()` if the permission name does not exist by @mtawil in https://github.com/spatie/laravel-permission/pull/2248 + +### Doc Updates + +- [Docs] Add syncPermissions() in role-permissions.md by @xorinzor in https://github.com/spatie/laravel-permission/pull/2235 +- [Docs] Fix broken Link that link to freek's blog post by @chengkangzai in https://github.com/spatie/laravel-permission/pull/2234 + +### New Contributors + +- @xorinzor made their first contribution in https://github.com/spatie/laravel-permission/pull/2235 +- @chengkangzai made their first contribution in https://github.com/spatie/laravel-permission/pull/2234 +- @mtawil made their first contribution in https://github.com/spatie/laravel-permission/pull/2248 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.16...5.6.0 + ## 5.5.16 - 2022-10-23 ### What's Changed @@ -509,6 +528,7 @@ The following changes are not "breaking", but worth making the updates to your a - app()['cache']->forget('spatie.permission.cache'); + $this->app->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions(); + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -557,6 +577,7 @@ The following changes are not "breaking", but worth making the updates to your a // user hasRole 'roleB' but not 'roleA' @endrole + ``` ## 2.19.1 - 2018-09-14 From 4af34b00bc753aa953f57e1798873fb3a9324c1d Mon Sep 17 00:00:00 2001 From: drbyte Date: Wed, 23 Nov 2022 07:04:54 +0000 Subject: [PATCH 0566/1013] Update CHANGELOG --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5ee15a1e..b12a5c516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.7.0 - 2022-11-23 + +### What's Changed + +- [Bugfix] Avoid checking permissions-via-roles on `Role` model (ref `Model::preventAccessingMissingAttributes()`) by @juliomotol in https://github.com/spatie/laravel-permission/pull/2227 + +### New Contributors + +- @juliomotol made their first contribution in https://github.com/spatie/laravel-permission/pull/2227 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.6.0...5.7.0 + ## 5.6.0 - 2022-11-19 ### What's Changed @@ -529,6 +541,7 @@ The following changes are not "breaking", but worth making the updates to your a + $this->app->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions(); + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -578,6 +591,7 @@ The following changes are not "breaking", but worth making the updates to your a @endrole + ``` ## 2.19.1 - 2018-09-14 From 70d5b0ca8588f6985c2ae585462244d5bddf9b92 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Wed, 23 Nov 2022 10:42:34 -0500 Subject: [PATCH 0567/1013] Custom wildcard verification support --- src/Contracts/Wildcard.php | 13 +++++ ...ildcardPermissionNotImplementsContract.php | 13 +++++ src/Traits/HasPermissions.php | 30 +++++++++++- src/WildcardPermission.php | 3 +- tests/WildcardHasPermissionsTest.php | 49 +++++++++++++++++++ tests/WildcardPermission.php | 17 +++++++ 6 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 src/Contracts/Wildcard.php create mode 100644 src/Exceptions/WildcardPermissionNotImplementsContract.php create mode 100644 tests/WildcardPermission.php diff --git a/src/Contracts/Wildcard.php b/src/Contracts/Wildcard.php new file mode 100644 index 000000000..073a99dd1 --- /dev/null +++ b/src/Contracts/Wildcard.php @@ -0,0 +1,13 @@ +permissionClass; } + protected function getWildcardClass() + { + if (! is_null($this->wildcardClass)) { + return $this->wildcardClass; + } + + $this->wildcardClass = false; + + if (config('permission.enable_wildcard_permission', false)) { + $this->wildcardClass = config('permission.wildcard_permission', WildcardPermission::class); + + if (! is_subclass_of($this->wildcardClass, Wildcard::class)) { + throw WildcardPermissionNotImplementsContract::create(); + } + } + + return $this->wildcardClass; + } + /** * A model may have multiple direct permissions. */ @@ -158,7 +182,7 @@ public function filterPermission($permission, $guardName = null) */ public function hasPermissionTo($permission, $guardName = null): bool { - if (config('permission.enable_wildcard_permission', false)) { + if ($this->getWildcardClass()) { return $this->hasWildcardPermission($permission, $guardName); } @@ -191,12 +215,14 @@ protected function hasWildcardPermission($permission, $guardName = null): bool throw WildcardPermissionInvalidArgument::create(); } + $WildcardPermissionClass = $this->getWildcardClass(); + foreach ($this->getAllPermissions() as $userPermission) { if ($guardName !== $userPermission->guard_name) { continue; } - $userPermission = new WildcardPermission($userPermission->name); + $userPermission = new $WildcardPermissionClass($userPermission->name); if ($userPermission->implies($permission)) { return true; diff --git a/src/WildcardPermission.php b/src/WildcardPermission.php index 73797a854..3ff0ba81a 100644 --- a/src/WildcardPermission.php +++ b/src/WildcardPermission.php @@ -3,9 +3,10 @@ namespace Spatie\Permission; use Illuminate\Support\Collection; +use Spatie\Permission\Contracts\Wildcard; use Spatie\Permission\Exceptions\WildcardPermissionNotProperlyFormatted; -class WildcardPermission +class WildcardPermission implements Wildcard { /** @var string */ public const WILDCARD_TOKEN = '*'; diff --git a/tests/WildcardHasPermissionsTest.php b/tests/WildcardHasPermissionsTest.php index 5ae2e3c79..3a952c6c7 100644 --- a/tests/WildcardHasPermissionsTest.php +++ b/tests/WildcardHasPermissionsTest.php @@ -52,6 +52,55 @@ public function it_can_check_wildcard_permissions_via_roles() $this->assertFalse($user1->hasPermissionTo('projects.list')); } + /** @test */ + public function it_can_check_custom_wildcard_permission() + { + app('config')->set('permission.enable_wildcard_permission', true); + app('config')->set('permission.wildcard_permission', WildcardPermission::class); + + $user1 = User::create(['email' => 'user1@test.com']); + + $permission1 = Permission::create(['name' => 'articles:edit;view;create']); + $permission2 = Permission::create(['name' => 'news:@']); + $permission3 = Permission::create(['name' => 'posts:@']); + + $user1->givePermissionTo([$permission1, $permission2, $permission3]); + + $this->assertTrue($user1->hasPermissionTo('posts:create')); + $this->assertTrue($user1->hasPermissionTo('posts:create:123')); + $this->assertTrue($user1->hasPermissionTo('posts:@')); + $this->assertTrue($user1->hasPermissionTo('articles:view')); + $this->assertFalse($user1->hasPermissionTo('posts.*')); + $this->assertFalse($user1->hasPermissionTo('articles.view')); + $this->assertFalse($user1->hasPermissionTo('projects:view')); + } + + /** @test */ + public function it_can_check_custom_wildcard_permissions_via_roles() + { + app('config')->set('permission.enable_wildcard_permission', true); + app('config')->set('permission.wildcard_permission', WildcardPermission::class); + + $user1 = User::create(['email' => 'user1@test.com']); + + $user1->assignRole('testRole'); + + $permission1 = Permission::create(['name' => 'articles;projects:edit;view;create']); + $permission2 = Permission::create(['name' => 'news:@:456']); + $permission3 = Permission::create(['name' => 'posts']); + + $this->testUserRole->givePermissionTo([$permission1, $permission2, $permission3]); + + $this->assertTrue($user1->hasPermissionTo('posts:create')); + $this->assertTrue($user1->hasPermissionTo('news:create:456')); + $this->assertTrue($user1->hasPermissionTo('projects:create')); + $this->assertTrue($user1->hasPermissionTo('articles:view')); + $this->assertFalse($user1->hasPermissionTo('news.create.456')); + $this->assertFalse($user1->hasPermissionTo('projects.create')); + $this->assertFalse($user1->hasPermissionTo('articles:list')); + $this->assertFalse($user1->hasPermissionTo('projects:list')); + } + /** @test */ public function it_can_check_non_wildcard_permissions() { diff --git a/tests/WildcardPermission.php b/tests/WildcardPermission.php new file mode 100644 index 000000000..9b1dfe0d6 --- /dev/null +++ b/tests/WildcardPermission.php @@ -0,0 +1,17 @@ + Date: Wed, 23 Nov 2022 21:04:45 +0330 Subject: [PATCH 0568/1013] Name changed. Name changed. --- docs/advanced-usage/ui-options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-usage/ui-options.md b/docs/advanced-usage/ui-options.md index e8029ff22..86151cfed 100644 --- a/docs/advanced-usage/ui-options.md +++ b/docs/advanced-usage/ui-options.md @@ -20,4 +20,4 @@ The package doesn't come with any screens out of the box, you should build that - [Generating UI boilerplate using InfyOm](https://youtu.be/hlGu2pa1bdU) video tutorial by [Shailesh](https://github.com/shailesh-ladumor) -- [LiveWire Base Admin Panel](https://github.com/alighasemzadeh/bap) User management by [AliGhasemzadeh](https://github.com/alighasemzadeh) +- [LiveWire Base Admin Panel](https://github.com/aliqasemzadeh/bap) User management by [AliQasemzadeh](https://github.com/aliqasemzadeh) From f2fc5a8fe848aff3e945b32c8f00a7529ff2eead Mon Sep 17 00:00:00 2001 From: Patrick Organ Date: Mon, 5 Dec 2022 04:37:00 -0500 Subject: [PATCH 0569/1013] Normalize composer.json (#2259) * wip * wip --- composer.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/composer.json b/composer.json index c1a09b24d..d4f66ca5c 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,7 @@ { "name": "spatie/laravel-permission", "description": "Permission handling for Laravel 6.0 and up", + "license": "MIT", "keywords": [ "spatie", "laravel", @@ -11,8 +12,6 @@ "rbac", "security" ], - "homepage": "https://github.com/spatie/laravel-permission", - "license": "MIT", "authors": [ { "name": "Freek Van der Herten", @@ -21,8 +20,9 @@ "role": "Developer" } ], + "homepage": "https://github.com/spatie/laravel-permission", "require": { - "php" : "^7.3|^8.0|^8.1", + "php": "^7.3|^8.0|^8.1", "illuminate/auth": "^7.0|^8.0|^9.0", "illuminate/container": "^7.0|^8.0|^9.0", "illuminate/contracts": "^7.0|^8.0|^9.0", @@ -33,6 +33,8 @@ "phpunit/phpunit": "^9.4", "predis/predis": "^1.1" }, + "minimum-stability": "dev", + "prefer-stable": true, "autoload": { "psr-4": { "Spatie\\Permission\\": "src" @@ -46,23 +48,21 @@ "Spatie\\Permission\\Test\\": "tests" } }, + "config": { + "sort-packages": true + }, "extra": { + "branch-alias": { + "dev-main": "5.x-dev", + "dev-master": "5.x-dev" + }, "laravel": { "providers": [ "Spatie\\Permission\\PermissionServiceProvider" ] - }, - "branch-alias": { - "dev-main": "5.x-dev", - "dev-master": "5.x-dev" } }, "scripts": { "test": "phpunit" - }, - "config": { - "sort-packages": true - }, - "minimum-stability": "dev", - "prefer-stable": true + } } From 4011bccc8d9f1575a1352bd648a42f3326d129d4 Mon Sep 17 00:00:00 2001 From: Patrick Organ Date: Mon, 5 Dec 2022 04:37:16 -0500 Subject: [PATCH 0570/1013] Add Dependabot Automation (#2257) * add dependabot configuration * add workflow to auto-merge dependabot PRs --- .github/dependabot.yml | 8 +++++ .github/workflows/dependabot-auto-merge.yml | 40 +++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/dependabot-auto-merge.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..a76dd83f9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 + +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 000000000..78e5cd252 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,40 @@ +name: dependabot-auto-merge +on: pull_request_target + +permissions: + pull-requests: write + contents: write + +jobs: + dependabot: + runs-on: ubuntu-latest + if: ${{ github.actor == 'dependabot[bot]' }} + steps: + + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v1.3.5 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + compat-lookup: true + + - name: Auto-merge Dependabot PRs for semver-minor updates + if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + + - name: Auto-merge Dependabot PRs for semver-patch updates + if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + + - name: Auto-merge Dependabot PRs for Action major versions when compatibility is higher than 90% + if: ${{steps.metadata.outputs.package-ecosystem == 'github_actions' && steps.metadata.outputs.update-type == 'version-update:semver-major' && steps.metadata.outputs.compatibility-score >= 90}} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GH_TOKEN: ${{secrets.GITHUB_TOKEN}} From 58946d5e28c82a1fb54a465c2819a38e1bbad092 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Dec 2022 09:37:45 +0000 Subject: [PATCH 0571/1013] Bump actions/checkout from 2 to 3 Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/php-cs-fixer.yml | 2 +- .github/workflows/run-tests-L7.yml | 2 +- .github/workflows/run-tests-L8.yml | 2 +- .github/workflows/update-changelog.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml index b3c985609..d54c70741 100644 --- a/.github/workflows/php-cs-fixer.yml +++ b/.github/workflows/php-cs-fixer.yml @@ -8,7 +8,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Fix style uses: docker://oskarstark/php-cs-fixer-ga diff --git a/.github/workflows/run-tests-L7.yml b/.github/workflows/run-tests-L7.yml index 4f347b351..6849039e9 100644 --- a/.github/workflows/run-tests-L7.yml +++ b/.github/workflows/run-tests-L7.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/run-tests-L8.yml b/.github/workflows/run-tests-L8.yml index 11c4fcbae..d64ef9545 100644 --- a/.github/workflows/run-tests-L8.yml +++ b/.github/workflows/run-tests-L8.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index fa56639f2..b20f3b6fb 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -10,7 +10,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: ref: main From 50223e6aa9492a028a4f901b957fe463be1134df Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Dec 2022 09:37:48 +0000 Subject: [PATCH 0572/1013] Bump stefanzweifel/git-auto-commit-action from 2.3.0 to 4.16.0 Bumps [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action) from 2.3.0 to 4.16.0. - [Release notes](https://github.com/stefanzweifel/git-auto-commit-action/releases) - [Changelog](https://github.com/stefanzweifel/git-auto-commit-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/stefanzweifel/git-auto-commit-action/compare/v2.3.0...v4.16.0) --- updated-dependencies: - dependency-name: stefanzweifel/git-auto-commit-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/php-cs-fixer.yml | 2 +- .github/workflows/update-changelog.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml index b3c985609..1db1528e5 100644 --- a/.github/workflows/php-cs-fixer.yml +++ b/.github/workflows/php-cs-fixer.yml @@ -21,7 +21,7 @@ jobs: id: extract_branch - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v2.3.0 + uses: stefanzweifel/git-auto-commit-action@v4.16.0 with: commit_message: Fix styling branch: ${{ steps.extract_branch.outputs.branch }} diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index fa56639f2..63b26c41d 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -21,7 +21,7 @@ jobs: release-notes: ${{ github.event.release.body }} - name: Commit updated CHANGELOG - uses: stefanzweifel/git-auto-commit-action@v4 + uses: stefanzweifel/git-auto-commit-action@v4.16.0 with: branch: main commit_message: Update CHANGELOG From 1bd0355baeccb6cf86b8d52885f4f2ab47f3978e Mon Sep 17 00:00:00 2001 From: Patrick Organ Date: Fri, 9 Dec 2022 08:00:42 -0500 Subject: [PATCH 0573/1013] Add Laravel Pint Support (#2269) * add pint workflow * remove php-cs-fixer workflow * remove php-cs-fixer config * Fix styling --- .../workflows/fix-php-code-style-issues.yml | 24 +++++++ .github/workflows/php-cs-fixer.yml | 29 --------- .php_cs.dist.php | 40 ------------ src/Commands/CreateRole.php | 4 +- src/Commands/Show.php | 2 +- src/Commands/UpgradeForTeams.php | 18 +++--- src/Contracts/Permission.php | 19 +++--- src/Contracts/Role.php | 18 +++--- src/Guard.php | 6 +- src/Models/Permission.php | 27 ++++---- src/Models/Role.php | 18 +++--- src/PermissionRegistrar.php | 14 ++--- src/Traits/HasPermissions.php | 63 ++++++++----------- src/Traits/HasRoles.php | 20 +++--- src/WildcardPermission.php | 10 ++- src/helpers.php | 6 +- tests/CacheTest.php | 3 + tests/HasPermissionsTest.php | 26 ++++---- tests/Permission.php | 4 +- tests/Role.php | 4 +- tests/RuntimeRole.php | 4 +- tests/TestCase.php | 12 ++-- tests/TestHelper.php | 5 +- tests/WildcardMiddlewareTest.php | 2 + 24 files changed, 150 insertions(+), 228 deletions(-) create mode 100644 .github/workflows/fix-php-code-style-issues.yml delete mode 100644 .github/workflows/php-cs-fixer.yml delete mode 100644 .php_cs.dist.php diff --git a/.github/workflows/fix-php-code-style-issues.yml b/.github/workflows/fix-php-code-style-issues.yml new file mode 100644 index 000000000..150750cbb --- /dev/null +++ b/.github/workflows/fix-php-code-style-issues.yml @@ -0,0 +1,24 @@ +name: Fix PHP code style issues + +on: + push: + paths: + - '**.php' + +jobs: + php-code-styling: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + + - name: Fix PHP code style issues + uses: aglipanci/laravel-pint-action@1.0.0 + + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: Fix styling diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml deleted file mode 100644 index dc1e82abf..000000000 --- a/.github/workflows/php-cs-fixer.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Check & fix styling - -on: [push] - -jobs: - style: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Fix style - uses: docker://oskarstark/php-cs-fixer-ga - with: - args: --config=.php_cs.dist.php --allow-risky=yes - - - name: Extract branch name - shell: bash - run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" - id: extract_branch - - - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v4.16.0 - with: - commit_message: Fix styling - branch: ${{ steps.extract_branch.outputs.branch }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.php_cs.dist.php b/.php_cs.dist.php deleted file mode 100644 index e89f88628..000000000 --- a/.php_cs.dist.php +++ /dev/null @@ -1,40 +0,0 @@ -in([ - __DIR__ . '/src', - __DIR__ . '/tests', - ]) - ->name('*.php') - ->notName('*.blade.php') - ->ignoreDotFiles(true) - ->ignoreVCS(true); - -return (new PhpCsFixer\Config()) - ->setRules([ - '@PSR12' => true, - 'array_syntax' => ['syntax' => 'short'], - 'ordered_imports' => ['sort_algorithm' => 'alpha'], - 'no_unused_imports' => true, - 'not_operator_with_successor_space' => true, - 'trailing_comma_in_multiline' => true, - 'phpdoc_scalar' => true, - 'unary_operator_spaces' => true, - 'binary_operator_spaces' => true, - 'blank_line_before_statement' => [ - 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], - ], - 'phpdoc_single_line_var_spacing' => true, - 'phpdoc_var_without_name' => true, - 'class_attributes_separation' => [ - 'elements' => [ - 'method' => 'one', - ], - ], - 'method_argument_space' => [ - 'on_multiline' => 'ensure_fully_multiline', - 'keep_multiple_spaces_after_comma' => true, - ], - 'single_trait_insert_per_statement' => true, - ]) - ->setFinder($finder); diff --git a/src/Commands/CreateRole.php b/src/Commands/CreateRole.php index e4701f014..f84385b09 100644 --- a/src/Commands/CreateRole.php +++ b/src/Commands/CreateRole.php @@ -25,7 +25,7 @@ public function handle() setPermissionsTeamId($this->option('team-id') ?: null); if (! PermissionRegistrar::$teams && $this->option('team-id')) { - $this->warn("Teams feature disabled, argument --team-id has no effect. Either enable it in permissions config file or remove --team-id parameter"); + $this->warn('Teams feature disabled, argument --team-id has no effect. Either enable it in permissions config file or remove --team-id parameter'); return; } @@ -44,7 +44,7 @@ public function handle() } /** - * @param array|null|string $string + * @param array|null|string $string */ protected function makePermissions($string = null) { diff --git a/src/Commands/Show.php b/src/Commands/Show.php index fbd61967a..bcbe07381 100644 --- a/src/Commands/Show.php +++ b/src/Commands/Show.php @@ -40,7 +40,7 @@ public function handle() $q->orderBy($team_key); }) ->orderBy('name')->get()->mapWithKeys(function ($role) use ($team_key) { - return [$role->name.'_'.($role->$team_key ?: '') => ['permissions' => $role->permissions->pluck('id'), $team_key => $role->$team_key ]]; + return [$role->name.'_'.($role->$team_key ?: '') => ['permissions' => $role->permissions->pluck('id'), $team_key => $role->$team_key]]; }); $permissions = $permissionClass::whereGuardName($guard)->orderBy('name')->pluck('name', 'id'); diff --git a/src/Commands/UpgradeForTeams.php b/src/Commands/UpgradeForTeams.php index 9923ca19d..210ca4407 100644 --- a/src/Commands/UpgradeForTeams.php +++ b/src/Commands/UpgradeForTeams.php @@ -23,7 +23,7 @@ public function handle() } $this->line(''); - $this->info("The teams feature setup is going to add a migration and a model"); + $this->info('The teams feature setup is going to add a migration and a model'); $existingMigrations = $this->alreadyExistingMigrations(); @@ -35,20 +35,20 @@ public function handle() $this->line(''); - if (! $this->confirm("Proceed with the migration creation?", "yes")) { + if (! $this->confirm('Proceed with the migration creation?', 'yes')) { return; } $this->line(''); - $this->line("Creating migration"); + $this->line('Creating migration'); if ($this->createMigration()) { - $this->info("Migration created successfully."); + $this->info('Migration created successfully.'); } else { $this->error( "Couldn't create migration.\n". - "Check the write permissions within the database/migrations directory." + 'Check the write permissions within the database/migrations directory.' ); } @@ -78,7 +78,7 @@ protected function createMigration() * Build a warning regarding possible duplication * due to already existing migrations. * - * @param array $existingMigrations + * @param array $existingMigrations * @return string */ protected function getExistingMigrationsWarning(array $existingMigrations) @@ -89,8 +89,8 @@ protected function getExistingMigrationsWarning(array $existingMigrations) $base = "Setup teams migration already exists.\nFollowing file was found: "; } - return $base . array_reduce($existingMigrations, function ($carry, $fileName) { - return $carry . "\n - " . $fileName; + return $base.array_reduce($existingMigrations, function ($carry, $fileName) { + return $carry."\n - ".$fileName; }); } @@ -115,7 +115,7 @@ protected function alreadyExistingMigrations() * The date parameter is optional for ability * to provide a custom value or a wildcard. * - * @param string|null $date + * @param string|null $date * @return string */ protected function getMigrationPath($date = null) diff --git a/src/Contracts/Permission.php b/src/Contracts/Permission.php index 344173901..91e0162a4 100644 --- a/src/Contracts/Permission.php +++ b/src/Contracts/Permission.php @@ -16,33 +16,30 @@ public function roles(): BelongsToMany; /** * Find a permission by its name. * - * @param string $name - * @param string|null $guardName + * @param string $name + * @param string|null $guardName + * @return Permission * * @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist - * - * @return Permission */ public static function findByName(string $name, $guardName): self; /** * Find a permission by its id. * - * @param int $id - * @param string|null $guardName + * @param int $id + * @param string|null $guardName + * @return Permission * * @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist - * - * @return Permission */ public static function findById(int $id, $guardName): self; /** * Find or Create a permission by its name and guard name. * - * @param string $name - * @param string|null $guardName - * + * @param string $name + * @param string|null $guardName * @return Permission */ public static function findOrCreate(string $name, $guardName): self; diff --git a/src/Contracts/Role.php b/src/Contracts/Role.php index 1292cdb42..28f3d4308 100644 --- a/src/Contracts/Role.php +++ b/src/Contracts/Role.php @@ -16,9 +16,8 @@ public function permissions(): BelongsToMany; /** * Find a role by its name and guard name. * - * @param string $name - * @param string|null $guardName - * + * @param string $name + * @param string|null $guardName * @return \Spatie\Permission\Contracts\Role * * @throws \Spatie\Permission\Exceptions\RoleDoesNotExist @@ -28,9 +27,8 @@ public static function findByName(string $name, $guardName): self; /** * Find a role by its id and guard name. * - * @param int $id - * @param string|null $guardName - * + * @param int $id + * @param string|null $guardName * @return \Spatie\Permission\Contracts\Role * * @throws \Spatie\Permission\Exceptions\RoleDoesNotExist @@ -40,9 +38,8 @@ public static function findById(int $id, $guardName): self; /** * Find or create a role by its name and guard name. * - * @param string $name - * @param string|null $guardName - * + * @param string $name + * @param string|null $guardName * @return \Spatie\Permission\Contracts\Role */ public static function findOrCreate(string $name, $guardName): self; @@ -50,8 +47,7 @@ public static function findOrCreate(string $name, $guardName): self; /** * Determine if the user may perform the given permission. * - * @param string|\Spatie\Permission\Contracts\Permission $permission - * + * @param string|\Spatie\Permission\Contracts\Permission $permission * @return bool */ public function hasPermissionTo($permission): bool; diff --git a/src/Guard.php b/src/Guard.php index c0630392b..395d4cfec 100644 --- a/src/Guard.php +++ b/src/Guard.php @@ -11,7 +11,7 @@ class Guard * Return a collection of guard names suitable for the $model, * as indicated by the presence of a $guard_name property or a guardName() method on the model. * - * @param string|Model $model model class object or name + * @param string|Model $model model class object or name * @return Collection */ public static function getNames($model): Collection @@ -46,7 +46,7 @@ public static function getNames($model): Collection * - keys() gives just the names of the matched guards * - return collection of guard names * - * @param string $class + * @param string $class * @return Collection */ protected static function getConfigAuthGuards(string $class): Collection @@ -68,7 +68,7 @@ protected static function getConfigAuthGuards(string $class): Collection /** * Lookup a guard name relevant for the $class model and the current user. * - * @param string|Model $class model class object or name + * @param string|Model $class model class object or name * @return string guard name */ public static function getDefaultName($class): string diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 1a044e694..10f9dd174 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -77,12 +77,11 @@ public function users(): BelongsToMany /** * Find a permission by its name (and optionally guardName). * - * @param string $name - * @param string|null $guardName + * @param string $name + * @param string|null $guardName + * @return \Spatie\Permission\Contracts\Permission * * @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist - * - * @return \Spatie\Permission\Contracts\Permission */ public static function findByName(string $name, $guardName = null): PermissionContract { @@ -98,12 +97,11 @@ public static function findByName(string $name, $guardName = null): PermissionCo /** * Find a permission by its id (and optionally guardName). * - * @param int $id - * @param string|null $guardName + * @param int $id + * @param string|null $guardName + * @return \Spatie\Permission\Contracts\Permission * * @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist - * - * @return \Spatie\Permission\Contracts\Permission */ public static function findById(int $id, $guardName = null): PermissionContract { @@ -120,9 +118,8 @@ public static function findById(int $id, $guardName = null): PermissionContract /** * Find or create permission by its name (and optionally guardName). * - * @param string $name - * @param string|null $guardName - * + * @param string $name + * @param string|null $guardName * @return \Spatie\Permission\Contracts\Permission */ public static function findOrCreate(string $name, $guardName = null): PermissionContract @@ -140,9 +137,8 @@ public static function findOrCreate(string $name, $guardName = null): Permission /** * Get the current cached permissions. * - * @param array $params - * @param bool $onlyOne - * + * @param array $params + * @param bool $onlyOne * @return \Illuminate\Database\Eloquent\Collection */ protected static function getPermissions(array $params = [], bool $onlyOne = false): Collection @@ -155,8 +151,7 @@ protected static function getPermissions(array $params = [], bool $onlyOne = fal /** * Get the current cached first permission. * - * @param array $params - * + * @param array $params * @return \Spatie\Permission\Contracts\Permission */ protected static function getPermission(array $params = []): ?PermissionContract diff --git a/src/Models/Role.php b/src/Models/Role.php index bb4173d82..6628f9165 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -83,9 +83,8 @@ public function users(): BelongsToMany /** * Find a role by its name and guard name. * - * @param string $name - * @param string|null $guardName - * + * @param string $name + * @param string|null $guardName * @return \Spatie\Permission\Contracts\Role|\Spatie\Permission\Models\Role * * @throws \Spatie\Permission\Exceptions\RoleDoesNotExist @@ -106,9 +105,8 @@ public static function findByName(string $name, $guardName = null): RoleContract /** * Find a role by its id (and optionally guardName). * - * @param int $id - * @param string|null $guardName - * + * @param int $id + * @param string|null $guardName * @return \Spatie\Permission\Contracts\Role|\Spatie\Permission\Models\Role */ public static function findById(int $id, $guardName = null): RoleContract @@ -127,9 +125,8 @@ public static function findById(int $id, $guardName = null): RoleContract /** * Find or create role by its name (and optionally guardName). * - * @param string $name - * @param string|null $guardName - * + * @param string $name + * @param string|null $guardName * @return \Spatie\Permission\Contracts\Role|\Spatie\Permission\Models\Role */ public static function findOrCreate(string $name, $guardName = null): RoleContract @@ -167,8 +164,7 @@ protected static function findByParam(array $params = []) /** * Determine if the user may perform the given permission. * - * @param string|Permission $permission - * + * @param string|Permission $permission * @return bool * * @throws \Spatie\Permission\Exceptions\GuardDoesNotMatch diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index e886c4961..ca2bf66eb 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -61,7 +61,7 @@ class PermissionRegistrar /** * PermissionRegistrar constructor. * - * @param \Illuminate\Cache\CacheManager $cacheManager + * @param \Illuminate\Cache\CacheManager $cacheManager */ public function __construct(CacheManager $cacheManager) { @@ -109,7 +109,7 @@ protected function getCacheStoreFromConfig(): Repository /** * Set the team id for teams/groups support, this id is used when querying permissions/roles * - * @param int|string|\Illuminate\Database\Eloquent\Model $id + * @param int|string|\Illuminate\Database\Eloquent\Model $id */ public function setPermissionsTeamId($id) { @@ -120,7 +120,6 @@ public function setPermissionsTeamId($id) } /** - * * @return int|string */ public function getPermissionsTeamId() @@ -199,9 +198,8 @@ private function loadPermissions() /** * Get the permissions based on the passed params. * - * @param array $params - * @param bool $onlyOne - * + * @param array $params + * @param bool $onlyOne * @return \Illuminate\Database\Eloquent\Collection */ public function getPermissions(array $params = [], bool $onlyOne = false): Collection @@ -259,7 +257,7 @@ public function getRoleClass(): Role public function setRoleClass($roleClass) { $this->roleClass = $roleClass; - config()->set('permission.models.role', $roleClass); + config()->set('permission.models.role', $roleClass); app()->bind(Role::class, $roleClass); return $this; @@ -310,7 +308,7 @@ private function aliasModelFields($newKeys = []): void */ private function getSerializedPermissionsForCache() { - $this->except = config('permission.cache.column_names_except', ['created_at','updated_at', 'deleted_at']); + $this->except = config('permission.cache.column_names_except', ['created_at', 'updated_at', 'deleted_at']); $permissions = $this->getPermissionClass()->select()->with('roles')->get() ->map(function ($permission) { diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 462493b63..c7e641768 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -63,9 +63,8 @@ public function permissions(): BelongsToMany /** * Scope the model query to certain permissions only. * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions - * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions * @return \Illuminate\Database\Eloquent\Builder */ public function scopePermission(Builder $query, $permissions): Builder @@ -93,9 +92,9 @@ public function scopePermission(Builder $query, $permissions): Builder } /** - * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions - * + * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions * @return array + * * @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist */ protected function convertToPermissionModels($permissions): array @@ -117,9 +116,9 @@ protected function convertToPermissionModels($permissions): array /** * Find a permission. * - * @param string|int|\Spatie\Permission\Contracts\Permission $permission - * + * @param string|int|\Spatie\Permission\Contracts\Permission $permission * @return \Spatie\Permission\Contracts\Permission + * * @throws PermissionDoesNotExist */ public function filterPermission($permission, $guardName = null) @@ -150,10 +149,10 @@ public function filterPermission($permission, $guardName = null) /** * Determine if the model may perform the given permission. * - * @param string|int|\Spatie\Permission\Contracts\Permission $permission - * @param string|null $guardName - * + * @param string|int|\Spatie\Permission\Contracts\Permission $permission + * @param string|null $guardName * @return bool + * * @throws PermissionDoesNotExist */ public function hasPermissionTo($permission, $guardName = null): bool @@ -170,9 +169,8 @@ public function hasPermissionTo($permission, $guardName = null): bool /** * Validates a wildcard permission against all permissions of a user. * - * @param string|int|\Spatie\Permission\Contracts\Permission $permission - * @param string|null $guardName - * + * @param string|int|\Spatie\Permission\Contracts\Permission $permission + * @param string|null $guardName * @return bool */ protected function hasWildcardPermission($permission, $guardName = null): bool @@ -209,9 +207,8 @@ protected function hasWildcardPermission($permission, $guardName = null): bool /** * An alias to hasPermissionTo(), but avoids throwing an exception. * - * @param string|int|\Spatie\Permission\Contracts\Permission $permission - * @param string|null $guardName - * + * @param string|int|\Spatie\Permission\Contracts\Permission $permission + * @param string|null $guardName * @return bool */ public function checkPermissionTo($permission, $guardName = null): bool @@ -226,8 +223,7 @@ public function checkPermissionTo($permission, $guardName = null): bool /** * Determine if the model has any of the given permissions. * - * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions - * + * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions * @return bool */ public function hasAnyPermission(...$permissions): bool @@ -246,8 +242,7 @@ public function hasAnyPermission(...$permissions): bool /** * Determine if the model has all of the given permissions. * - * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions - * + * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions * @return bool */ public function hasAllPermissions(...$permissions): bool @@ -266,8 +261,7 @@ public function hasAllPermissions(...$permissions): bool /** * Determine if the model has, via roles, the given permission. * - * @param \Spatie\Permission\Contracts\Permission $permission - * + * @param \Spatie\Permission\Contracts\Permission $permission * @return bool */ protected function hasPermissionViaRole(Permission $permission): bool @@ -278,9 +272,9 @@ protected function hasPermissionViaRole(Permission $permission): bool /** * Determine if the model has the given permission. * - * @param string|int|\Spatie\Permission\Contracts\Permission $permission - * + * @param string|int|\Spatie\Permission\Contracts\Permission $permission * @return bool + * * @throws PermissionDoesNotExist */ public function hasDirectPermission($permission): bool @@ -319,8 +313,7 @@ public function getAllPermissions(): Collection /** * Returns permissions ids as array keys * - * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions - * + * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions * @return array */ public function collectPermissions(...$permissions) @@ -349,8 +342,7 @@ public function collectPermissions(...$permissions) /** * Grant the given permission(s) to a role. * - * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions - * + * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions * @return $this */ public function givePermissionTo(...$permissions) @@ -386,8 +378,7 @@ function ($object) use ($permissions, $model) { /** * Remove all current permissions and set the given ones. * - * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions - * + * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions * @return $this */ public function syncPermissions(...$permissions) @@ -400,8 +391,7 @@ public function syncPermissions(...$permissions) /** * Revoke the given permission(s). * - * @param \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Permission[]|string|string[] $permission - * + * @param \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Permission[]|string|string[] $permission * @return $this */ public function revokePermissionTo($permission) @@ -423,8 +413,7 @@ public function getPermissionNames(): Collection } /** - * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions - * + * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions * @return \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Permission[]|\Illuminate\Support\Collection */ protected function getStoredPermission($permissions) @@ -454,7 +443,7 @@ protected function getStoredPermission($permissions) } /** - * @param \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Role $roleOrPermission + * @param \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Role $roleOrPermission * * @throws \Spatie\Permission\Exceptions\GuardDoesNotMatch */ @@ -486,7 +475,7 @@ public function forgetCachedPermissions() /** * Check if the model has All of the requested Direct permissions. * - * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions + * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions * @return bool */ public function hasAllDirectPermissions(...$permissions): bool @@ -505,7 +494,7 @@ public function hasAllDirectPermissions(...$permissions): bool /** * Check if the model has Any of the requested Direct permissions. * - * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions + * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions * @return bool */ public function hasAnyDirectPermission(...$permissions): bool diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 6e4d7e5fe..39c02f68d 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -64,10 +64,9 @@ public function roles(): BelongsToMany /** * Scope the model query to certain roles only. * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param string|int|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles - * @param string $guard - * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string|int|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles + * @param string $guard * @return \Illuminate\Database\Eloquent\Builder */ public function scopeRole(Builder $query, $roles, $guard = null): Builder @@ -96,8 +95,7 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder /** * Assign the given role to the model. * - * @param array|string|int|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection ...$roles - * + * @param array|string|int|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection ...$roles * @return $this */ public function assignRole(...$roles) @@ -151,7 +149,7 @@ function ($object) use ($roles, $model) { /** * Revoke the given role from the model. * - * @param string|int|\Spatie\Permission\Contracts\Role $role + * @param string|int|\Spatie\Permission\Contracts\Role $role */ public function removeRole($role) { @@ -170,7 +168,6 @@ public function removeRole($role) * Remove all current roles and set the given ones. * * @param array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection|string|int ...$roles - * * @return $this */ public function syncRoles(...$roles) @@ -183,8 +180,8 @@ public function syncRoles(...$roles) /** * Determine if the model has (one of) the given role(s). * - * @param string|int|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles - * @param string|null $guard + * @param string|int|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles + * @param string|null $guard * @return bool */ public function hasRole($roles, string $guard = null): bool @@ -232,8 +229,7 @@ public function hasRole($roles, string $guard = null): bool * * Alias to hasRole() but without Guard controls * - * @param string|int|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles - * + * @param string|int|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles * @return bool */ public function hasAnyRole(...$roles): bool diff --git a/src/WildcardPermission.php b/src/WildcardPermission.php index 73797a854..ebac8ba40 100644 --- a/src/WildcardPermission.php +++ b/src/WildcardPermission.php @@ -23,7 +23,7 @@ class WildcardPermission protected $parts; /** - * @param string $permission + * @param string $permission */ public function __construct(string $permission) { @@ -34,8 +34,7 @@ public function __construct(string $permission) } /** - * @param string|WildcardPermission $permission - * + * @param string|WildcardPermission $permission * @return bool */ public function implies($permission): bool @@ -71,9 +70,8 @@ public function implies($permission): bool } /** - * @param Collection $part - * @param Collection $otherPart - * + * @param Collection $part + * @param Collection $otherPart * @return bool */ protected function containsAll(Collection $part, Collection $otherPart): bool diff --git a/src/helpers.php b/src/helpers.php index 25116d0ff..ac6fc3699 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -2,8 +2,7 @@ if (! function_exists('getModelForGuard')) { /** - * @param string $guard - * + * @param string $guard * @return string|null */ function getModelForGuard(string $guard) @@ -21,8 +20,7 @@ function getModelForGuard(string $guard) if (! function_exists('setPermissionsTeamId')) { /** - * @param int|string|\Illuminate\Database\Eloquent\Model $id - * + * @param int|string|\Illuminate\Database\Eloquent\Model $id */ function setPermissionsTeamId($id) { diff --git a/tests/CacheTest.php b/tests/CacheTest.php index 62123f792..a44f03a2b 100644 --- a/tests/CacheTest.php +++ b/tests/CacheTest.php @@ -12,8 +12,11 @@ class CacheTest extends TestCase { protected $cache_init_count = 0; + protected $cache_load_count = 0; + protected $cache_run_count = 2; // roles lookup, permissions lookup + protected $cache_relations_count = 1; protected $registrar; diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index 40151b67e..61bba126b 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -587,35 +587,35 @@ public function it_can_check_if_there_is_any_of_the_direct_permissions_given() public function it_can_check_permission_based_on_logged_in_user_guard() { $this->testUser->givePermissionTo(app(Permission::class)::create([ - 'name' => 'do_that', - 'guard_name' => 'api', - ])); + 'name' => 'do_that', + 'guard_name' => 'api', + ])); $response = $this->actingAs($this->testUser, 'api') ->json('GET', '/check-api-guard-permission'); $response->assertJson([ - 'status' => true, - ]); + 'status' => true, + ]); } /** @test */ public function it_can_reject_permission_based_on_logged_in_user_guard() { $unassignedPermission = app(Permission::class)::create([ - 'name' => 'do_that', - 'guard_name' => 'api', - ]); + 'name' => 'do_that', + 'guard_name' => 'api', + ]); $assignedPermission = app(Permission::class)::create([ - 'name' => 'do_that', - 'guard_name' => 'web', - ]); + 'name' => 'do_that', + 'guard_name' => 'web', + ]); $this->testUser->givePermissionTo($assignedPermission); $response = $this->withExceptionHandling() ->actingAs($this->testUser, 'api') ->json('GET', '/check-api-guard-permission'); $response->assertJson([ - 'status' => false, - ]); + 'status' => false, + ]); } } diff --git a/tests/Permission.php b/tests/Permission.php index d7529ed69..24d4938b8 100644 --- a/tests/Permission.php +++ b/tests/Permission.php @@ -7,7 +7,7 @@ class Permission extends \Spatie\Permission\Models\Permission protected $primaryKey = 'permission_test_id'; protected $visible = [ - 'permission_test_id', - 'name', + 'permission_test_id', + 'name', ]; } diff --git a/tests/Role.php b/tests/Role.php index bbe13ece7..ee2862f82 100644 --- a/tests/Role.php +++ b/tests/Role.php @@ -7,7 +7,7 @@ class Role extends \Spatie\Permission\Models\Role protected $primaryKey = 'role_test_id'; protected $visible = [ - 'role_test_id', - 'name', + 'role_test_id', + 'name', ]; } diff --git a/tests/RuntimeRole.php b/tests/RuntimeRole.php index 82f24a09f..b8cbac7da 100644 --- a/tests/RuntimeRole.php +++ b/tests/RuntimeRole.php @@ -5,7 +5,7 @@ class RuntimeRole extends \Spatie\Permission\Models\Role { protected $visible = [ - 'id', - 'name', + 'id', + 'name', ]; } diff --git a/tests/TestCase.php b/tests/TestCase.php index 8f913c53e..7fd2ebb57 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -40,6 +40,7 @@ abstract class TestCase extends Orchestra protected $hasTeams = false; protected static $migration; + protected static $customMigration; public function setUp(): void @@ -60,8 +61,7 @@ public function setUp(): void } /** - * @param \Illuminate\Foundation\Application $app - * + * @param \Illuminate\Foundation\Application $app * @return array */ protected function getPackageProviders($app) @@ -74,7 +74,7 @@ protected function getPackageProviders($app) /** * Set up the environment. * - * @param \Illuminate\Foundation\Application $app + * @param \Illuminate\Foundation\Application $app */ protected function getEnvironmentSetUp($app) { @@ -112,7 +112,7 @@ protected function getEnvironmentSetUp($app) /** * Set up the database. * - * @param \Illuminate\Foundation\Application $app + * @param \Illuminate\Foundation\Application $app */ protected function setUpDatabase($app) { @@ -200,8 +200,8 @@ public function setUpRoutes(): void { Route::middleware('auth:api')->get('/check-api-guard-permission', function (Request $request) { return [ - 'status' => $request->user()->hasPermissionTo('do_that'), - ]; + 'status' => $request->user()->hasPermissionTo('do_that'), + ]; }); } } diff --git a/tests/TestHelper.php b/tests/TestHelper.php index 2ad3b2a5e..c081d2f3d 100644 --- a/tests/TestHelper.php +++ b/tests/TestHelper.php @@ -9,9 +9,8 @@ class TestHelper { /** - * @param string $middleware - * @param object $parameter - * + * @param string $middleware + * @param object $parameter * @return int */ public function testMiddleware($middleware, $parameter) diff --git a/tests/WildcardMiddlewareTest.php b/tests/WildcardMiddlewareTest.php index 9fe52796d..e354800eb 100644 --- a/tests/WildcardMiddlewareTest.php +++ b/tests/WildcardMiddlewareTest.php @@ -14,7 +14,9 @@ class WildcardMiddlewareTest extends TestCase { protected $roleMiddleware; + protected $permissionMiddleware; + protected $roleOrPermissionMiddleware; public function setUp(): void From 567a199d75240835cca075e1ee836a7b4928db84 Mon Sep 17 00:00:00 2001 From: Anders Jenbo Date: Thu, 27 Oct 2022 00:05:19 +0200 Subject: [PATCH 0574/1013] Hint model properties --- src/Models/Permission.php | 7 +++++++ src/Models/Role.php | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 1a044e694..0479b7430 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -13,6 +13,13 @@ use Spatie\Permission\Traits\HasRoles; use Spatie\Permission\Traits\RefreshesPermissionCache; +/** + * @property int $id + * @property string $name + * @property string $guard_name + * @property ?\Illuminate\Support\Carbon $created_at + * @property ?\Illuminate\Support\Carbon $updated_at + */ class Permission extends Model implements PermissionContract { use HasRoles; diff --git a/src/Models/Role.php b/src/Models/Role.php index bb4173d82..56c884826 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -13,6 +13,13 @@ use Spatie\Permission\Traits\HasPermissions; use Spatie\Permission\Traits\RefreshesPermissionCache; +/** + * @property int $id + * @property string $name + * @property string $guard_name + * @property ?\Illuminate\Support\Carbon $created_at + * @property ?\Illuminate\Support\Carbon $updated_at + */ class Role extends Model implements RoleContract { use HasPermissions; From fd2dfbca918d5c9ea640836ff3675773892969b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jan 2023 05:07:12 +0000 Subject: [PATCH 0575/1013] Bump aglipanci/laravel-pint-action from 1.0.0 to 2.1.0 Bumps [aglipanci/laravel-pint-action](https://github.com/aglipanci/laravel-pint-action) from 1.0.0 to 2.1.0. - [Release notes](https://github.com/aglipanci/laravel-pint-action/releases) - [Commits](https://github.com/aglipanci/laravel-pint-action/compare/1.0.0...2.1.0) --- updated-dependencies: - dependency-name: aglipanci/laravel-pint-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/fix-php-code-style-issues.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/fix-php-code-style-issues.yml b/.github/workflows/fix-php-code-style-issues.yml index 150750cbb..39a77e7b3 100644 --- a/.github/workflows/fix-php-code-style-issues.yml +++ b/.github/workflows/fix-php-code-style-issues.yml @@ -16,7 +16,7 @@ jobs: ref: ${{ github.head_ref }} - name: Fix PHP code style issues - uses: aglipanci/laravel-pint-action@1.0.0 + uses: aglipanci/laravel-pint-action@2.1.0 - name: Commit changes uses: stefanzweifel/git-auto-commit-action@v4 From d67b5c201aa35edceeba7efae433520cc3362aa0 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Thu, 12 Jan 2023 10:40:19 -0500 Subject: [PATCH 0576/1013] Laravel 10.x Support --- .github/workflows/run-tests-L8.yml | 10 +++++++++- composer.json | 10 +++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/run-tests-L8.yml b/.github/workflows/run-tests-L8.yml index d64ef9545..a8e9ce842 100644 --- a/.github/workflows/run-tests-L8.yml +++ b/.github/workflows/run-tests-L8.yml @@ -10,14 +10,22 @@ jobs: fail-fast: false matrix: php: [8.2, 8.1, 8.0, 7.4, 7.3] - laravel: [9.*, 8.*] + laravel: [10.*, 9.*, 8.*] dependency-version: [prefer-lowest, prefer-stable] include: + - laravel: 10.* + testbench: 8.* - laravel: 9.* testbench: 7.* - laravel: 8.* testbench: 6.23 exclude: + - laravel: 10.* + php: 8.0 + - laravel: 10.* + php: 7.4 + - laravel: 10.* + php: 7.3 - laravel: 9.* php: 7.4 - laravel: 9.* diff --git a/composer.json b/composer.json index d4f66ca5c..e7d68ab6d 100644 --- a/composer.json +++ b/composer.json @@ -23,13 +23,13 @@ "homepage": "https://github.com/spatie/laravel-permission", "require": { "php": "^7.3|^8.0|^8.1", - "illuminate/auth": "^7.0|^8.0|^9.0", - "illuminate/container": "^7.0|^8.0|^9.0", - "illuminate/contracts": "^7.0|^8.0|^9.0", - "illuminate/database": "^7.0|^8.0|^9.0" + "illuminate/auth": "^7.0|^8.0|^9.0|^10.0", + "illuminate/container": "^7.0|^8.0|^9.0|^10.0", + "illuminate/contracts": "^7.0|^8.0|^9.0|^10.0", + "illuminate/database": "^7.0|^8.0|^9.0|^10.0" }, "require-dev": { - "orchestra/testbench": "^5.0|^6.0|^7.0", + "orchestra/testbench": "^5.0|^6.0|^7.0|^8.0", "phpunit/phpunit": "^9.4", "predis/predis": "^1.1" }, From 392feff3c27ff9347ec91f7b2893395c8260ffb9 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Fri, 13 Jan 2023 17:02:18 -0500 Subject: [PATCH 0577/1013] Fix tests badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d090b8b24..9a6408e69 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-permission.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-permission) -![](https://github.com/spatie/laravel-permission/workflows/Run%20Tests/badge.svg?branch=master) +[![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/spatie/laravel-permission/run-tests-L8.yml?branch=main&label=Tests)](https://github.com/spatie/laravel-permission/actions?query=workflow%3ATests+branch%3Amain) [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-permission.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-permission) ## Documentation, Installation, and Usage Instructions From ad69f772b0d5fe626fa6d780a54f29f934036990 Mon Sep 17 00:00:00 2001 From: drbyte Date: Sat, 14 Jan 2023 05:33:58 +0000 Subject: [PATCH 0578/1013] Update CHANGELOG --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b12a5c516..5d9188db3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.8.0 - 2023-01-14 + +### What's Changed + +- Laravel 10.x Support by @erikn69 in https://github.com/spatie/laravel-permission/pull/2298 + +#### Administrative + +- [Docs] Link updated to match name change of related tool repo by @aliqasemzadeh in https://github.com/spatie/laravel-permission/pull/2253 +- Fix tests badge by @erikn69 in https://github.com/spatie/laravel-permission/pull/2300 +- Add Laravel Pint Support by @patinthehat in https://github.com/spatie/laravel-permission/pull/2269 +- Normalize composer.json by @patinthehat in https://github.com/spatie/laravel-permission/pull/2259 +- Add Dependabot Automation by @patinthehat in https://github.com/spatie/laravel-permission/pull/2257 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.7.0...5.8.0 + ## 5.7.0 - 2022-11-23 ### What's Changed @@ -542,6 +558,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -592,6 +609,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From a6b22b9b63929b9b533be57a33320190e8a8b9e2 Mon Sep 17 00:00:00 2001 From: Navid Sedehi Date: Sat, 14 Jan 2023 18:36:45 +0330 Subject: [PATCH 0579/1013] update publish tag name --- src/PermissionServiceProvider.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index e408064a9..8162b2332 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -54,11 +54,11 @@ protected function offerPublishing() $this->publishes([ __DIR__.'/../config/permission.php' => config_path('permission.php'), - ], 'config'); + ], 'permission-config'); $this->publishes([ __DIR__.'/../database/migrations/create_permission_tables.php.stub' => $this->getMigrationFileName('create_permission_tables.php'), - ], 'migrations'); + ], 'permission-migrations'); } protected function registerCommands() From 97da4c029e4ff34627ab0fac8eb199d44afb9537 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 18 Jan 2023 01:38:59 -0500 Subject: [PATCH 0580/1013] [Docs] Clarify meaning of pipe operator in middleware rules Closes #2304 --- docs/basic-usage/middleware.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index a10aa3f36..5d6ddae88 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -41,16 +41,12 @@ Route::group(['middleware' => ['role:super-admin','permission:publish articles'] // }); -Route::group(['middleware' => ['role_or_permission:super-admin|edit articles']], function () { - // -}); - Route::group(['middleware' => ['role_or_permission:publish articles']], function () { // }); ``` -Alternatively, you can separate multiple roles or permission with a `|` (pipe) character: +You can specify multiple roles or permissions with a `|` (pipe) character, which is treated as `OR`: ```php Route::group(['middleware' => ['role:super-admin|writer']], function () { From 3df2a29b5a8150f77189107272b8db419be59fe9 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 18 Jan 2023 02:03:25 -0500 Subject: [PATCH 0581/1013] [Docs] Add Laravel compatibility matrix --- docs/installation-laravel.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index c37db5d91..40bd0b032 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -3,9 +3,19 @@ title: Installation in Laravel weight: 4 --- +## Laravel Version Compatibility + This package can be used with Laravel 6.0 or higher. -(For Laravel 5.8, use v3.17.0) +Package | Laravel Version +--------|----------- + ^5.8 | 7,8,9,10 + ^5.7 | 7,8,9 +^5.4-^5.6 | 7,8 +5.0-5.3 | 6,7,8 + ^4 | 6,7,8 + ^3 | 5.8 + ## Installing From 2227bb5dae8bd2d51916a6093190c70c10ae4428 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 18 Jan 2023 02:08:13 -0500 Subject: [PATCH 0582/1013] [Docs] Tidy installation instructions --- docs/installation-laravel.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 40bd0b032..642ac5a3c 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -21,9 +21,9 @@ Package | Laravel Version 1. Consult the **Prerequisites** page for important considerations regarding your **User** models! -2. This package publishes a `config/permission.php` file. If you already have a file by that name, you must rename or remove it. +2. This package **publishes a `config/permission.php` file**. If you already have a file by that name, you must rename or remove it. -3. You can install the package via composer: +3. You can **install the package via composer**: composer require spatie/laravel-permission @@ -36,7 +36,7 @@ Package | Laravel Version ]; ``` -5. You should publish [the migration](https://github.com/spatie/laravel-permission/blob/main/database/migrations/create_permission_tables.php.stub) and the [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/main/config/permission.php) with: +5. **You should publish** [the migration](https://github.com/spatie/laravel-permission/blob/main/database/migrations/create_permission_tables.php.stub) and the [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/main/config/permission.php) with: ``` php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" @@ -45,17 +45,24 @@ Package | Laravel Version 6. NOTE: If you are using UUIDs, see the Advanced section of the docs on UUID steps, before you continue. It explains some changes you may want to make to the migrations and config file before continuing. It also mentions important considerations after extending this package's models for UUID capability. If you are going to use teams feature, you have to update your [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/main/config/permission.php) and set `'teams' => true,`, if you want to use a custom foreign key for teams you must change `team_foreign_key`. -7. Clear your config cache. This package requires access to the `permission` config. Generally it's bad practice to do config-caching in a development environment. If you've been caching configurations locally, clear your config cache with either of these commands: +7. **Clear your config cache**. This package requires access to the `permission` config. Generally it's bad practice to do config-caching in a development environment. If you've been caching configurations locally, clear your config cache with either of these commands: php artisan optimize:clear # or php artisan config:clear -8. Run the migrations: After the config and migration have been published and configured, you can create the tables for this package by running: +8. **Run the migrations**: After the config and migration have been published and configured, you can create the tables for this package by running: php artisan migrate -9. Add the necessary trait to your User model: Consult the Basic Usage section of the docs for how to get started using the features of this package. +9. **Add the necessary trait to your User model**: + + // The User model requires this trait + use HasRoles; + + Consult the **Basic Usage** section of the docs to get started using the features of this package. + +. ### Default config file contents From d55b02cde202751e2026eb51b73b8e2ac0b3902d Mon Sep 17 00:00:00 2001 From: parallels999 <109294935+parallels999@users.noreply.github.com> Date: Thu, 19 Jan 2023 14:43:45 -0500 Subject: [PATCH 0583/1013] Shorten required php version (#2265) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e7d68ab6d..e3abef547 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ ], "homepage": "https://github.com/spatie/laravel-permission", "require": { - "php": "^7.3|^8.0|^8.1", + "php": "^7.3|^8.0", "illuminate/auth": "^7.0|^8.0|^9.0|^10.0", "illuminate/container": "^7.0|^8.0|^9.0|^10.0", "illuminate/contracts": "^7.0|^8.0|^9.0|^10.0", From 298736569d2f43cc799aa8267d3c6ade0667f770 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 18 Jan 2023 15:48:59 +0100 Subject: [PATCH 0584/1013] fix: Lazily bind dependencies --- src/PermissionServiceProvider.php | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index e408064a9..0867b8522 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -13,7 +13,7 @@ class PermissionServiceProvider extends ServiceProvider { - public function boot(PermissionRegistrar $permissionLoader) + public function boot() { $this->offerPublishing(); @@ -23,14 +23,14 @@ public function boot(PermissionRegistrar $permissionLoader) $this->registerModelBindings(); - if ($this->app->config['permission.register_permission_check_method']) { - $permissionLoader->clearClassPermissions(); - $permissionLoader->registerPermissions(); - } - - $this->app->singleton(PermissionRegistrar::class, function ($app) use ($permissionLoader) { - return $permissionLoader; + $this->callAfterResolving(PermissionRegistrar::class, function (PermissionRegistrar $permissionLoader) { + if ($this->app->config['permission.register_permission_check_method']) { + $permissionLoader->clearClassPermissions(); + $permissionLoader->registerPermissions(); + } }); + + $this->app->singleton(PermissionRegistrar::class); } public function register() @@ -74,14 +74,16 @@ protected function registerCommands() protected function registerModelBindings() { - $config = $this->app->config['permission.models']; + $this->app->bind(PermissionContract::class, function ($app) { + $config = $app->config['permission.models']; - if (! $config) { - return; - } + return $app->make($config['permission']); + }); + $this->app->bind(RoleContract::class, function ($app) { + $config = $app->config['permission.models']; - $this->app->bind(PermissionContract::class, $config['permission']); - $this->app->bind(RoleContract::class, $config['role']); + return $app->make($config['role']); + }); } public static function bladeMethodWrapper($method, $role, $guard = null) From 384e06f1bdb34717998e7273ed07ae95fc8296cf Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Mon, 23 Jan 2023 15:26:30 +0100 Subject: [PATCH 0585/1013] Update README.md --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 9a6408e69..9850ee4ee 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,3 @@ - -[](https://supportukrainenow.org) -

Social Card of Laravel Permission

# Associate users with permissions and roles From c14aced21d0a5542d446a1a21650e8e207962092 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Jan 2023 05:11:21 +0000 Subject: [PATCH 0586/1013] Bump dependabot/fetch-metadata from 1.3.5 to 1.3.6 Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 1.3.5 to 1.3.6. - [Release notes](https://github.com/dependabot/fetch-metadata/releases) - [Commits](https://github.com/dependabot/fetch-metadata/compare/v1.3.5...v1.3.6) --- updated-dependencies: - dependency-name: dependabot/fetch-metadata dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/dependabot-auto-merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 78e5cd252..f2e85e7d4 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -13,7 +13,7 @@ jobs: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v1.3.5 + uses: dependabot/fetch-metadata@v1.3.6 with: github-token: "${{ secrets.GITHUB_TOKEN }}" compat-lookup: true From 79ae998bad35f92c76bc027679669ab8a2dcb1fe Mon Sep 17 00:00:00 2001 From: Francisco Madeira Date: Fri, 3 Feb 2023 14:30:30 +0000 Subject: [PATCH 0587/1013] Extract query to `getPermissionsWithRoles` method. --- src/PermissionRegistrar.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index ca2bf66eb..8a7b4ca19 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -273,6 +273,11 @@ public function getCacheStore(): Store return $this->cache->getStore(); } + protected function getPermissionsWithRoles(): Collection + { + return $this->getPermissionClass()->select()->with('roles')->get(); + } + /** * Changes array keys with alias * @@ -310,7 +315,7 @@ private function getSerializedPermissionsForCache() { $this->except = config('permission.cache.column_names_except', ['created_at', 'updated_at', 'deleted_at']); - $permissions = $this->getPermissionClass()->select()->with('roles')->get() + $permissions = $this->getPermissionsWithRoles() ->map(function ($permission) { if (! $this->alias) { $this->aliasModelFields($permission); From 72e78cd07004e6caa5c8ffde5128bddf0c23a12a Mon Sep 17 00:00:00 2001 From: drbyte Date: Mon, 6 Feb 2023 17:08:47 +0000 Subject: [PATCH 0588/1013] Fix styling --- src/Contracts/Wildcard.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Contracts/Wildcard.php b/src/Contracts/Wildcard.php index 073a99dd1..4e9478bd5 100644 --- a/src/Contracts/Wildcard.php +++ b/src/Contracts/Wildcard.php @@ -5,8 +5,7 @@ interface Wildcard { /** - * @param string|Wildcard $permission - * + * @param string|Wildcard $permission * @return bool */ public function implies($permission): bool; From d5747753a80532b665efd09437004b75407a282b Mon Sep 17 00:00:00 2001 From: drbyte Date: Mon, 6 Feb 2023 17:16:02 +0000 Subject: [PATCH 0589/1013] Update CHANGELOG --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d9188db3..efb20a419 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,28 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.9.0 - 2023-02-06 + +### What's Changed + +- Add `permission-` prefix to publish tag names by @sedehi in https://github.com/spatie/laravel-permission/pull/2301 +- Fix detaching user models on teams feature #2220 by @erikn69 in https://github.com/spatie/laravel-permission/pull/2221 +- Hint model properties by @AJenbo in https://github.com/spatie/laravel-permission/pull/2230 +- Custom wildcard verification/separators support by @erikn69 in https://github.com/spatie/laravel-permission/pull/2252 +- fix: Lazily bind dependencies by @olivernybroe in https://github.com/spatie/laravel-permission/pull/2309 +- Extract query to `getPermissionsWithRoles` method. by @xiCO2k in https://github.com/spatie/laravel-permission/pull/2316 +- This will allow to extend the PermissionRegistrar class and change the query. + +### New Contributors + +- @sedehi made their first contribution in https://github.com/spatie/laravel-permission/pull/2301 +- @parallels999 made their first contribution in https://github.com/spatie/laravel-permission/pull/2265 +- @AJenbo made their first contribution in https://github.com/spatie/laravel-permission/pull/2230 +- @olivernybroe made their first contribution in https://github.com/spatie/laravel-permission/pull/2309 +- @xiCO2k made their first contribution in https://github.com/spatie/laravel-permission/pull/2316 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.8.0...5.9.0 + ## 5.8.0 - 2023-01-14 ### What's Changed @@ -559,6 +581,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -610,6 +633,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From 25f906619b03965da122f0c6000ef63a77507816 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 6 Feb 2023 16:34:04 -0500 Subject: [PATCH 0590/1013] Revert "fix: Lazily bind dependencies" --- src/PermissionServiceProvider.php | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 15683b66a..8162b2332 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -13,7 +13,7 @@ class PermissionServiceProvider extends ServiceProvider { - public function boot() + public function boot(PermissionRegistrar $permissionLoader) { $this->offerPublishing(); @@ -23,14 +23,14 @@ public function boot() $this->registerModelBindings(); - $this->callAfterResolving(PermissionRegistrar::class, function (PermissionRegistrar $permissionLoader) { - if ($this->app->config['permission.register_permission_check_method']) { - $permissionLoader->clearClassPermissions(); - $permissionLoader->registerPermissions(); - } - }); + if ($this->app->config['permission.register_permission_check_method']) { + $permissionLoader->clearClassPermissions(); + $permissionLoader->registerPermissions(); + } - $this->app->singleton(PermissionRegistrar::class); + $this->app->singleton(PermissionRegistrar::class, function ($app) use ($permissionLoader) { + return $permissionLoader; + }); } public function register() @@ -74,16 +74,14 @@ protected function registerCommands() protected function registerModelBindings() { - $this->app->bind(PermissionContract::class, function ($app) { - $config = $app->config['permission.models']; + $config = $this->app->config['permission.models']; - return $app->make($config['permission']); - }); - $this->app->bind(RoleContract::class, function ($app) { - $config = $app->config['permission.models']; + if (! $config) { + return; + } - return $app->make($config['role']); - }); + $this->app->bind(PermissionContract::class, $config['permission']); + $this->app->bind(RoleContract::class, $config['role']); } public static function bladeMethodWrapper($method, $role, $guard = null) From 6e0f9574d26d4d7ea16d63508db5b0a44424a862 Mon Sep 17 00:00:00 2001 From: drbyte Date: Mon, 6 Feb 2023 21:39:11 +0000 Subject: [PATCH 0591/1013] Update CHANGELOG --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index efb20a419..ebd548ee1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.9.1 - 2023-02-06 + +Apologies for the break caused by 5.9.0 ! + +### Reverted Lazy binding of dependencies. + +- Revert "fix: Lazily bind dependencies", originally #2309 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.9.0...5.9.1 + ## 5.9.0 - 2023-02-06 ### What's Changed @@ -582,6 +592,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -634,6 +645,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From f58e97a6a5b7554e0c786e3f7ee107b615f0aad0 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 18 Jan 2023 15:48:59 +0100 Subject: [PATCH 0592/1013] fix: Lazily bind dependencies --- src/PermissionRegistrar.php | 4 ++-- src/PermissionServiceProvider.php | 35 ++++++++++++++++++------------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index ca2bf66eb..cbff87357 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -133,9 +133,9 @@ public function getPermissionsTeamId() * * @return bool */ - public function registerPermissions(): bool + public function registerPermissions(Gate $gate): bool { - app(Gate::class)->before(function (Authorizable $user, string $ability) { + $gate->before(function (Authorizable $user, string $ability) { if (method_exists($user, 'checkPermissionTo')) { return $user->checkPermissionTo($ability) ?: null; } diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index e408064a9..3b0310673 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -2,6 +2,8 @@ namespace Spatie\Permission; +use Illuminate\Contracts\Auth\Access\Gate; +use Illuminate\Contracts\Foundation\Application; use Illuminate\Filesystem\Filesystem; use Illuminate\Routing\Route; use Illuminate\Support\Arr; @@ -13,7 +15,7 @@ class PermissionServiceProvider extends ServiceProvider { - public function boot(PermissionRegistrar $permissionLoader) + public function boot() { $this->offerPublishing(); @@ -23,14 +25,17 @@ public function boot(PermissionRegistrar $permissionLoader) $this->registerModelBindings(); - if ($this->app->config['permission.register_permission_check_method']) { - $permissionLoader->clearClassPermissions(); - $permissionLoader->registerPermissions(); - } - - $this->app->singleton(PermissionRegistrar::class, function ($app) use ($permissionLoader) { - return $permissionLoader; + $this->callAfterResolving(Gate::class, function (Gate $gate, Application $app) { + if ($this->app->config['permission.register_permission_check_method']) { + /** @var PermissionRegistrar $permissionLoader */ + $permissionLoader = $app->get(PermissionRegistrar::class); + $permissionLoader->clearClassPermissions(); + $permissionLoader->registerPermissions($gate); + } }); + + + $this->app->singleton(PermissionRegistrar::class); } public function register() @@ -74,14 +79,16 @@ protected function registerCommands() protected function registerModelBindings() { - $config = $this->app->config['permission.models']; + $this->app->bind(PermissionContract::class, function ($app) { + $config = $app->config['permission.models']; - if (! $config) { - return; - } + return $app->make($config['permission']); + }); + $this->app->bind(RoleContract::class, function ($app) { + $config = $app->config['permission.models']; - $this->app->bind(PermissionContract::class, $config['permission']); - $this->app->bind(RoleContract::class, $config['role']); + return $app->make($config['role']); + }); } public static function bladeMethodWrapper($method, $role, $guard = null) From 3859d92ef37184dafcefc16e8b403a0b5e57c552 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 7 Feb 2023 19:06:22 -0500 Subject: [PATCH 0593/1013] `master` now refers to v6 --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index e3abef547..afefa994d 100644 --- a/composer.json +++ b/composer.json @@ -53,8 +53,8 @@ }, "extra": { "branch-alias": { - "dev-main": "5.x-dev", - "dev-master": "5.x-dev" + "dev-main": "6.x-dev", + "dev-master": "6.x-dev" }, "laravel": { "providers": [ From b881a0777bdccd536d72ab36128e4f581417d575 Mon Sep 17 00:00:00 2001 From: xenaio <10925715+xenaio-daniil@users.noreply.github.com> Date: Wed, 8 Feb 2023 03:08:35 +0300 Subject: [PATCH 0594/1013] Fix Role::withCount if belongsToMany declared (#2280) * Fix Role::withCount if belongsToMany declared fixes spatie/laravel-permission#2277 --- src/Models/Role.php | 6 +- tests/RoleWithNesting.php | 39 ++++++++ tests/RoleWithNestingTest.php | 90 +++++++++++++++++++ .../roles_with_nesting_migration.php.stub | 40 +++++++++ 4 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 tests/RoleWithNesting.php create mode 100644 tests/RoleWithNestingTest.php create mode 100644 tests/customMigrations/roles_with_nesting_migration.php.stub diff --git a/src/Models/Role.php b/src/Models/Role.php index 9c6de4771..104916dd2 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -34,11 +34,7 @@ public function __construct(array $attributes = []) parent::__construct($attributes); $this->guarded[] = $this->primaryKey; - } - - public function getTable() - { - return config('permission.table_names.roles', parent::getTable()); + $this->table = config('permission.table_names.roles', parent::getTable()); } public static function create(array $attributes = []) diff --git a/tests/RoleWithNesting.php b/tests/RoleWithNesting.php new file mode 100644 index 000000000..9a8c33a6a --- /dev/null +++ b/tests/RoleWithNesting.php @@ -0,0 +1,39 @@ +belongsToMany( + static::class, + static::HIERARCHY_TABLE, + 'child_id', + 'parent_id'); + } + + /** + * @return BelongsToMany + */ + public function children(): BelongsToMany + { + return $this->belongsToMany( + static::class, + static::HIERARCHY_TABLE, + 'parent_id', + 'child_id'); + } +} diff --git a/tests/RoleWithNestingTest.php b/tests/RoleWithNestingTest.php new file mode 100644 index 000000000..543f8e3a0 --- /dev/null +++ b/tests/RoleWithNestingTest.php @@ -0,0 +1,90 @@ +parent_roles = []; + $this->child_roles = []; + $this->parent_roles["has_no_children"] = RoleWithNesting::create(["name"=>"has_no_children"]); + $this->parent_roles["has_1_child"] = RoleWithNesting::create(["name"=>"has_1_child"]); + $this->parent_roles["has_3_children"] = RoleWithNesting::create(["name"=>"has_3_children"]); + + $this->child_roles["has_no_parents"] = RoleWithNesting::create(["name"=>"has_no_parents"]); + $this->child_roles["has_1_parent"] = RoleWithNesting::create(["name"=>"has_1_parent"]); + $this->child_roles["has_2_parents"] = RoleWithNesting::create(["name"=>"has_2_parents"]); + $this->child_roles["third_child"] = RoleWithNesting::create(["name"=>"third_child"]); + + $this->parent_roles["has_1_child"]->children()->attach($this->child_roles["has_2_parents"]); + $this->parent_roles["has_3_children"]->children()->attach($this->child_roles["has_2_parents"]); + $this->parent_roles["has_3_children"]->children()->attach($this->child_roles["has_1_parent"]); + $this->parent_roles["has_3_children"]->children()->attach($this->child_roles["third_child"]); + } + + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + self::$migration = self::$old_migration; + } + + protected function getEnvironmentSetUp($app) + { + parent::getEnvironmentSetUp($app); + $app['config']->set('permission.models.role', RoleWithNesting::class); + $app['config']->set('permission.table_names.roles', "nesting_role"); + } + + protected static function getMigration() + { + require_once __DIR__."/customMigrations/roles_with_nesting_migration.php.stub"; + return new \CreatePermissionTablesWithNested(); + } + + /** @test + * @dataProvider roles_list + */ + public function it_returns_correct_withCount_of_nested_roles($role_group,$index,$relation,$expectedCount) + { + $role = $this->$role_group[$index]; + $count_field_name = sprintf("%s_count", $relation); + + $actualCount = intval(RoleWithNesting::query()->withCount($relation)->find($role->id)->$count_field_name); + + $this->assertSame( + $expectedCount, + $actualCount, + sprintf("%s expects %d %s, %d found",$role->name,$expectedCount,$relation,$actualCount) + ); + } + + public function roles_list(){ + return [ + ["parent_roles","has_no_children","children",0], + ["parent_roles","has_1_child","children",1], + ["parent_roles","has_3_children","children",3], + ["child_roles","has_no_parents","parents",0], + ["child_roles","has_1_parent","parents",1], + ["child_roles","has_2_parents","parents",2], + ]; + } +} diff --git a/tests/customMigrations/roles_with_nesting_migration.php.stub b/tests/customMigrations/roles_with_nesting_migration.php.stub new file mode 100644 index 000000000..81909275c --- /dev/null +++ b/tests/customMigrations/roles_with_nesting_migration.php.stub @@ -0,0 +1,40 @@ +id(); + $table->bigInteger("parent_id", false, true); + $table->bigInteger("child_id", false, true); + $table->foreign("parent_id")->references("id")->on($tableNames['roles']); + $table->foreign("child_id")->references("id")->on($tableNames['roles']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + parent::down(); + Schema::drop(\Spatie\Permission\Test\RoleWithNesting::HIERARCHY_TABLE); + } +} From 625a69d1a5cf8f7b7b921b271aad525577798cfd Mon Sep 17 00:00:00 2001 From: drbyte Date: Wed, 8 Feb 2023 00:09:03 +0000 Subject: [PATCH 0595/1013] Fix styling --- tests/RoleWithNestingTest.php | 58 +++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/tests/RoleWithNestingTest.php b/tests/RoleWithNestingTest.php index 543f8e3a0..4261dc03f 100644 --- a/tests/RoleWithNestingTest.php +++ b/tests/RoleWithNestingTest.php @@ -5,14 +5,16 @@ class RoleWithNestingTest extends TestCase { private static $old_migration; + /** * @var RoleWithNesting[] */ - protected $parent_roles=[]; + protected $parent_roles = []; + /** * @var RoleWithNesting[] */ - protected $child_roles=[]; + protected $child_roles = []; public static function setUpBeforeClass(): void { @@ -26,19 +28,19 @@ public function setUp(): void parent::setUp(); $this->parent_roles = []; $this->child_roles = []; - $this->parent_roles["has_no_children"] = RoleWithNesting::create(["name"=>"has_no_children"]); - $this->parent_roles["has_1_child"] = RoleWithNesting::create(["name"=>"has_1_child"]); - $this->parent_roles["has_3_children"] = RoleWithNesting::create(["name"=>"has_3_children"]); - - $this->child_roles["has_no_parents"] = RoleWithNesting::create(["name"=>"has_no_parents"]); - $this->child_roles["has_1_parent"] = RoleWithNesting::create(["name"=>"has_1_parent"]); - $this->child_roles["has_2_parents"] = RoleWithNesting::create(["name"=>"has_2_parents"]); - $this->child_roles["third_child"] = RoleWithNesting::create(["name"=>"third_child"]); - - $this->parent_roles["has_1_child"]->children()->attach($this->child_roles["has_2_parents"]); - $this->parent_roles["has_3_children"]->children()->attach($this->child_roles["has_2_parents"]); - $this->parent_roles["has_3_children"]->children()->attach($this->child_roles["has_1_parent"]); - $this->parent_roles["has_3_children"]->children()->attach($this->child_roles["third_child"]); + $this->parent_roles['has_no_children'] = RoleWithNesting::create(['name' => 'has_no_children']); + $this->parent_roles['has_1_child'] = RoleWithNesting::create(['name' => 'has_1_child']); + $this->parent_roles['has_3_children'] = RoleWithNesting::create(['name' => 'has_3_children']); + + $this->child_roles['has_no_parents'] = RoleWithNesting::create(['name' => 'has_no_parents']); + $this->child_roles['has_1_parent'] = RoleWithNesting::create(['name' => 'has_1_parent']); + $this->child_roles['has_2_parents'] = RoleWithNesting::create(['name' => 'has_2_parents']); + $this->child_roles['third_child'] = RoleWithNesting::create(['name' => 'third_child']); + + $this->parent_roles['has_1_child']->children()->attach($this->child_roles['has_2_parents']); + $this->parent_roles['has_3_children']->children()->attach($this->child_roles['has_2_parents']); + $this->parent_roles['has_3_children']->children()->attach($this->child_roles['has_1_parent']); + $this->parent_roles['has_3_children']->children()->attach($this->child_roles['third_child']); } public static function tearDownAfterClass(): void @@ -51,40 +53,42 @@ protected function getEnvironmentSetUp($app) { parent::getEnvironmentSetUp($app); $app['config']->set('permission.models.role', RoleWithNesting::class); - $app['config']->set('permission.table_names.roles', "nesting_role"); + $app['config']->set('permission.table_names.roles', 'nesting_role'); } protected static function getMigration() { - require_once __DIR__."/customMigrations/roles_with_nesting_migration.php.stub"; + require_once __DIR__.'/customMigrations/roles_with_nesting_migration.php.stub'; + return new \CreatePermissionTablesWithNested(); } /** @test * @dataProvider roles_list */ - public function it_returns_correct_withCount_of_nested_roles($role_group,$index,$relation,$expectedCount) + public function it_returns_correct_withCount_of_nested_roles($role_group, $index, $relation, $expectedCount) { $role = $this->$role_group[$index]; - $count_field_name = sprintf("%s_count", $relation); + $count_field_name = sprintf('%s_count', $relation); $actualCount = intval(RoleWithNesting::query()->withCount($relation)->find($role->id)->$count_field_name); $this->assertSame( $expectedCount, $actualCount, - sprintf("%s expects %d %s, %d found",$role->name,$expectedCount,$relation,$actualCount) + sprintf('%s expects %d %s, %d found', $role->name, $expectedCount, $relation, $actualCount) ); } - public function roles_list(){ + public function roles_list() + { return [ - ["parent_roles","has_no_children","children",0], - ["parent_roles","has_1_child","children",1], - ["parent_roles","has_3_children","children",3], - ["child_roles","has_no_parents","parents",0], - ["child_roles","has_1_parent","parents",1], - ["child_roles","has_2_parents","parents",2], + ['parent_roles', 'has_no_children', 'children', 0], + ['parent_roles', 'has_1_child', 'children', 1], + ['parent_roles', 'has_3_children', 'children', 3], + ['child_roles', 'has_no_parents', 'parents', 0], + ['child_roles', 'has_1_parent', 'parents', 1], + ['child_roles', 'has_2_parents', 'parents', 2], ]; } } From 991a000492611d2793a043dd70cbce18bf2dcca6 Mon Sep 17 00:00:00 2001 From: drbyte Date: Thu, 9 Feb 2023 02:41:41 +0000 Subject: [PATCH 0596/1013] Fix styling --- src/PermissionServiceProvider.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 08688b621..4abcd2cb8 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -34,7 +34,6 @@ public function boot() } }); - $this->app->singleton(PermissionRegistrar::class); } From cc8610e104a278144c475db9bee8037a33c298c5 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Mon, 6 Feb 2023 12:49:29 -0500 Subject: [PATCH 0597/1013] Full uuid support --- src/Models/Permission.php | 4 +-- src/Models/Role.php | 4 +-- src/Traits/HasPermissions.php | 10 ++++---- src/Traits/HasRoles.php | 8 +++--- tests/HasPermissionsWithCustomModelsTest.php | 27 ++++++++++++++++++++ tests/Permission.php | 20 +++++++++++++++ tests/Role.php | 20 +++++++++++++++ tests/TestCase.php | 6 +++++ 8 files changed, 86 insertions(+), 13 deletions(-) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 9bbf367e4..d0ea85122 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -104,13 +104,13 @@ public static function findByName(string $name, $guardName = null): PermissionCo /** * Find a permission by its id (and optionally guardName). * - * @param int $id + * @param int|string $id * @param string|null $guardName * @return \Spatie\Permission\Contracts\Permission * * @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist */ - public static function findById(int $id, $guardName = null): PermissionContract + public static function findById($id, $guardName = null): PermissionContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); $permission = static::getPermission([(new static())->getKeyName() => $id, 'guard_name' => $guardName]); diff --git a/src/Models/Role.php b/src/Models/Role.php index 104916dd2..aa1639e1d 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -108,11 +108,11 @@ public static function findByName(string $name, $guardName = null): RoleContract /** * Find a role by its id (and optionally guardName). * - * @param int $id + * @param int|string $id * @param string|null $guardName * @return \Spatie\Permission\Contracts\Role|\Spatie\Permission\Models\Role */ - public static function findById(int $id, $guardName = null): RoleContract + public static function findById($id, $guardName = null): RoleContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 0d85b998f..217ea526e 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -134,7 +134,7 @@ protected function convertToPermissionModels($permissions): array if ($permission instanceof Permission) { return $permission; } - $method = is_string($permission) ? 'findByName' : 'findById'; + $method = is_string($permission) && ! \Str::isUuid($permission) ? 'findByName' : 'findById'; return $this->getPermissionClass()->{$method}($permission, $this->getDefaultGuardName()); }, Arr::wrap($permissions)); @@ -152,14 +152,14 @@ public function filterPermission($permission, $guardName = null) { $permissionClass = $this->getPermissionClass(); - if (is_string($permission)) { + if (is_string($permission) && ! \Str::isUuid($permission)) { $permission = $permissionClass->findByName( $permission, $guardName ?? $this->getDefaultGuardName() ); } - if (is_int($permission)) { + if (is_int($permission) || is_string($permission)) { $permission = $permissionClass->findById( $permission, $guardName ?? $this->getDefaultGuardName() @@ -204,7 +204,7 @@ protected function hasWildcardPermission($permission, $guardName = null): bool { $guardName = $guardName ?? $this->getDefaultGuardName(); - if (is_int($permission)) { + if (is_int($permission) || \Str::isUuid($permission)) { $permission = $this->getPermissionClass()->findById($permission, $guardName); } @@ -449,7 +449,7 @@ protected function getStoredPermission($permissions) { $permissionClass = $this->getPermissionClass(); - if (is_numeric($permissions)) { + if (is_numeric($permissions) || \Str::isUuid($permissions)) { return $permissionClass->findById($permissions, $this->getDefaultGuardName()); } diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 8ab3732ad..35d008aa4 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -83,7 +83,7 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder return $role; } - $method = is_numeric($role) ? 'findById' : 'findByName'; + $method = is_numeric($role) || \Str::isUuid($role) ? 'findById' : 'findByName'; return $this->getRoleClass()->{$method}($role, $guard ?: $this->getDefaultGuardName()); }, Arr::wrap($roles)); @@ -195,13 +195,13 @@ public function hasRole($roles, string $guard = null): bool $roles = $this->convertPipeToArray($roles); } - if (is_string($roles)) { + if (is_string($roles) && ! \Str::isUuid($roles)) { return $guard ? $this->roles->where('guard_name', $guard)->contains('name', $roles) : $this->roles->contains('name', $roles); } - if (is_int($roles)) { + if (is_int($roles) || is_string($roles)) { $roleClass = $this->getRoleClass(); $key = (new $roleClass())->getKeyName(); @@ -325,7 +325,7 @@ protected function getStoredRole($role): Role { $roleClass = $this->getRoleClass(); - if (is_numeric($role)) { + if (is_numeric($role) || \Str::isUuid($role)) { return $roleClass->findById($role, $this->getDefaultGuardName()); } diff --git a/tests/HasPermissionsWithCustomModelsTest.php b/tests/HasPermissionsWithCustomModelsTest.php index 6b9fa34ca..b491c8e05 100644 --- a/tests/HasPermissionsWithCustomModelsTest.php +++ b/tests/HasPermissionsWithCustomModelsTest.php @@ -36,4 +36,31 @@ public function it_can_use_custom_fields_from_cache() $this->assertSame(0, count(DB::getQueryLog())); } + + /** @test */ + public function it_can_scope_users_using_a_int() + { + // Skipped because custom model uses uuid, + // replacement "it_can_scope_users_using_a_uuid" + $this->assertTrue(true); + } + + /** @test */ + public function it_can_scope_users_using_a_uuid() + { + $uuid1 = $this->testUserPermission->getKey(); + $uuid2 = app(Permission::class)::where('name', 'edit-news')->first()->getKey(); + + $user1 = User::create(['email' => 'user1@test.com']); + $user2 = User::create(['email' => 'user2@test.com']); + $user1->givePermissionTo([$uuid1, $uuid2]); + $this->testUserRole->givePermissionTo($uuid1); + $user2->assignRole('testRole'); + + $scopedUsers1 = User::permission($uuid1)->get(); + $scopedUsers2 = User::permission([$uuid2])->get(); + + $this->assertEquals(2, $scopedUsers1->count()); + $this->assertEquals(1, $scopedUsers2->count()); + } } diff --git a/tests/Permission.php b/tests/Permission.php index 24d4938b8..9d8f2cd91 100644 --- a/tests/Permission.php +++ b/tests/Permission.php @@ -10,4 +10,24 @@ class Permission extends \Spatie\Permission\Models\Permission 'permission_test_id', 'name', ]; + + protected static function boot() + { + parent::boot(); + static::creating(function ($model) { + if (empty($model->{$model->getKeyName()})) { + $model->{$model->getKeyName()} = \Str::uuid()->toString(); + } + }); + } + + public function getIncrementing() + { + return false; + } + + public function getKeyType() + { + return 'string'; + } } diff --git a/tests/Role.php b/tests/Role.php index ee2862f82..79f8cdbfd 100644 --- a/tests/Role.php +++ b/tests/Role.php @@ -10,4 +10,24 @@ class Role extends \Spatie\Permission\Models\Role 'role_test_id', 'name', ]; + + protected static function boot() + { + parent::boot(); + static::creating(function ($model) { + if (empty($model->{$model->getKeyName()})) { + $model->{$model->getKeyName()} = \Str::uuid()->toString(); + } + }); + } + + public function getIncrementing() + { + return false; + } + + public function getKeyType() + { + return 'string'; + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 7fd2ebb57..9481feb84 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -159,6 +159,9 @@ private function prepareMigration() '(\'id\'); // role id', 'references(\'id\') // permission id', 'references(\'id\') // role id', + 'bigIncrements', + 'unsignedBigInteger(PermissionRegistrar::$pivotRole)', + 'unsignedBigInteger(PermissionRegistrar::$pivotPermission)', ], [ 'CreatePermissionCustomTables', @@ -166,6 +169,9 @@ private function prepareMigration() '(\'role_test_id\');', 'references(\'permission_test_id\')', 'references(\'role_test_id\')', + 'uuid', + 'uuid(PermissionRegistrar::$pivotRole)->nullable(false)', + 'uuid(PermissionRegistrar::$pivotPermission)->nullable(false)', ], file_get_contents(__DIR__.'/../database/migrations/create_permission_tables.php.stub') ); From b2c858bc3cce4a7dfa13fd262a57ce258ceb4193 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Thu, 9 Feb 2023 10:12:19 -0500 Subject: [PATCH 0598/1013] Support ULIDs --- src/PermissionRegistrar.php | 21 +++++++++++++ src/Traits/HasPermissions.php | 8 ++--- src/Traits/HasRoles.php | 6 ++-- tests/PermissionRegistarTest.php | 51 ++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 tests/PermissionRegistarTest.php diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 38f71e4bf..d771b2205 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -384,4 +384,25 @@ private function hydrateRolesCache() $this->permissions['roles'] = []; } + + public static function isUid($value) + { + if (! is_string($value) || empty(trim($value))) { + return false; + } + + // check if is UUID/GUID + $uid = preg_match('/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iD', $value) > 0; + if ($uid) { + return true; + } + + // check if is ULID + $ulid = 26 == strlen($value) && 26 == strspn($value, '0123456789ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz') && $value[0] <= '7'; + if ($ulid) { + return true; + } + + return false; + } } diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 217ea526e..a29717d71 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -134,7 +134,7 @@ protected function convertToPermissionModels($permissions): array if ($permission instanceof Permission) { return $permission; } - $method = is_string($permission) && ! \Str::isUuid($permission) ? 'findByName' : 'findById'; + $method = is_string($permission) && ! PermissionRegistrar::isUid($permission) ? 'findByName' : 'findById'; return $this->getPermissionClass()->{$method}($permission, $this->getDefaultGuardName()); }, Arr::wrap($permissions)); @@ -152,7 +152,7 @@ public function filterPermission($permission, $guardName = null) { $permissionClass = $this->getPermissionClass(); - if (is_string($permission) && ! \Str::isUuid($permission)) { + if (is_string($permission) && ! PermissionRegistrar::isUid($permission)) { $permission = $permissionClass->findByName( $permission, $guardName ?? $this->getDefaultGuardName() @@ -204,7 +204,7 @@ protected function hasWildcardPermission($permission, $guardName = null): bool { $guardName = $guardName ?? $this->getDefaultGuardName(); - if (is_int($permission) || \Str::isUuid($permission)) { + if (is_int($permission) || PermissionRegistrar::isUid($permission)) { $permission = $this->getPermissionClass()->findById($permission, $guardName); } @@ -449,7 +449,7 @@ protected function getStoredPermission($permissions) { $permissionClass = $this->getPermissionClass(); - if (is_numeric($permissions) || \Str::isUuid($permissions)) { + if (is_numeric($permissions) || PermissionRegistrar::isUid($permissions)) { return $permissionClass->findById($permissions, $this->getDefaultGuardName()); } diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 35d008aa4..42cfb4ac0 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -83,7 +83,7 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder return $role; } - $method = is_numeric($role) || \Str::isUuid($role) ? 'findById' : 'findByName'; + $method = is_numeric($role) || PermissionRegistrar::isUid($role) ? 'findById' : 'findByName'; return $this->getRoleClass()->{$method}($role, $guard ?: $this->getDefaultGuardName()); }, Arr::wrap($roles)); @@ -195,7 +195,7 @@ public function hasRole($roles, string $guard = null): bool $roles = $this->convertPipeToArray($roles); } - if (is_string($roles) && ! \Str::isUuid($roles)) { + if (is_string($roles) && ! PermissionRegistrar::isUid($roles)) { return $guard ? $this->roles->where('guard_name', $guard)->contains('name', $roles) : $this->roles->contains('name', $roles); @@ -325,7 +325,7 @@ protected function getStoredRole($role): Role { $roleClass = $this->getRoleClass(); - if (is_numeric($role) || \Str::isUuid($role)) { + if (is_numeric($role) || PermissionRegistrar::isUid($role)) { return $roleClass->findById($role, $this->getDefaultGuardName()); } diff --git a/tests/PermissionRegistarTest.php b/tests/PermissionRegistarTest.php new file mode 100644 index 000000000..b864b3e90 --- /dev/null +++ b/tests/PermissionRegistarTest.php @@ -0,0 +1,51 @@ +assertTrue(PermissionRegistrar::isUid($uid)); + } + + foreach ($not_uids as $not_uid) { + $this->assertFalse(PermissionRegistrar::isUid($not_uid)); + } + } +} From d70317fc8e223f5e90efa13f8d732acafdeb9879 Mon Sep 17 00:00:00 2001 From: Oliver Nybroe Date: Sat, 11 Feb 2023 00:21:42 +0100 Subject: [PATCH 0599/1013] refactor: Change static properties to non-static (#2324) * refactor: Change static properties to non-static * Use config helper on migrations --------- Co-authored-by: erikn69 --- database/migrations/add_teams_fields.php.stub | 19 +++++----- .../create_permission_tables.php.stub | 37 ++++++++++--------- src/Commands/CreateRole.php | 8 ++-- src/Models/Permission.php | 6 +-- src/Models/Role.php | 30 ++++++++------- src/PermissionRegistrar.php | 28 +++++++------- src/Traits/HasPermissions.php | 16 ++++---- src/Traits/HasRoles.php | 18 ++++----- 8 files changed, 84 insertions(+), 78 deletions(-) diff --git a/database/migrations/add_teams_fields.php.stub b/database/migrations/add_teams_fields.php.stub index 6abdf8d8f..be79ee047 100644 --- a/database/migrations/add_teams_fields.php.stub +++ b/database/migrations/add_teams_fields.php.stub @@ -4,7 +4,6 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; -use Spatie\Permission\PermissionRegistrar; class AddTeamsFields extends Migration { @@ -18,6 +17,8 @@ class AddTeamsFields extends Migration $teams = config('permission.teams'); $tableNames = config('permission.table_names'); $columnNames = config('permission.column_names'); + $pivotRole = $columnNames['role_pivot_key'] ?? 'role_id'; + $pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id'; if (! $teams) { return; @@ -40,38 +41,38 @@ class AddTeamsFields extends Migration } if (! Schema::hasColumn($tableNames['model_has_permissions'], $columnNames['team_foreign_key'])) { - Schema::table($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) { + Schema::table($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission) { $table->unsignedBigInteger($columnNames['team_foreign_key'])->default('1'); $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index'); if (DB::getDriverName() !== 'sqlite') { - $table->dropForeign([PermissionRegistrar::$pivotPermission]); + $table->dropForeign([$pivotPermission]); } $table->dropPrimary(); - $table->primary([$columnNames['team_foreign_key'], PermissionRegistrar::$pivotPermission, $columnNames['model_morph_key'], 'model_type'], + $table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_permission_model_type_primary'); if (DB::getDriverName() !== 'sqlite') { - $table->foreign(PermissionRegistrar::$pivotPermission) + $table->foreign($pivotPermission) ->references('id')->on($tableNames['permissions'])->onDelete('cascade'); } }); } if (! Schema::hasColumn($tableNames['model_has_roles'], $columnNames['team_foreign_key'])) { - Schema::table($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames) { + Schema::table($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole) { $table->unsignedBigInteger($columnNames['team_foreign_key'])->default('1');; $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index'); if (DB::getDriverName() !== 'sqlite') { - $table->dropForeign([PermissionRegistrar::$pivotRole]); + $table->dropForeign([$pivotRole]); } $table->dropPrimary(); - $table->primary([$columnNames['team_foreign_key'], PermissionRegistrar::$pivotRole, $columnNames['model_morph_key'], 'model_type'], + $table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'], 'model_has_roles_role_model_type_primary'); if (DB::getDriverName() !== 'sqlite') { - $table->foreign(PermissionRegistrar::$pivotRole) + $table->foreign($pivotRole) ->references('id')->on($tableNames['roles'])->onDelete('cascade'); } }); diff --git a/database/migrations/create_permission_tables.php.stub b/database/migrations/create_permission_tables.php.stub index 04c3278b9..4874c05a8 100644 --- a/database/migrations/create_permission_tables.php.stub +++ b/database/migrations/create_permission_tables.php.stub @@ -3,7 +3,6 @@ use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; -use Spatie\Permission\PermissionRegistrar; class CreatePermissionTables extends Migration { @@ -14,9 +13,11 @@ class CreatePermissionTables extends Migration */ public function up() { + $teams = config('permission.teams'); $tableNames = config('permission.table_names'); $columnNames = config('permission.column_names'); - $teams = config('permission.teams'); + $pivotRole = $columnNames['role_pivot_key'] ?? 'role_id'; + $pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id'; if (empty($tableNames)) { throw new \Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.'); @@ -50,14 +51,14 @@ class CreatePermissionTables extends Migration } }); - Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames, $teams) { - $table->unsignedBigInteger(PermissionRegistrar::$pivotPermission); + Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) { + $table->unsignedBigInteger($pivotPermission); $table->string('model_type'); $table->unsignedBigInteger($columnNames['model_morph_key']); $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index'); - $table->foreign(PermissionRegistrar::$pivotPermission) + $table->foreign($pivotPermission) ->references('id') // permission id ->on($tableNames['permissions']) ->onDelete('cascade'); @@ -65,23 +66,23 @@ class CreatePermissionTables extends Migration $table->unsignedBigInteger($columnNames['team_foreign_key']); $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index'); - $table->primary([$columnNames['team_foreign_key'], PermissionRegistrar::$pivotPermission, $columnNames['model_morph_key'], 'model_type'], + $table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_permission_model_type_primary'); } else { - $table->primary([PermissionRegistrar::$pivotPermission, $columnNames['model_morph_key'], 'model_type'], + $table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_permission_model_type_primary'); } }); - Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames, $teams) { - $table->unsignedBigInteger(PermissionRegistrar::$pivotRole); + Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) { + $table->unsignedBigInteger($pivotRole); $table->string('model_type'); $table->unsignedBigInteger($columnNames['model_morph_key']); $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index'); - $table->foreign(PermissionRegistrar::$pivotRole) + $table->foreign($pivotRole) ->references('id') // role id ->on($tableNames['roles']) ->onDelete('cascade'); @@ -89,29 +90,29 @@ class CreatePermissionTables extends Migration $table->unsignedBigInteger($columnNames['team_foreign_key']); $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index'); - $table->primary([$columnNames['team_foreign_key'], PermissionRegistrar::$pivotRole, $columnNames['model_morph_key'], 'model_type'], + $table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'], 'model_has_roles_role_model_type_primary'); } else { - $table->primary([PermissionRegistrar::$pivotRole, $columnNames['model_morph_key'], 'model_type'], + $table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'], 'model_has_roles_role_model_type_primary'); } }); - Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames) { - $table->unsignedBigInteger(PermissionRegistrar::$pivotPermission); - $table->unsignedBigInteger(PermissionRegistrar::$pivotRole); + Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) { + $table->unsignedBigInteger($pivotPermission); + $table->unsignedBigInteger($pivotRole); - $table->foreign(PermissionRegistrar::$pivotPermission) + $table->foreign($pivotPermission) ->references('id') // permission id ->on($tableNames['permissions']) ->onDelete('cascade'); - $table->foreign(PermissionRegistrar::$pivotRole) + $table->foreign($pivotRole) ->references('id') // role id ->on($tableNames['roles']) ->onDelete('cascade'); - $table->primary([PermissionRegistrar::$pivotPermission, PermissionRegistrar::$pivotRole], 'role_has_permissions_permission_id_role_id_primary'); + $table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary'); }); app('cache') diff --git a/src/Commands/CreateRole.php b/src/Commands/CreateRole.php index f84385b09..7cb92aa35 100644 --- a/src/Commands/CreateRole.php +++ b/src/Commands/CreateRole.php @@ -17,14 +17,14 @@ class CreateRole extends Command protected $description = 'Create a role'; - public function handle() + public function handle(PermissionRegistrar $permissionRegistrar) { $roleClass = app(RoleContract::class); $teamIdAux = getPermissionsTeamId(); setPermissionsTeamId($this->option('team-id') ?: null); - if (! PermissionRegistrar::$teams && $this->option('team-id')) { + if (! $permissionRegistrar->teams && $this->option('team-id')) { $this->warn('Teams feature disabled, argument --team-id has no effect. Either enable it in permissions config file or remove --team-id parameter'); return; @@ -33,8 +33,8 @@ public function handle() $role = $roleClass::findOrCreate($this->argument('name'), $this->argument('guard')); setPermissionsTeamId($teamIdAux); - $teams_key = PermissionRegistrar::$teamsKey; - if (PermissionRegistrar::$teams && $this->option('team-id') && is_null($role->$teams_key)) { + $teams_key = $permissionRegistrar->teamsKey; + if ($permissionRegistrar->teams && $this->option('team-id') && is_null($role->$teams_key)) { $this->warn("Role `{$role->name}` already exists on the global team; argument --team-id has no effect"); } diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 9bbf367e4..1fabdf162 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -62,8 +62,8 @@ public function roles(): BelongsToMany return $this->belongsToMany( config('permission.models.role'), config('permission.table_names.role_has_permissions'), - PermissionRegistrar::$pivotPermission, - PermissionRegistrar::$pivotRole + app(PermissionRegistrar::class)->pivotPermission, + app(PermissionRegistrar::class)->pivotRole ); } @@ -76,7 +76,7 @@ public function users(): BelongsToMany getModelForGuard($this->attributes['guard_name'] ?? config('auth.defaults.guard')), 'model', config('permission.table_names.model_has_permissions'), - PermissionRegistrar::$pivotPermission, + app(PermissionRegistrar::class)->pivotPermission, config('permission.column_names.model_morph_key') ); } diff --git a/src/Models/Role.php b/src/Models/Role.php index 104916dd2..f32766de3 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -42,11 +42,13 @@ public static function create(array $attributes = []) $attributes['guard_name'] = $attributes['guard_name'] ?? Guard::getDefaultName(static::class); $params = ['name' => $attributes['name'], 'guard_name' => $attributes['guard_name']]; - if (PermissionRegistrar::$teams) { - if (array_key_exists(PermissionRegistrar::$teamsKey, $attributes)) { - $params[PermissionRegistrar::$teamsKey] = $attributes[PermissionRegistrar::$teamsKey]; + if (app(PermissionRegistrar::class)->teams) { + $teamsKey = app(PermissionRegistrar::class)->teamsKey; + + if (array_key_exists($teamsKey, $attributes)) { + $params[$teamsKey] = $attributes[$teamsKey]; } else { - $attributes[PermissionRegistrar::$teamsKey] = getPermissionsTeamId(); + $attributes[$teamsKey] = getPermissionsTeamId(); } } if (static::findByParam($params)) { @@ -64,8 +66,8 @@ public function permissions(): BelongsToMany return $this->belongsToMany( config('permission.models.permission'), config('permission.table_names.role_has_permissions'), - PermissionRegistrar::$pivotRole, - PermissionRegistrar::$pivotPermission + app(PermissionRegistrar::class)->pivotRole, + app(PermissionRegistrar::class)->pivotPermission ); } @@ -78,7 +80,7 @@ public function users(): BelongsToMany getModelForGuard($this->attributes['guard_name'] ?? config('auth.defaults.guard')), 'model', config('permission.table_names.model_has_roles'), - PermissionRegistrar::$pivotRole, + app(PermissionRegistrar::class)->pivotRole, config('permission.column_names.model_morph_key') ); } @@ -139,7 +141,7 @@ public static function findOrCreate(string $name, $guardName = null): RoleContra $role = static::findByParam(['name' => $name, 'guard_name' => $guardName]); if (! $role) { - return static::query()->create(['name' => $name, 'guard_name' => $guardName] + (PermissionRegistrar::$teams ? [PermissionRegistrar::$teamsKey => getPermissionsTeamId()] : [])); + return static::query()->create(['name' => $name, 'guard_name' => $guardName] + (app(PermissionRegistrar::class)->teams ? [app(PermissionRegistrar::class)->teamsKey => getPermissionsTeamId()] : [])); } return $role; @@ -149,12 +151,14 @@ protected static function findByParam(array $params = []) { $query = static::query(); - if (PermissionRegistrar::$teams) { - $query->where(function ($q) use ($params) { - $q->whereNull(PermissionRegistrar::$teamsKey) - ->orWhere(PermissionRegistrar::$teamsKey, $params[PermissionRegistrar::$teamsKey] ?? getPermissionsTeamId()); + if (app(PermissionRegistrar::class)->teams) { + $teamsKey = app(PermissionRegistrar::class)->teamsKey; + + $query->where(function ($q) use ($params, $teamsKey) { + $q->whereNull($teamsKey) + ->orWhere($teamsKey, $params[$teamsKey] ?? getPermissionsTeamId()); }); - unset($params[PermissionRegistrar::$teamsKey]); + unset($params[$teamsKey]); } foreach ($params as $key => $value) { diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 38f71e4bf..54a3a369f 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -29,25 +29,25 @@ class PermissionRegistrar protected $permissions; /** @var string */ - public static $pivotRole; + public $pivotRole; /** @var string */ - public static $pivotPermission; + public $pivotPermission; /** @var \DateInterval|int */ - public static $cacheExpirationTime; + public $cacheExpirationTime; /** @var bool */ - public static $teams; + public $teams; /** @var string */ - public static $teamsKey; + public $teamsKey; /** @var int|string */ protected $teamId = null; /** @var string */ - public static $cacheKey; + public $cacheKey; /** @var array */ private $cachedRoles = []; @@ -74,15 +74,15 @@ public function __construct(CacheManager $cacheManager) public function initializeCache() { - self::$cacheExpirationTime = config('permission.cache.expiration_time') ?: \DateInterval::createFromDateString('24 hours'); + $this->cacheExpirationTime = config('permission.cache.expiration_time') ?: \DateInterval::createFromDateString('24 hours'); - self::$teams = config('permission.teams', false); - self::$teamsKey = config('permission.column_names.team_foreign_key'); + $this->teams = config('permission.teams', false); + $this->teamsKey = config('permission.column_names.team_foreign_key'); - self::$cacheKey = config('permission.cache.key'); + $this->cacheKey = config('permission.cache.key'); - self::$pivotRole = config('permission.column_names.role_pivot_key') ?: 'role_id'; - self::$pivotPermission = config('permission.column_names.permission_pivot_key') ?: 'permission_id'; + $this->pivotRole = config('permission.column_names.role_pivot_key') ?: 'role_id'; + $this->pivotPermission = config('permission.column_names.permission_pivot_key') ?: 'permission_id'; $this->cache = $this->getCacheStoreFromConfig(); } @@ -151,7 +151,7 @@ public function forgetCachedPermissions() { $this->permissions = null; - return $this->cache->forget(self::$cacheKey); + return $this->cache->forget($this->cacheKey); } /** @@ -174,7 +174,7 @@ private function loadPermissions() return; } - $this->permissions = $this->cache->remember(self::$cacheKey, self::$cacheExpirationTime, function () { + $this->permissions = $this->cache->remember($this->cacheKey, $this->cacheExpirationTime, function () { return $this->getSerializedPermissionsForCache(); }); diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 0d85b998f..2cf849d78 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -32,10 +32,10 @@ public static function bootHasPermissions() return; } - $teams = PermissionRegistrar::$teams; - PermissionRegistrar::$teams = false; + $teams = app(PermissionRegistrar::class)->teams; + app(PermissionRegistrar::class)->teams = false; $model->permissions()->detach(); - PermissionRegistrar::$teams = $teams; + app(PermissionRegistrar::class)->teams = $teams; }); } @@ -77,14 +77,14 @@ public function permissions(): BelongsToMany 'model', config('permission.table_names.model_has_permissions'), config('permission.column_names.model_morph_key'), - PermissionRegistrar::$pivotPermission + app(PermissionRegistrar::class)->pivotPermission ); - if (! PermissionRegistrar::$teams) { + if (! app(PermissionRegistrar::class)->teams) { return $relation; } - return $relation->wherePivot(PermissionRegistrar::$teamsKey, getPermissionsTeamId()); + return $relation->wherePivot(app(PermissionRegistrar::class)->teamsKey, getPermissionsTeamId()); } /** @@ -361,8 +361,8 @@ public function collectPermissions(...$permissions) $this->ensureModelSharesGuard($permission); - $array[$permission->getKey()] = PermissionRegistrar::$teams && ! is_a($this, Role::class) ? - [PermissionRegistrar::$teamsKey => getPermissionsTeamId()] : []; + $array[$permission->getKey()] = app(PermissionRegistrar::class)->teams && ! is_a($this, Role::class) ? + [app(PermissionRegistrar::class)->teamsKey => getPermissionsTeamId()] : []; return $array; }, []); diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 8ab3732ad..eaa3c2b14 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -24,10 +24,10 @@ public static function bootHasRoles() return; } - $teams = PermissionRegistrar::$teams; - PermissionRegistrar::$teams = false; + $teams = app(PermissionRegistrar::class)->teams; + app(PermissionRegistrar::class)->teams = false; $model->roles()->detach(); - PermissionRegistrar::$teams = $teams; + app(PermissionRegistrar::class)->teams = $teams; }); } @@ -50,16 +50,16 @@ public function roles(): BelongsToMany 'model', config('permission.table_names.model_has_roles'), config('permission.column_names.model_morph_key'), - PermissionRegistrar::$pivotRole + app(PermissionRegistrar::class)->pivotRole ); - if (! PermissionRegistrar::$teams) { + if (! app(PermissionRegistrar::class)->teams) { return $relation; } - return $relation->wherePivot(PermissionRegistrar::$teamsKey, getPermissionsTeamId()) + return $relation->wherePivot(app(PermissionRegistrar::class)->teamsKey, getPermissionsTeamId()) ->where(function ($q) { - $teamField = config('permission.table_names.roles').'.'.PermissionRegistrar::$teamsKey; + $teamField = config('permission.table_names.roles').'.'. app(PermissionRegistrar::class)->teamsKey; $q->whereNull($teamField)->orWhere($teamField, getPermissionsTeamId()); }); } @@ -117,8 +117,8 @@ public function assignRole(...$roles) $this->ensureModelSharesGuard($role); - $array[$role->getKey()] = PermissionRegistrar::$teams && ! is_a($this, Permission::class) ? - [PermissionRegistrar::$teamsKey => getPermissionsTeamId()] : []; + $array[$role->getKey()] = app(PermissionRegistrar::class)->teams && ! is_a($this, Permission::class) ? + [app(PermissionRegistrar::class)->teamsKey => getPermissionsTeamId()] : []; return $array; }, []); From b1ff160a7a09057cbcdd1b56eda7813a0c4697f9 Mon Sep 17 00:00:00 2001 From: drbyte Date: Fri, 10 Feb 2023 23:22:22 +0000 Subject: [PATCH 0600/1013] Fix styling --- src/Traits/HasRoles.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index eaa3c2b14..de7d224a9 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -59,7 +59,7 @@ public function roles(): BelongsToMany return $relation->wherePivot(app(PermissionRegistrar::class)->teamsKey, getPermissionsTeamId()) ->where(function ($q) { - $teamField = config('permission.table_names.roles').'.'. app(PermissionRegistrar::class)->teamsKey; + $teamField = config('permission.table_names.roles').'.'.app(PermissionRegistrar::class)->teamsKey; $q->whereNull($teamField)->orWhere($teamField, getPermissionsTeamId()); }); } From 13e08f71efa1581b44be3d0389290db23ae10a3f Mon Sep 17 00:00:00 2001 From: erikn69 Date: Mon, 13 Feb 2023 08:56:21 -0500 Subject: [PATCH 0601/1013] Fix tests --- tests/TestCase.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/TestCase.php b/tests/TestCase.php index 9481feb84..b04d17839 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -160,8 +160,8 @@ private function prepareMigration() 'references(\'id\') // permission id', 'references(\'id\') // role id', 'bigIncrements', - 'unsignedBigInteger(PermissionRegistrar::$pivotRole)', - 'unsignedBigInteger(PermissionRegistrar::$pivotPermission)', + 'unsignedBigInteger($pivotRole)', + 'unsignedBigInteger($pivotPermission)', ], [ 'CreatePermissionCustomTables', @@ -170,8 +170,8 @@ private function prepareMigration() 'references(\'permission_test_id\')', 'references(\'role_test_id\')', 'uuid', - 'uuid(PermissionRegistrar::$pivotRole)->nullable(false)', - 'uuid(PermissionRegistrar::$pivotPermission)->nullable(false)', + 'uuid($pivotRole)->nullable(false)', + 'uuid($pivotPermission)->nullable(false)', ], file_get_contents(__DIR__.'/../database/migrations/create_permission_tables.php.stub') ); From 47a587a446c07f9040689e903c489f76eb7b850b Mon Sep 17 00:00:00 2001 From: "Mr. Alpaca" <85284773+JensvandeWiel@users.noreply.github.com> Date: Mon, 20 Feb 2023 15:16:18 +0100 Subject: [PATCH 0602/1013] Update middleware.md --- docs/basic-usage/middleware.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index 5d6ddae88..04391c683 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -17,6 +17,7 @@ Route::group(['middleware' => ['can:publish articles']], function () { This package comes with `RoleMiddleware`, `PermissionMiddleware` and `RoleOrPermissionMiddleware` middleware. You can add them inside your `app/Http/Kernel.php` file. +### Laravel 9 ```php protected $routeMiddleware = [ // ... @@ -25,6 +26,15 @@ protected $routeMiddleware = [ 'role_or_permission' => \Spatie\Permission\Middlewares\RoleOrPermissionMiddleware::class, ]; ``` +### Laravel 10 +```php +protected $middlewareAliases = [ + // ... + 'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class, + 'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class, + 'role_or_permission' => \Spatie\Permission\Middlewares\RoleOrPermissionMiddleware::class, +]; +``` Then you can protect your routes using middleware rules: From 490fa00b0c8286659a534d822f7e74c684c86d27 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 20 Feb 2023 15:01:00 -0500 Subject: [PATCH 0603/1013] [docs] Update middleware example for Laravel 10 --- docs/basic-usage/middleware.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index 04391c683..8a1971877 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -17,7 +17,9 @@ Route::group(['middleware' => ['can:publish articles']], function () { This package comes with `RoleMiddleware`, `PermissionMiddleware` and `RoleOrPermissionMiddleware` middleware. You can add them inside your `app/Http/Kernel.php` file. -### Laravel 9 +Note the differences between Laravel 10 and older versions of Laravel is the name of the `protected` property: + +### Laravel 9 (and older) ```php protected $routeMiddleware = [ // ... From 248e93f7f20b8b68e4182c0a32fb6f391f6b9c8b Mon Sep 17 00:00:00 2001 From: erikn69 Date: Wed, 1 Mar 2023 11:18:18 -0500 Subject: [PATCH 0604/1013] Avoid loss of all permissions/roles pivots on sync error --- src/Traits/HasPermissions.php | 7 ++++--- src/Traits/HasRoles.php | 22 +++++++++++++++++----- tests/HasPermissionsTest.php | 12 ++++++++++++ tests/HasRolesTest.php | 12 ++++++++++++ tests/RoleTest.php | 12 ++++++++++++ 5 files changed, 57 insertions(+), 8 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 7e2b91f53..ee6d07da1 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -343,9 +343,8 @@ public function getAllPermissions(): Collection * Returns permissions ids as array keys * * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions - * @return array */ - public function collectPermissions(...$permissions) + private function collectPermissions(...$permissions): array { return collect($permissions) ->flatten() @@ -376,7 +375,7 @@ public function collectPermissions(...$permissions) */ public function givePermissionTo(...$permissions) { - $permissions = $this->collectPermissions(...$permissions); + $permissions = $this->collectPermissions($permissions); $model = $this->getModel(); @@ -412,6 +411,8 @@ function ($object) use ($permissions, $model) { */ public function syncPermissions(...$permissions) { + $this->collectPermissions($permissions); + $this->permissions()->detach(); return $this->givePermissionTo($permissions); diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 78787df0f..267528c2e 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -96,14 +96,13 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder } /** - * Assign the given role to the model. + * Returns roles ids as array keys * - * @param array|string|int|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection ...$roles - * @return $this + * @param array|string|int|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles */ - public function assignRole(...$roles) + private function collectRoles(...$roles): array { - $roles = collect($roles) + return collect($roles) ->flatten() ->reduce(function ($array, $role) { if (empty($role)) { @@ -122,6 +121,17 @@ public function assignRole(...$roles) return $array; }, []); + } + + /** + * Assign the given role to the model. + * + * @param array|string|int|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection ...$roles + * @return $this + */ + public function assignRole(...$roles) + { + $roles = $this->collectRoles($roles); $model = $this->getModel(); @@ -175,6 +185,8 @@ public function removeRole($role) */ public function syncRoles(...$roles) { + $this->collectRoles($roles); + $this->roles()->detach(); return $this->assignRole($roles); diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index 61bba126b..a71867581 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -476,6 +476,18 @@ public function sync_permission_ignores_null_inputs() $this->assertFalse($this->testUser->hasDirectPermission('edit-news')); } + /** @test */ + public function sync_permission_error_does_not_detach_permissions() + { + $this->testUser->givePermissionTo('edit-news'); + + $this->expectException(PermissionDoesNotExist::class); + + $this->testUser->syncPermissions('edit-articles', 'permission-that-does-not-exist'); + + $this->assertTrue($this->testUser->fresh()->hasDirectPermission('edit-news')); + } + /** @test */ public function it_does_not_remove_already_associated_permissions_when_assigning_new_permissions() { diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 0031b58da..1e7dcc8d5 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -227,6 +227,18 @@ public function it_will_remove_all_roles_when_an_empty_array_is_passed_to_sync_r $this->assertFalse($this->testUser->hasRole('testRole2')); } + /** @test */ + public function sync_roles_error_does_not_detach_roles() + { + $this->testUser->assignRole('testRole'); + + $this->expectException(RoleDoesNotExist::class); + + $this->testUser->syncRoles('testRole2', 'role-that-does-not-exist'); + + $this->assertTrue($this->testUser->fresh()->hasRole('testRole')); + } + /** @test */ public function it_will_sync_roles_to_a_model_that_is_not_persisted() { diff --git a/tests/RoleTest.php b/tests/RoleTest.php index 35e287d50..e76582f55 100644 --- a/tests/RoleTest.php +++ b/tests/RoleTest.php @@ -155,6 +155,18 @@ public function it_will_remove_all_permissions_when_passing_an_empty_array_to_sy $this->assertFalse($this->testUserRole->hasPermissionTo('edit-news')); } + /** @test */ + public function sync_permission_error_does_not_detach_permissions() + { + $this->testUserRole->givePermissionTo('edit-news'); + + $this->expectException(PermissionDoesNotExist::class); + + $this->testUserRole->syncPermissions('edit-articles', 'permission-that-does-not-exist'); + + $this->assertTrue($this->testUserRole->fresh()->hasDirectPermission('edit-news')); + } + /** @test */ public function it_can_revoke_a_permission() { From 7a1e128e2e5ffceb03e5dcd91c53c4a4db5aac8f Mon Sep 17 00:00:00 2001 From: drbyte Date: Thu, 16 Mar 2023 20:32:05 +0000 Subject: [PATCH 0605/1013] Fix styling --- src/Commands/UpgradeForTeams.php | 1 - src/Contracts/Permission.php | 8 -------- src/Contracts/Role.php | 6 ------ src/Contracts/Wildcard.php | 1 - src/Guard.php | 4 ---- src/Models/Permission.php | 10 ---------- src/Models/Role.php | 3 --- src/PermissionRegistrar.php | 14 -------------- src/PermissionServiceProvider.php | 2 -- src/Traits/HasPermissions.php | 14 -------------- src/Traits/HasRoles.php | 9 --------- src/WildcardPermission.php | 14 -------------- src/helpers.php | 1 - tests/RoleWithNesting.php | 3 --- 14 files changed, 90 deletions(-) diff --git a/src/Commands/UpgradeForTeams.php b/src/Commands/UpgradeForTeams.php index 210ca4407..70c790bc1 100644 --- a/src/Commands/UpgradeForTeams.php +++ b/src/Commands/UpgradeForTeams.php @@ -78,7 +78,6 @@ protected function createMigration() * Build a warning regarding possible duplication * due to already existing migrations. * - * @param array $existingMigrations * @return string */ protected function getExistingMigrationsWarning(array $existingMigrations) diff --git a/src/Contracts/Permission.php b/src/Contracts/Permission.php index 91e0162a4..fe151d077 100644 --- a/src/Contracts/Permission.php +++ b/src/Contracts/Permission.php @@ -8,17 +8,13 @@ interface Permission { /** * A permission can be applied to roles. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function roles(): BelongsToMany; /** * Find a permission by its name. * - * @param string $name * @param string|null $guardName - * @return Permission * * @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist */ @@ -27,9 +23,7 @@ public static function findByName(string $name, $guardName): self; /** * Find a permission by its id. * - * @param int $id * @param string|null $guardName - * @return Permission * * @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist */ @@ -38,9 +32,7 @@ public static function findById(int $id, $guardName): self; /** * Find or Create a permission by its name and guard name. * - * @param string $name * @param string|null $guardName - * @return Permission */ public static function findOrCreate(string $name, $guardName): self; } diff --git a/src/Contracts/Role.php b/src/Contracts/Role.php index 28f3d4308..94773e4fa 100644 --- a/src/Contracts/Role.php +++ b/src/Contracts/Role.php @@ -8,15 +8,12 @@ interface Role { /** * A role may be given various permissions. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function permissions(): BelongsToMany; /** * Find a role by its name and guard name. * - * @param string $name * @param string|null $guardName * @return \Spatie\Permission\Contracts\Role * @@ -27,7 +24,6 @@ public static function findByName(string $name, $guardName): self; /** * Find a role by its id and guard name. * - * @param int $id * @param string|null $guardName * @return \Spatie\Permission\Contracts\Role * @@ -38,7 +34,6 @@ public static function findById(int $id, $guardName): self; /** * Find or create a role by its name and guard name. * - * @param string $name * @param string|null $guardName * @return \Spatie\Permission\Contracts\Role */ @@ -48,7 +43,6 @@ public static function findOrCreate(string $name, $guardName): self; * Determine if the user may perform the given permission. * * @param string|\Spatie\Permission\Contracts\Permission $permission - * @return bool */ public function hasPermissionTo($permission): bool; } diff --git a/src/Contracts/Wildcard.php b/src/Contracts/Wildcard.php index 4e9478bd5..d2aabd8f8 100644 --- a/src/Contracts/Wildcard.php +++ b/src/Contracts/Wildcard.php @@ -6,7 +6,6 @@ interface Wildcard { /** * @param string|Wildcard $permission - * @return bool */ public function implies($permission): bool; } diff --git a/src/Guard.php b/src/Guard.php index 395d4cfec..376ffb897 100644 --- a/src/Guard.php +++ b/src/Guard.php @@ -12,7 +12,6 @@ class Guard * as indicated by the presence of a $guard_name property or a guardName() method on the model. * * @param string|Model $model model class object or name - * @return Collection */ public static function getNames($model): Collection { @@ -45,9 +44,6 @@ public static function getNames($model): Collection * - filter for provider models matching the model $class being checked (important for Lumen) * - keys() gives just the names of the matched guards * - return collection of guard names - * - * @param string $class - * @return Collection */ protected static function getConfigAuthGuards(string $class): Collection { diff --git a/src/Models/Permission.php b/src/Models/Permission.php index d5a7e39be..247c812dc 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -84,9 +84,7 @@ public function users(): BelongsToMany /** * Find a permission by its name (and optionally guardName). * - * @param string $name * @param string|null $guardName - * @return \Spatie\Permission\Contracts\Permission * * @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist */ @@ -106,7 +104,6 @@ public static function findByName(string $name, $guardName = null): PermissionCo * * @param int|string $id * @param string|null $guardName - * @return \Spatie\Permission\Contracts\Permission * * @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist */ @@ -125,9 +122,7 @@ public static function findById($id, $guardName = null): PermissionContract /** * Find or create permission by its name (and optionally guardName). * - * @param string $name * @param string|null $guardName - * @return \Spatie\Permission\Contracts\Permission */ public static function findOrCreate(string $name, $guardName = null): PermissionContract { @@ -143,10 +138,6 @@ public static function findOrCreate(string $name, $guardName = null): Permission /** * Get the current cached permissions. - * - * @param array $params - * @param bool $onlyOne - * @return \Illuminate\Database\Eloquent\Collection */ protected static function getPermissions(array $params = [], bool $onlyOne = false): Collection { @@ -158,7 +149,6 @@ protected static function getPermissions(array $params = [], bool $onlyOne = fal /** * Get the current cached first permission. * - * @param array $params * @return \Spatie\Permission\Contracts\Permission */ protected static function getPermission(array $params = []): ?PermissionContract diff --git a/src/Models/Role.php b/src/Models/Role.php index fe9f10117..a18d80930 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -88,7 +88,6 @@ public function users(): BelongsToMany /** * Find a role by its name and guard name. * - * @param string $name * @param string|null $guardName * @return \Spatie\Permission\Contracts\Role|\Spatie\Permission\Models\Role * @@ -130,7 +129,6 @@ public static function findById($id, $guardName = null): RoleContract /** * Find or create role by its name (and optionally guardName). * - * @param string $name * @param string|null $guardName * @return \Spatie\Permission\Contracts\Role|\Spatie\Permission\Models\Role */ @@ -172,7 +170,6 @@ protected static function findByParam(array $params = []) * Determine if the user may perform the given permission. * * @param string|Permission $permission - * @return bool * * @throws \Spatie\Permission\Exceptions\GuardDoesNotMatch */ diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index a3fc36f12..986477129 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -60,8 +60,6 @@ class PermissionRegistrar /** * PermissionRegistrar constructor. - * - * @param \Illuminate\Cache\CacheManager $cacheManager */ public function __construct(CacheManager $cacheManager) { @@ -130,8 +128,6 @@ public function getPermissionsTeamId() /** * Register the permission check method on the gate. * We resolve the Gate fresh here, for benefit of long-running instances. - * - * @return bool */ public function registerPermissions(Gate $gate): bool { @@ -197,10 +193,6 @@ private function loadPermissions() /** * Get the permissions based on the passed params. - * - * @param array $params - * @param bool $onlyOne - * @return \Illuminate\Database\Eloquent\Collection */ public function getPermissions(array $params = [], bool $onlyOne = false): Collection { @@ -227,8 +219,6 @@ public function getPermissions(array $params = [], bool $onlyOne = false): Colle /** * Get an instance of the permission class. - * - * @return \Spatie\Permission\Contracts\Permission */ public function getPermissionClass(): Permission { @@ -246,8 +236,6 @@ public function setPermissionClass($permissionClass) /** * Get an instance of the role class. - * - * @return \Spatie\Permission\Contracts\Role */ public function getRoleClass(): Role { @@ -280,8 +268,6 @@ protected function getPermissionsWithRoles(): Collection /** * Changes array keys with alias - * - * @return array */ private function aliasedArray($model): array { diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 4abcd2cb8..bce255014 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -168,8 +168,6 @@ protected function registerMacroHelpers() /** * Returns existing migration file if found, else uses the current timestamp. - * - * @return string */ protected function getMigrationFileName($migrationFileName): string { diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index ee6d07da1..fbea0a7ec 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -90,9 +90,7 @@ public function permissions(): BelongsToMany /** * Scope the model query to certain permissions only. * - * @param \Illuminate\Database\Eloquent\Builder $query * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions - * @return \Illuminate\Database\Eloquent\Builder */ public function scopePermission(Builder $query, $permissions): Builder { @@ -120,7 +118,6 @@ public function scopePermission(Builder $query, $permissions): Builder /** * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions - * @return array * * @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist */ @@ -178,7 +175,6 @@ public function filterPermission($permission, $guardName = null) * * @param string|int|\Spatie\Permission\Contracts\Permission $permission * @param string|null $guardName - * @return bool * * @throws PermissionDoesNotExist */ @@ -198,7 +194,6 @@ public function hasPermissionTo($permission, $guardName = null): bool * * @param string|int|\Spatie\Permission\Contracts\Permission $permission * @param string|null $guardName - * @return bool */ protected function hasWildcardPermission($permission, $guardName = null): bool { @@ -238,7 +233,6 @@ protected function hasWildcardPermission($permission, $guardName = null): bool * * @param string|int|\Spatie\Permission\Contracts\Permission $permission * @param string|null $guardName - * @return bool */ public function checkPermissionTo($permission, $guardName = null): bool { @@ -253,7 +247,6 @@ public function checkPermissionTo($permission, $guardName = null): bool * Determine if the model has any of the given permissions. * * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions - * @return bool */ public function hasAnyPermission(...$permissions): bool { @@ -272,7 +265,6 @@ public function hasAnyPermission(...$permissions): bool * Determine if the model has all of the given permissions. * * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions - * @return bool */ public function hasAllPermissions(...$permissions): bool { @@ -289,9 +281,6 @@ public function hasAllPermissions(...$permissions): bool /** * Determine if the model has, via roles, the given permission. - * - * @param \Spatie\Permission\Contracts\Permission $permission - * @return bool */ protected function hasPermissionViaRole(Permission $permission): bool { @@ -302,7 +291,6 @@ protected function hasPermissionViaRole(Permission $permission): bool * Determine if the model has the given permission. * * @param string|int|\Spatie\Permission\Contracts\Permission $permission - * @return bool * * @throws PermissionDoesNotExist */ @@ -506,7 +494,6 @@ public function forgetCachedPermissions() * Check if the model has All of the requested Direct permissions. * * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions - * @return bool */ public function hasAllDirectPermissions(...$permissions): bool { @@ -525,7 +512,6 @@ public function hasAllDirectPermissions(...$permissions): bool * Check if the model has Any of the requested Direct permissions. * * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions - * @return bool */ public function hasAnyDirectPermission(...$permissions): bool { diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 267528c2e..fac948e06 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -67,10 +67,8 @@ public function roles(): BelongsToMany /** * Scope the model query to certain roles only. * - * @param \Illuminate\Database\Eloquent\Builder $query * @param string|int|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles * @param string $guard - * @return \Illuminate\Database\Eloquent\Builder */ public function scopeRole(Builder $query, $roles, $guard = null): Builder { @@ -196,8 +194,6 @@ public function syncRoles(...$roles) * Determine if the model has (one of) the given role(s). * * @param string|int|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles - * @param string|null $guard - * @return bool */ public function hasRole($roles, string $guard = null): bool { @@ -245,7 +241,6 @@ public function hasRole($roles, string $guard = null): bool * Alias to hasRole() but without Guard controls * * @param string|int|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles - * @return bool */ public function hasAnyRole(...$roles): bool { @@ -256,8 +251,6 @@ public function hasAnyRole(...$roles): bool * Determine if the model has all of the given role(s). * * @param string|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles - * @param string|null $guard - * @return bool */ public function hasAllRoles($roles, string $guard = null): bool { @@ -292,8 +285,6 @@ public function hasAllRoles($roles, string $guard = null): bool * Determine if the model has exactly all of the given role(s). * * @param string|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles - * @param string|null $guard - * @return bool */ public function hasExactRoles($roles, string $guard = null): bool { diff --git a/src/WildcardPermission.php b/src/WildcardPermission.php index da1ce33bf..f8946a5c5 100644 --- a/src/WildcardPermission.php +++ b/src/WildcardPermission.php @@ -23,9 +23,6 @@ class WildcardPermission implements Wildcard /** @var Collection */ protected $parts; - /** - * @param string $permission - */ public function __construct(string $permission) { $this->permission = $permission; @@ -36,7 +33,6 @@ public function __construct(string $permission) /** * @param string|WildcardPermission $permission - * @return bool */ public function implies($permission): bool { @@ -70,11 +66,6 @@ public function implies($permission): bool return true; } - /** - * @param Collection $part - * @param Collection $otherPart - * @return bool - */ protected function containsAll(Collection $part, Collection $otherPart): bool { foreach ($otherPart->toArray() as $item) { @@ -86,9 +77,6 @@ protected function containsAll(Collection $part, Collection $otherPart): bool return true; } - /** - * @return Collection - */ public function getParts(): Collection { return $this->parts; @@ -96,8 +84,6 @@ public function getParts(): Collection /** * Sets the different parts and subparts from permission string. - * - * @return void */ protected function setParts(): void { diff --git a/src/helpers.php b/src/helpers.php index ac6fc3699..b29354655 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -2,7 +2,6 @@ if (! function_exists('getModelForGuard')) { /** - * @param string $guard * @return string|null */ function getModelForGuard(string $guard) diff --git a/tests/RoleWithNesting.php b/tests/RoleWithNesting.php index 9a8c33a6a..0be2b3076 100644 --- a/tests/RoleWithNesting.php +++ b/tests/RoleWithNesting.php @@ -25,9 +25,6 @@ public function parents() 'parent_id'); } - /** - * @return BelongsToMany - */ public function children(): BelongsToMany { return $this->belongsToMany( From 1c16bcd4a88d69ce2aa3147e9ce308bd6ad39a87 Mon Sep 17 00:00:00 2001 From: Faraz Samapoor Date: Sun, 19 Mar 2023 11:28:07 +0330 Subject: [PATCH 0606/1013] Rephrases teams-permissions.md to improve the readability. --- docs/basic-usage/teams-permissions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/basic-usage/teams-permissions.md b/docs/basic-usage/teams-permissions.md index 7c2bf2f54..9ff4b3364 100644 --- a/docs/basic-usage/teams-permissions.md +++ b/docs/basic-usage/teams-permissions.md @@ -23,8 +23,8 @@ Also, if you want to use a custom foreign key for teams you must change in the p ## Working with Teams Permissions -After implements on login a solution for select a team on authentication (for example set `team_id` of the current selected team on **session**: `session(['team_id' => $team->team_id]);` ), -we can set global `team_id` from anywhere, but works better if you create a `Middleware`, example: +After implementing a solution for selecting a team on the authentication process (for example, setting the `team_id` of the currently selected team on the **session**: `session(['team_id' => $team->team_id]);` ), +we can set global `team_id` from anywhere, but works better if you create a `Middleware`. Example: ```php namespace App\Http\Middleware; From f96456de92637fed4b716017c5991442cf185c84 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 19 Mar 2023 16:23:10 -0400 Subject: [PATCH 0607/1013] [Docs] Update teams-permissions.md for readability --- docs/basic-usage/teams-permissions.md | 31 +++++++++++++++++---------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/docs/basic-usage/teams-permissions.md b/docs/basic-usage/teams-permissions.md index 9ff4b3364..1908765a7 100644 --- a/docs/basic-usage/teams-permissions.md +++ b/docs/basic-usage/teams-permissions.md @@ -3,9 +3,14 @@ title: Teams permissions weight: 3 --- -NOTE: Those changes must be made before performing the migration. If you have already run the migration and want to upgrade your solution, you can run the artisan console command `php artisan permission:setup-teams`, to create a new migration file named [xxxx_xx_xx_xx_add_teams_fields.php](https://github.com/spatie/laravel-permission/blob/main/database/migrations/add_teams_fields.php.stub) and then run `php artisan migrate` to upgrade your database tables. +When enabled, teams permissions offers you flexible control for a variety of scenarios. The idea behind teams permissions is inspired by the default permission implementation of [Laratrust](https://laratrust.santigarcor.me/). -When enabled, teams permissions offers you a flexible control for a variety of scenarios. The idea behind teams permissions is inspired by the default permission implementation of [Laratrust](https://laratrust.santigarcor.me/). + +## Enabling Teams Permissions Feature + +NOTE: These configuration changes must be made **before** performing the migration when first installing the package. + +If you have already run the migration and want to upgrade your implementation, you can run the artisan console command `php artisan spatie-permission:setup-teams`, to create a new migration file named [xxxx_xx_xx_xx_add_teams_fields.php](https://github.com/spatie/laravel-permission/blob/main/database/migrations/add_teams_fields.php.stub) and then run `php artisan migrate` to upgrade your database tables. Teams permissions can be enabled in the permission config file: @@ -15,7 +20,7 @@ Teams permissions can be enabled in the permission config file: 'teams' => true, ``` -Also, if you want to use a custom foreign key for teams you must change in the permission config file: +Also, if you want to use a custom foreign key for teams you set it in the permission config file: ```php // config/permission.php 'team_foreign_key' => 'custom_team_id', @@ -24,7 +29,9 @@ Also, if you want to use a custom foreign key for teams you must change in the p ## Working with Teams Permissions After implementing a solution for selecting a team on the authentication process (for example, setting the `team_id` of the currently selected team on the **session**: `session(['team_id' => $team->team_id]);` ), -we can set global `team_id` from anywhere, but works better if you create a `Middleware`. Example: +we can set global `team_id` from anywhere, but works better if you create a `Middleware`. + +Example Team Middleware: ```php namespace App\Http\Middleware; @@ -46,17 +53,17 @@ class TeamsPermission{ } } ``` -NOTE: You must add your custom `Middleware` to `$middlewarePriority` on `app/Http/Kernel.php`. +NOTE: You must add your custom `Middleware` to `$middlewarePriority` in `app/Http/Kernel.php`. ## Roles Creating When creating a role you can pass the `team_id` as an optional parameter ```php -// with null team_id it creates a global role, global roles can be assigned to any team and they are unique +// with null team_id it creates a global role; global roles can be assigned to any team and they are unique Role::create(['name' => 'writer', 'team_id' => null]); -// creates a role with team_id = 1, team roles can have the same name on different teams +// creates a role with team_id = 1; team roles can have the same name on different teams Role::create(['name' => 'reader', 'team_id' => 1]); // creating a role without team_id makes the role take the default global team_id @@ -65,11 +72,13 @@ Role::create(['name' => 'reviewer']); ## Roles/Permissions Assignment & Removal -The role/permission assignment and removal are the same, but they take the global `team_id` set on login for sync. +The role/permission assignment and removal for teams are the same as without teams, but they take the global `team_id` set on login for sync. ## Defining a Super-Admin on Teams -Global roles can be assigned to different teams, `team_id` as the primary key of the relationships is always required. If you want a "Super Admin" global role for a user, when you creates a new team you must assign it to your user. Example: +Global roles can be assigned to different teams, and `team_id` as the primary key of the relationships is always required. + +If you want a "Super Admin" global role for a user, when you create a new team you must assign it to your user. Example: ```php namespace App\Models; @@ -83,13 +92,13 @@ class YourTeamModel extends \Illuminate\Database\Eloquent\Model // here assign this team to a global user with global default role self::created(function ($model) { - // get session team_id for restore it later + // temporary: get session team_id for restore at end $session_team_id = getPermissionsTeamId(); // set actual new team_id to package instance setPermissionsTeamId($model); // get the admin user and assign roles/permissions on new team model User::find('your_user_id')->assignRole('Super Admin'); - // restore session team_id to package instance + // restore session team_id to package instance using temporary value stored above setPermissionsTeamId($session_team_id); }); } From 21367074ab4177fdbf4c9fbc2a4ebeabb853266a Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 19 Mar 2023 16:29:45 -0400 Subject: [PATCH 0608/1013] [Docs] Update teams-permissions.md for readability --- docs/basic-usage/teams-permissions.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/basic-usage/teams-permissions.md b/docs/basic-usage/teams-permissions.md index 1908765a7..d62fa92c8 100644 --- a/docs/basic-usage/teams-permissions.md +++ b/docs/basic-usage/teams-permissions.md @@ -5,14 +5,12 @@ weight: 3 When enabled, teams permissions offers you flexible control for a variety of scenarios. The idea behind teams permissions is inspired by the default permission implementation of [Laratrust](https://laratrust.santigarcor.me/). - ## Enabling Teams Permissions Feature NOTE: These configuration changes must be made **before** performing the migration when first installing the package. If you have already run the migration and want to upgrade your implementation, you can run the artisan console command `php artisan spatie-permission:setup-teams`, to create a new migration file named [xxxx_xx_xx_xx_add_teams_fields.php](https://github.com/spatie/laravel-permission/blob/main/database/migrations/add_teams_fields.php.stub) and then run `php artisan migrate` to upgrade your database tables. - Teams permissions can be enabled in the permission config file: ```php @@ -28,7 +26,8 @@ Also, if you want to use a custom foreign key for teams you set it in the permis ## Working with Teams Permissions -After implementing a solution for selecting a team on the authentication process (for example, setting the `team_id` of the currently selected team on the **session**: `session(['team_id' => $team->team_id]);` ), +After implementing a solution for selecting a team on the authentication process +(for example, setting the `team_id` of the currently selected team on the **session**: `session(['team_id' => $team->team_id]);` ), we can set global `team_id` from anywhere, but works better if you create a `Middleware`. Example Team Middleware: @@ -72,11 +71,11 @@ Role::create(['name' => 'reviewer']); ## Roles/Permissions Assignment & Removal -The role/permission assignment and removal for teams are the same as without teams, but they take the global `team_id` set on login for sync. +The role/permission assignment and removal for teams are the same as without teams, but they take the global `team_id` which is set on login. ## Defining a Super-Admin on Teams -Global roles can be assigned to different teams, and `team_id` as the primary key of the relationships is always required. +Global roles can be assigned to different teams, and `team_id` (which is the primary key of the relationships) is always required. If you want a "Super Admin" global role for a user, when you create a new team you must assign it to your user. Example: From 33543616f673f30a1146f8f338b58ecb54180efc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Mar 2023 06:01:16 +0000 Subject: [PATCH 0609/1013] Bump aglipanci/laravel-pint-action from 2.1.0 to 2.2.0 Bumps [aglipanci/laravel-pint-action](https://github.com/aglipanci/laravel-pint-action) from 2.1.0 to 2.2.0. - [Release notes](https://github.com/aglipanci/laravel-pint-action/releases) - [Commits](https://github.com/aglipanci/laravel-pint-action/compare/2.1.0...2.2.0) --- updated-dependencies: - dependency-name: aglipanci/laravel-pint-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/fix-php-code-style-issues.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/fix-php-code-style-issues.yml b/.github/workflows/fix-php-code-style-issues.yml index 39a77e7b3..f76938275 100644 --- a/.github/workflows/fix-php-code-style-issues.yml +++ b/.github/workflows/fix-php-code-style-issues.yml @@ -16,7 +16,7 @@ jobs: ref: ${{ github.head_ref }} - name: Fix PHP code style issues - uses: aglipanci/laravel-pint-action@2.1.0 + uses: aglipanci/laravel-pint-action@2.2.0 - name: Commit changes uses: stefanzweifel/git-auto-commit-action@v4 From aebf4c942f10b175d5a3f3b881816fe0b3a5900d Mon Sep 17 00:00:00 2001 From: erikn69 Date: Mon, 20 Mar 2023 14:33:03 -0500 Subject: [PATCH 0610/1013] Fix nesting tests --- src/Models/Permission.php | 6 +- src/Models/Role.php | 2 +- tests/Role.php | 26 +++++++ tests/RoleWithNesting.php | 36 --------- tests/RoleWithNestingTest.php | 77 +++++++++---------- .../roles_with_nesting_migration.php.stub | 40 ---------- 6 files changed, 64 insertions(+), 123 deletions(-) delete mode 100644 tests/RoleWithNesting.php delete mode 100644 tests/customMigrations/roles_with_nesting_migration.php.stub diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 247c812dc..e7b428c9d 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -34,11 +34,7 @@ public function __construct(array $attributes = []) parent::__construct($attributes); $this->guarded[] = $this->primaryKey; - } - - public function getTable() - { - return config('permission.table_names.permissions', parent::getTable()); + $this->table = config('permission.table_names.permissions') ?: parent::getTable(); } public static function create(array $attributes = []) diff --git a/src/Models/Role.php b/src/Models/Role.php index a18d80930..b8ad70353 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -34,7 +34,7 @@ public function __construct(array $attributes = []) parent::__construct($attributes); $this->guarded[] = $this->primaryKey; - $this->table = config('permission.table_names.roles', parent::getTable()); + $this->table = config('permission.table_names.roles') ?: parent::getTable(); } public static function create(array $attributes = []) diff --git a/tests/Role.php b/tests/Role.php index 79f8cdbfd..86bdd609f 100644 --- a/tests/Role.php +++ b/tests/Role.php @@ -11,6 +11,32 @@ class Role extends \Spatie\Permission\Models\Role 'name', ]; + const HIERARCHY_TABLE = 'roles_hierarchy'; + + /** + * @return BelongsToMany + */ + public function parents() + { + return $this->belongsToMany( + static::class, + static::HIERARCHY_TABLE, + 'child_id', + 'parent_id'); + } + + /** + * @return BelongsToMany + */ + public function children() + { + return $this->belongsToMany( + static::class, + static::HIERARCHY_TABLE, + 'parent_id', + 'child_id'); + } + protected static function boot() { parent::boot(); diff --git a/tests/RoleWithNesting.php b/tests/RoleWithNesting.php deleted file mode 100644 index 0be2b3076..000000000 --- a/tests/RoleWithNesting.php +++ /dev/null @@ -1,36 +0,0 @@ -belongsToMany( - static::class, - static::HIERARCHY_TABLE, - 'child_id', - 'parent_id'); - } - - public function children(): BelongsToMany - { - return $this->belongsToMany( - static::class, - static::HIERARCHY_TABLE, - 'parent_id', - 'child_id'); - } -} diff --git a/tests/RoleWithNestingTest.php b/tests/RoleWithNestingTest.php index 4261dc03f..6fcc82e73 100644 --- a/tests/RoleWithNestingTest.php +++ b/tests/RoleWithNestingTest.php @@ -4,63 +4,58 @@ class RoleWithNestingTest extends TestCase { - private static $old_migration; + /** @var bool */ + protected $useCustomModels = true; - /** - * @var RoleWithNesting[] - */ + /** @var Role[] */ protected $parent_roles = []; - /** - * @var RoleWithNesting[] - */ + /** @var Role[] */ protected $child_roles = []; - public static function setUpBeforeClass(): void - { - parent::setUpBeforeClass(); - self::$old_migration = self::$migration; - self::$migration = self::getMigration(); - } public function setUp(): void { parent::setUp(); - $this->parent_roles = []; - $this->child_roles = []; - $this->parent_roles['has_no_children'] = RoleWithNesting::create(['name' => 'has_no_children']); - $this->parent_roles['has_1_child'] = RoleWithNesting::create(['name' => 'has_1_child']); - $this->parent_roles['has_3_children'] = RoleWithNesting::create(['name' => 'has_3_children']); - $this->child_roles['has_no_parents'] = RoleWithNesting::create(['name' => 'has_no_parents']); - $this->child_roles['has_1_parent'] = RoleWithNesting::create(['name' => 'has_1_parent']); - $this->child_roles['has_2_parents'] = RoleWithNesting::create(['name' => 'has_2_parents']); - $this->child_roles['third_child'] = RoleWithNesting::create(['name' => 'third_child']); + $this->parent_roles = [ + 'has_no_children' => Role::create(['name' => 'has_no_children']), + 'has_1_child' => Role::create(['name' => 'has_1_child']), + 'has_3_children' => Role::create(['name' => 'has_3_children']), + ]; + $this->child_roles = [ + 'has_no_parents' => Role::create(['name' => 'has_no_parents']), + 'has_1_parent' => Role::create(['name' => 'has_1_parent']), + 'has_2_parents' => Role::create(['name' => 'has_2_parents']), + 'third_child' => Role::create(['name' => 'third_child']), + ]; $this->parent_roles['has_1_child']->children()->attach($this->child_roles['has_2_parents']); - $this->parent_roles['has_3_children']->children()->attach($this->child_roles['has_2_parents']); - $this->parent_roles['has_3_children']->children()->attach($this->child_roles['has_1_parent']); - $this->parent_roles['has_3_children']->children()->attach($this->child_roles['third_child']); + $this->parent_roles['has_3_children']->children()->attach([ + $this->child_roles['has_2_parents']->getKey(), + $this->child_roles['has_1_parent']->getKey(), + $this->child_roles['third_child']->getKey() + ]); } - public static function tearDownAfterClass(): void - { - parent::tearDownAfterClass(); - self::$migration = self::$old_migration; - } - - protected function getEnvironmentSetUp($app) + /** + * Set up the database. + * + * @param \Illuminate\Foundation\Application $app + */ + protected function setUpDatabase($app) { - parent::getEnvironmentSetUp($app); - $app['config']->set('permission.models.role', RoleWithNesting::class); - $app['config']->set('permission.table_names.roles', 'nesting_role'); - } + parent::setUpDatabase($app); - protected static function getMigration() - { - require_once __DIR__.'/customMigrations/roles_with_nesting_migration.php.stub'; + $tableRoles = $app['config']->get('permission.table_names.roles'); - return new \CreatePermissionTablesWithNested(); + $app['db']->connection()->getSchemaBuilder()->create(Role::HIERARCHY_TABLE, function ($table) use ($tableRoles) { + $table->id(); + $table->uuid("parent_id"); + $table->uuid("child_id"); + $table->foreign("parent_id")->references("role_test_id")->on($tableRoles); + $table->foreign("child_id")->references("role_test_id")->on($tableRoles); + }); } /** @test @@ -71,7 +66,7 @@ public function it_returns_correct_withCount_of_nested_roles($role_group, $index $role = $this->$role_group[$index]; $count_field_name = sprintf('%s_count', $relation); - $actualCount = intval(RoleWithNesting::query()->withCount($relation)->find($role->id)->$count_field_name); + $actualCount = intval(Role::withCount($relation)->find($role->getKey())->$count_field_name); $this->assertSame( $expectedCount, diff --git a/tests/customMigrations/roles_with_nesting_migration.php.stub b/tests/customMigrations/roles_with_nesting_migration.php.stub deleted file mode 100644 index 81909275c..000000000 --- a/tests/customMigrations/roles_with_nesting_migration.php.stub +++ /dev/null @@ -1,40 +0,0 @@ -id(); - $table->bigInteger("parent_id", false, true); - $table->bigInteger("child_id", false, true); - $table->foreign("parent_id")->references("id")->on($tableNames['roles']); - $table->foreign("child_id")->references("id")->on($tableNames['roles']); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - parent::down(); - Schema::drop(\Spatie\Permission\Test\RoleWithNesting::HIERARCHY_TABLE); - } -} From 090b129cce224d54ea3d62047adde2284bdd3d0f Mon Sep 17 00:00:00 2001 From: erikn69 Date: Tue, 21 Mar 2023 09:02:05 -0500 Subject: [PATCH 0611/1013] Fix delete permissions on Permissions Model --- src/Traits/HasPermissions.php | 4 ++ tests/HasPermissionsTest.php | 2 +- tests/HasPermissionsWithCustomModelsTest.php | 50 +++++++++++++++++++ tests/HasRolesTest.php | 2 +- tests/HasRolesWithCustomModelsTest.php | 51 ++++++++++++++++++++ tests/Permission.php | 4 ++ tests/Role.php | 4 ++ tests/TestCase.php | 13 ++++- 8 files changed, 126 insertions(+), 4 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index fbea0a7ec..04a8b8abe 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -32,6 +32,10 @@ public static function bootHasPermissions() return; } + if (is_a($model, Permission::class)) { + return; + } + $teams = app(PermissionRegistrar::class)->teams; app(PermissionRegistrar::class)->teams = false; $model->permissions()->detach(); diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index a71867581..d6f584f75 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -217,7 +217,7 @@ public function it_throws_an_exception_when_trying_to_scope_a_permission_from_an } /** @test */ - public function it_doesnt_detach_permissions_when_soft_deleting() + public function it_doesnt_detach_permissions_when_user_soft_deleting() { $user = SoftDeletingUser::create(['email' => 'test@example.com']); $user->givePermissionTo(['edit-news']); diff --git a/tests/HasPermissionsWithCustomModelsTest.php b/tests/HasPermissionsWithCustomModelsTest.php index b491c8e05..28b7145bd 100644 --- a/tests/HasPermissionsWithCustomModelsTest.php +++ b/tests/HasPermissionsWithCustomModelsTest.php @@ -63,4 +63,54 @@ public function it_can_scope_users_using_a_uuid() $this->assertEquals(2, $scopedUsers1->count()); $this->assertEquals(1, $scopedUsers2->count()); } + + /** @test */ + public function it_doesnt_detach_roles_when_soft_deleting() + { + $this->testUserRole->givePermissionTo($this->testUserPermission); + + DB::enableQueryLog(); + $this->testUserPermission->delete(); + DB::disableQueryLog(); + + $this->assertSame(1, count(DB::getQueryLog())); + + $permission = Permission::onlyTrashed()->find($this->testUserPermission->getKey()); + + $this->assertEquals(1, DB::table(config('permission.table_names.role_has_permissions'))->where('permission_test_id', $this->testUserPermission->getKey())->count()); + } + + /** @test */ + public function it_doesnt_detach_users_when_soft_deleting() + { + $this->testUser->givePermissionTo($this->testUserPermission); + + DB::enableQueryLog(); + $this->testUserPermission->delete(); + DB::disableQueryLog(); + + $this->assertSame(1, count(DB::getQueryLog())); + + $permission = Permission::onlyTrashed()->find($this->testUserPermission->getKey()); + $permission->restore(); + + $this->assertEquals(1, DB::table(config('permission.table_names.model_has_permissions'))->where('permission_test_id', $this->testUserPermission->getKey())->count()); + } + + /** @test */ + public function it_does_detach_roles_when_force_deleting() + { + $this->testUserRole->givePermissionTo($this->testUserPermission); + + DB::enableQueryLog(); + $this->testUserPermission->forceDelete(); + DB::disableQueryLog(); + + $this->assertSame(2, count(DB::getQueryLog())); //avoid detach permissions on permissions + + $permission = Permission::withTrashed()->find($this->testUserPermission->getKey()); + + $this->assertNull($permission); + $this->assertEquals(0, DB::table(config('permission.table_names.role_has_permissions'))->where('permission_test_id', $this->testUserPermission->getKey())->count()); + } } diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 1e7dcc8d5..43f7f6217 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -584,7 +584,7 @@ public function it_can_retrieve_role_names() } /** @test */ - public function it_does_not_detach_roles_when_soft_deleting() + public function it_does_not_detach_roles_when_user_soft_deleting() { $user = SoftDeletingUser::create(['email' => 'test@example.com']); $user->assignRole('testRole'); diff --git a/tests/HasRolesWithCustomModelsTest.php b/tests/HasRolesWithCustomModelsTest.php index d39739205..fe6ea443d 100644 --- a/tests/HasRolesWithCustomModelsTest.php +++ b/tests/HasRolesWithCustomModelsTest.php @@ -2,6 +2,8 @@ namespace Spatie\Permission\Test; +use DB; + class HasRolesWithCustomModelsTest extends HasRolesTest { /** @var bool */ @@ -12,4 +14,53 @@ public function it_can_use_custom_model_role() { $this->assertSame(get_class($this->testUserRole), Role::class); } + + /** @test */ + public function it_doesnt_detach_permissions_when_soft_deleting() + { + $this->testUserRole->givePermissionTo($this->testUserPermission); + + DB::enableQueryLog(); + $this->testUserRole->delete(); + DB::disableQueryLog(); + + $this->assertSame(1, count(DB::getQueryLog())); + + $role = Role::onlyTrashed()->find($this->testUserRole->getKey()); + + $this->assertEquals(1, DB::table(config('permission.table_names.role_has_permissions'))->where('role_test_id', $this->testUserRole->getKey())->count()); + } + + /** @test */ + public function it_doesnt_detach_users_when_soft_deleting() + { + $this->testUser->assignRole($this->testUserRole); + + DB::enableQueryLog(); + $this->testUserRole->delete(); + DB::disableQueryLog(); + + $this->assertSame(1, count(DB::getQueryLog())); + + $role = Role::onlyTrashed()->find($this->testUserRole->getKey()); + + $this->assertEquals(1, DB::table(config('permission.table_names.model_has_roles'))->where('role_test_id', $this->testUserRole->getKey())->count()); + } + + /** @test */ + public function it_does_detach_permissions_when_force_deleting() + { + $this->testUserRole->givePermissionTo($this->testUserPermission); + + DB::enableQueryLog(); + $this->testUserRole->forceDelete(); + DB::disableQueryLog(); + + $this->assertSame(2, count(DB::getQueryLog())); + + $role = Role::withTrashed()->find($this->testUserRole->getKey()); + + $this->assertNull($role); + $this->assertEquals(0, DB::table(config('permission.table_names.role_has_permissions'))->where('role_test_id', $this->testUserRole->getKey())->count()); + } } diff --git a/tests/Permission.php b/tests/Permission.php index 9d8f2cd91..be35d20d5 100644 --- a/tests/Permission.php +++ b/tests/Permission.php @@ -2,8 +2,12 @@ namespace Spatie\Permission\Test; +use Illuminate\Database\Eloquent\SoftDeletes; + class Permission extends \Spatie\Permission\Models\Permission { + use SoftDeletes; + protected $primaryKey = 'permission_test_id'; protected $visible = [ diff --git a/tests/Role.php b/tests/Role.php index 79f8cdbfd..fc09476f2 100644 --- a/tests/Role.php +++ b/tests/Role.php @@ -2,8 +2,12 @@ namespace Spatie\Permission\Test; +use Illuminate\Database\Eloquent\SoftDeletes; + class Role extends \Spatie\Permission\Models\Role { + use SoftDeletes; + protected $primaryKey = 'role_test_id'; protected $visible = [ diff --git a/tests/TestCase.php b/tests/TestCase.php index b04d17839..67fc537dd 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -116,13 +116,15 @@ protected function getEnvironmentSetUp($app) */ protected function setUpDatabase($app) { - $app['db']->connection()->getSchemaBuilder()->create('users', function (Blueprint $table) { + $schema = $app['db']->connection()->getSchemaBuilder(); + + $schema->create('users', function (Blueprint $table) { $table->increments('id'); $table->string('email'); $table->softDeletes(); }); - $app['db']->connection()->getSchemaBuilder()->create('admins', function (Blueprint $table) { + $schema->create('admins', function (Blueprint $table) { $table->increments('id'); $table->string('email'); }); @@ -136,6 +138,13 @@ protected function setUpDatabase($app) self::$migration->up(); } else { self::$customMigration->up(); + + $schema->table(config('permission.table_names.roles'), function (Blueprint $table) { + $table->softDeletes(); + }); + $schema->table(config('permission.table_names.permissions'), function (Blueprint $table) { + $table->softDeletes(); + }); } $this->testUser = User::create(['email' => 'test@user.com']); From c7e0eb2851bcc0bdb42fd82d25ff8236aec463b8 Mon Sep 17 00:00:00 2001 From: drbyte Date: Wed, 22 Mar 2023 03:00:16 +0000 Subject: [PATCH 0612/1013] Update CHANGELOG --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebd548ee1..ec4980c00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.10.0 - 2023-03-22 + +### What's Changed + +- Fix delete permissions on Permissions Model by @erikn69 in https://github.com/spatie/laravel-permission/pull/2366 + ## 5.9.1 - 2023-02-06 Apologies for the break caused by 5.9.0 ! @@ -593,6 +599,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -646,6 +653,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From 25cb3a14faf5311e9344bd7b2e3d9a09bf6fd893 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Wed, 22 Mar 2023 08:53:00 -0500 Subject: [PATCH 0613/1013] Detach users on role/permission physical deletion --- src/Traits/HasPermissions.php | 13 +++++++------ src/Traits/HasRoles.php | 5 ++++- tests/HasPermissionsWithCustomModelsTest.php | 18 ++++++++++-------- tests/HasRolesWithCustomModelsTest.php | 17 ++++++++++------- 4 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 04a8b8abe..432723fbf 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -32,20 +32,21 @@ public static function bootHasPermissions() return; } - if (is_a($model, Permission::class)) { - return; - } - $teams = app(PermissionRegistrar::class)->teams; app(PermissionRegistrar::class)->teams = false; - $model->permissions()->detach(); + if (! is_a($model, Permission::class)) { + $model->permissions()->detach(); + } + if (is_a($model, Role::class)) { + $model->users()->detach(); + } app(PermissionRegistrar::class)->teams = $teams; }); } public function getPermissionClass() { - if (! isset($this->permissionClass)) { + if (! $this->permissionClass) { $this->permissionClass = app(PermissionRegistrar::class)->getPermissionClass(); } diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index fac948e06..7982c781c 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -27,13 +27,16 @@ public static function bootHasRoles() $teams = app(PermissionRegistrar::class)->teams; app(PermissionRegistrar::class)->teams = false; $model->roles()->detach(); + if (is_a($model, Permission::class)) { + $model->users()->detach(); + } app(PermissionRegistrar::class)->teams = $teams; }); } public function getRoleClass() { - if (! isset($this->roleClass)) { + if (! $this->roleClass) { $this->roleClass = app(PermissionRegistrar::class)->getRoleClass(); } diff --git a/tests/HasPermissionsWithCustomModelsTest.php b/tests/HasPermissionsWithCustomModelsTest.php index 28b7145bd..57f3310a6 100644 --- a/tests/HasPermissionsWithCustomModelsTest.php +++ b/tests/HasPermissionsWithCustomModelsTest.php @@ -77,7 +77,7 @@ public function it_doesnt_detach_roles_when_soft_deleting() $permission = Permission::onlyTrashed()->find($this->testUserPermission->getKey()); - $this->assertEquals(1, DB::table(config('permission.table_names.role_has_permissions'))->where('permission_test_id', $this->testUserPermission->getKey())->count()); + $this->assertEquals(1, DB::table(config('permission.table_names.role_has_permissions'))->where('permission_test_id', $permission->getKey())->count()); } /** @test */ @@ -92,25 +92,27 @@ public function it_doesnt_detach_users_when_soft_deleting() $this->assertSame(1, count(DB::getQueryLog())); $permission = Permission::onlyTrashed()->find($this->testUserPermission->getKey()); - $permission->restore(); - $this->assertEquals(1, DB::table(config('permission.table_names.model_has_permissions'))->where('permission_test_id', $this->testUserPermission->getKey())->count()); + $this->assertEquals(1, DB::table(config('permission.table_names.model_has_permissions'))->where('permission_test_id', $permission->getKey())->count()); } /** @test */ - public function it_does_detach_roles_when_force_deleting() + public function it_does_detach_roles_and_users_when_force_deleting() { - $this->testUserRole->givePermissionTo($this->testUserPermission); + $permission_id = $this->testUserPermission->getKey(); + $this->testUserRole->givePermissionTo($permission_id); + $this->testUser->givePermissionTo($permission_id); DB::enableQueryLog(); $this->testUserPermission->forceDelete(); DB::disableQueryLog(); - $this->assertSame(2, count(DB::getQueryLog())); //avoid detach permissions on permissions + $this->assertSame(3, count(DB::getQueryLog())); //avoid detach permissions on permissions - $permission = Permission::withTrashed()->find($this->testUserPermission->getKey()); + $permission = Permission::withTrashed()->find($permission_id); $this->assertNull($permission); - $this->assertEquals(0, DB::table(config('permission.table_names.role_has_permissions'))->where('permission_test_id', $this->testUserPermission->getKey())->count()); + $this->assertEquals(0, DB::table(config('permission.table_names.role_has_permissions'))->where('permission_test_id', $permission_id)->count()); + $this->assertEquals(0, DB::table(config('permission.table_names.model_has_permissions'))->where('permission_test_id', $permission_id)->count()); } } diff --git a/tests/HasRolesWithCustomModelsTest.php b/tests/HasRolesWithCustomModelsTest.php index fe6ea443d..3d7db9bb0 100644 --- a/tests/HasRolesWithCustomModelsTest.php +++ b/tests/HasRolesWithCustomModelsTest.php @@ -28,7 +28,7 @@ public function it_doesnt_detach_permissions_when_soft_deleting() $role = Role::onlyTrashed()->find($this->testUserRole->getKey()); - $this->assertEquals(1, DB::table(config('permission.table_names.role_has_permissions'))->where('role_test_id', $this->testUserRole->getKey())->count()); + $this->assertEquals(1, DB::table(config('permission.table_names.role_has_permissions'))->where('role_test_id', $role->getKey())->count()); } /** @test */ @@ -44,23 +44,26 @@ public function it_doesnt_detach_users_when_soft_deleting() $role = Role::onlyTrashed()->find($this->testUserRole->getKey()); - $this->assertEquals(1, DB::table(config('permission.table_names.model_has_roles'))->where('role_test_id', $this->testUserRole->getKey())->count()); + $this->assertEquals(1, DB::table(config('permission.table_names.model_has_roles'))->where('role_test_id', $role->getKey())->count()); } /** @test */ - public function it_does_detach_permissions_when_force_deleting() + public function it_does_detach_permissions_and_users_when_force_deleting() { - $this->testUserRole->givePermissionTo($this->testUserPermission); + $role_id = $this->testUserRole->getKey(); + $this->testUserPermission->assignRole($role_id); + $this->testUser->assignRole($role_id); DB::enableQueryLog(); $this->testUserRole->forceDelete(); DB::disableQueryLog(); - $this->assertSame(2, count(DB::getQueryLog())); + $this->assertSame(3, count(DB::getQueryLog())); - $role = Role::withTrashed()->find($this->testUserRole->getKey()); + $role = Role::withTrashed()->find($role_id); $this->assertNull($role); - $this->assertEquals(0, DB::table(config('permission.table_names.role_has_permissions'))->where('role_test_id', $this->testUserRole->getKey())->count()); + $this->assertEquals(0, DB::table(config('permission.table_names.role_has_permissions'))->where('role_test_id', $role_id)->count()); + $this->assertEquals(0, DB::table(config('permission.table_names.model_has_roles'))->where('role_test_id', $role_id)->count()); } } From ae8bb2108f048ab40b30a7403397962b7d0a255f Mon Sep 17 00:00:00 2001 From: erikn69 Date: Tue, 21 Mar 2023 12:57:46 -0500 Subject: [PATCH 0614/1013] Change clearClassPermissions to clearPermissionsCollection --- src/PermissionRegistrar.php | 13 +++++++++++-- src/PermissionServiceProvider.php | 2 +- tests/PermissionRegistarTest.php | 15 +++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 986477129..ade4413bf 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -151,15 +151,24 @@ public function forgetCachedPermissions() } /** - * Clear class permissions. + * Clear already loaded permissions collection. * This is only intended to be called by the PermissionServiceProvider on boot, * so that long-running instances like Swoole don't keep old data in memory. */ - public function clearClassPermissions() + public function clearPermissionsCollection(): void { $this->permissions = null; } + /** + * @deprecated + * @alias of clearPermissionsCollection() + */ + public function clearClassPermissions() + { + $this->clearPermissionsCollection(); + } + /** * Load permissions from cache * This get cache and turns array into \Illuminate\Database\Eloquent\Collection diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index bce255014..e80b292a2 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -29,7 +29,7 @@ public function boot() if ($this->app->config['permission.register_permission_check_method']) { /** @var PermissionRegistrar $permissionLoader */ $permissionLoader = $app->get(PermissionRegistrar::class); - $permissionLoader->clearClassPermissions(); + $permissionLoader->clearPermissionsCollection(); $permissionLoader->registerPermissions($gate); } }); diff --git a/tests/PermissionRegistarTest.php b/tests/PermissionRegistarTest.php index b864b3e90..ac73bbaf8 100644 --- a/tests/PermissionRegistarTest.php +++ b/tests/PermissionRegistarTest.php @@ -6,6 +6,21 @@ class PermissionRegistarTest extends TestCase { + /** @test */ + public function it_can_clear_loaded_permissions_collection() { + $reflectedClass = new \ReflectionClass(app(PermissionRegistrar::class)); + $reflectedProperty = $reflectedClass->getProperty('permissions'); + $reflectedProperty->setAccessible(true); + + app(PermissionRegistrar::class)->getPermissions(); + + $this->assertNotNull($reflectedProperty->getValue(app(PermissionRegistrar::class))); + + app(PermissionRegistrar::class)->clearPermissionsCollection(); + + $this->assertNull($reflectedProperty->getValue(app(PermissionRegistrar::class))); + } + /** @test */ public function it_can_check_uids() { From cbfe2dff35d6d10736f66fd3cb3c9d5ee05db5ee Mon Sep 17 00:00:00 2001 From: drbyte Date: Wed, 22 Mar 2023 20:35:48 +0000 Subject: [PATCH 0615/1013] Fix styling --- src/PermissionRegistrar.php | 1 + tests/PermissionRegistarTest.php | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index ade4413bf..aa5a6dcdc 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -162,6 +162,7 @@ public function clearPermissionsCollection(): void /** * @deprecated + * * @alias of clearPermissionsCollection() */ public function clearClassPermissions() diff --git a/tests/PermissionRegistarTest.php b/tests/PermissionRegistarTest.php index ac73bbaf8..4d07a9e82 100644 --- a/tests/PermissionRegistarTest.php +++ b/tests/PermissionRegistarTest.php @@ -7,7 +7,8 @@ class PermissionRegistarTest extends TestCase { /** @test */ - public function it_can_clear_loaded_permissions_collection() { + public function it_can_clear_loaded_permissions_collection() + { $reflectedClass = new \ReflectionClass(app(PermissionRegistrar::class)); $reflectedProperty = $reflectedClass->getProperty('permissions'); $reflectedProperty->setAccessible(true); From 746c0e9dedda969497de6c5f959bc3937a87217e Mon Sep 17 00:00:00 2001 From: drbyte Date: Wed, 22 Mar 2023 20:43:25 +0000 Subject: [PATCH 0616/1013] Fix styling --- tests/RoleWithNestingTest.php | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/tests/RoleWithNestingTest.php b/tests/RoleWithNestingTest.php index 6fcc82e73..4872a0876 100644 --- a/tests/RoleWithNestingTest.php +++ b/tests/RoleWithNestingTest.php @@ -13,28 +13,27 @@ class RoleWithNestingTest extends TestCase /** @var Role[] */ protected $child_roles = []; - public function setUp(): void { parent::setUp(); $this->parent_roles = [ - 'has_no_children' => Role::create(['name' => 'has_no_children']), - 'has_1_child' => Role::create(['name' => 'has_1_child']), - 'has_3_children' => Role::create(['name' => 'has_3_children']), + 'has_no_children' => Role::create(['name' => 'has_no_children']), + 'has_1_child' => Role::create(['name' => 'has_1_child']), + 'has_3_children' => Role::create(['name' => 'has_3_children']), ]; $this->child_roles = [ - 'has_no_parents' => Role::create(['name' => 'has_no_parents']), - 'has_1_parent' => Role::create(['name' => 'has_1_parent']), - 'has_2_parents' => Role::create(['name' => 'has_2_parents']), - 'third_child' => Role::create(['name' => 'third_child']), + 'has_no_parents' => Role::create(['name' => 'has_no_parents']), + 'has_1_parent' => Role::create(['name' => 'has_1_parent']), + 'has_2_parents' => Role::create(['name' => 'has_2_parents']), + 'third_child' => Role::create(['name' => 'third_child']), ]; $this->parent_roles['has_1_child']->children()->attach($this->child_roles['has_2_parents']); $this->parent_roles['has_3_children']->children()->attach([ $this->child_roles['has_2_parents']->getKey(), $this->child_roles['has_1_parent']->getKey(), - $this->child_roles['third_child']->getKey() + $this->child_roles['third_child']->getKey(), ]); } @@ -51,10 +50,10 @@ protected function setUpDatabase($app) $app['db']->connection()->getSchemaBuilder()->create(Role::HIERARCHY_TABLE, function ($table) use ($tableRoles) { $table->id(); - $table->uuid("parent_id"); - $table->uuid("child_id"); - $table->foreign("parent_id")->references("role_test_id")->on($tableRoles); - $table->foreign("child_id")->references("role_test_id")->on($tableRoles); + $table->uuid('parent_id'); + $table->uuid('child_id'); + $table->foreign('parent_id')->references('role_test_id')->on($tableRoles); + $table->foreign('child_id')->references('role_test_id')->on($tableRoles); }); } From bfc3e3bc5600d000d05ebd55ba6cea2f75cbb621 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Wed, 22 Mar 2023 15:53:39 -0500 Subject: [PATCH 0617/1013] Use anonymous migrations --- database/migrations/add_teams_fields.php.stub | 8 ++++---- .../migrations/create_permission_tables.php.stub | 8 ++++---- tests/CommandTest.php | 6 +++--- tests/TestCase.php | 12 ++++-------- 4 files changed, 15 insertions(+), 19 deletions(-) diff --git a/database/migrations/add_teams_fields.php.stub b/database/migrations/add_teams_fields.php.stub index be79ee047..9fb25ac09 100644 --- a/database/migrations/add_teams_fields.php.stub +++ b/database/migrations/add_teams_fields.php.stub @@ -5,14 +5,14 @@ use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; -class AddTeamsFields extends Migration +return new class extends Migration { /** * Run the migrations. * * @return void */ - public function up() + public function up(): void { $teams = config('permission.teams'); $tableNames = config('permission.table_names'); @@ -88,8 +88,8 @@ class AddTeamsFields extends Migration * * @return void */ - public function down() + public function down(): void { } -} +}; diff --git a/database/migrations/create_permission_tables.php.stub b/database/migrations/create_permission_tables.php.stub index 4874c05a8..5a7301abe 100644 --- a/database/migrations/create_permission_tables.php.stub +++ b/database/migrations/create_permission_tables.php.stub @@ -4,14 +4,14 @@ use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; -class CreatePermissionTables extends Migration +return new class extends Migration { /** * Run the migrations. * * @return void */ - public function up() + public function up(): void { $teams = config('permission.teams'); $tableNames = config('permission.table_names'); @@ -125,7 +125,7 @@ class CreatePermissionTables extends Migration * * @return void */ - public function down() + public function down(): void { $tableNames = config('permission.table_names'); @@ -139,4 +139,4 @@ class CreatePermissionTables extends Migration Schema::drop($tableNames['roles']); Schema::drop($tableNames['permissions']); } -} +}; diff --git a/tests/CommandTest.php b/tests/CommandTest.php index be1541f88..f1088f0e7 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -143,9 +143,9 @@ public function it_can_setup_teams_upgrade() $matchingFiles = glob(database_path('migrations/*_add_teams_fields.php')); $this->assertTrue(count($matchingFiles) > 0); - include_once $matchingFiles[count($matchingFiles) - 1]; - (new \AddTeamsFields())->up(); - (new \AddTeamsFields())->up(); //test upgrade teams migration fresh + $AddTeamsFields = require($matchingFiles[count($matchingFiles) - 1]); + $AddTeamsFields->up(); + $AddTeamsFields->up(); //test upgrade teams migration fresh Role::create(['name' => 'new-role', 'team_test_id' => 1]); $role = Role::where('name', 'new-role')->first(); diff --git a/tests/TestCase.php b/tests/TestCase.php index 67fc537dd..0c86ff007 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -163,7 +163,6 @@ private function prepareMigration() { $migration = str_replace( [ - 'CreatePermissionTables', '(\'id\'); // permission id', '(\'id\'); // role id', 'references(\'id\') // permission id', @@ -173,7 +172,6 @@ private function prepareMigration() 'unsignedBigInteger($pivotPermission)', ], [ - 'CreatePermissionCustomTables', '(\'permission_test_id\');', '(\'role_test_id\');', 'references(\'permission_test_id\')', @@ -186,12 +184,10 @@ private function prepareMigration() ); file_put_contents(__DIR__.'/CreatePermissionCustomTables.php', $migration); - - include_once __DIR__.'/../database/migrations/create_permission_tables.php.stub'; - self::$migration = new \CreatePermissionTables(); - - include_once __DIR__.'/CreatePermissionCustomTables.php'; - self::$customMigration = new \CreatePermissionCustomTables(); + + self::$migration = require(__DIR__.'/../database/migrations/create_permission_tables.php.stub'); + + self::$customMigration = require(__DIR__.'/CreatePermissionCustomTables.php'); } protected function reloadPermissions() From b55c2c80cf4a95bad00cecf8de5959dc32633ca7 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 22 Mar 2023 20:25:08 -0400 Subject: [PATCH 0618/1013] Refactor Tests - update namespace from `Test` to `Tests` - namespace models into `Tests\TestModels` - update PHPUnit setUp() fixture (protected, not public) Thanks to @AyoobMH - ref PR #2240 Co-authored-by: AyoobMH --- composer.json | 2 +- tests/BladeTest.php | 6 +-- tests/CacheTest.php | 5 +- tests/CommandTest.php | 2 +- tests/CustomGateTest.php | 2 +- tests/GateTest.php | 2 +- tests/HasPermissionsTest.php | 4 +- tests/HasPermissionsWithCustomModelsTest.php | 6 ++- tests/HasRolesTest.php | 7 ++- tests/HasRolesWithCustomModelsTest.php | 5 +- tests/MultipleGuardsTest.php | 3 +- tests/PermissionMiddlewareTest.php | 15 +----- tests/PermissionRegistarTest.php | 2 +- tests/PermissionTest.php | 3 +- tests/RoleMiddlewareTest.php | 15 +----- tests/RoleOrPermissionMiddlewareTest.php | 15 +----- tests/RoleTest.php | 7 ++- tests/RoleWithNestingTest.php | 6 ++- tests/RouteTest.php | 21 +-------- tests/TeamHasPermissionsTest.php | 4 +- tests/TeamHasRolesTest.php | 3 +- tests/TestCase.php | 46 ++++++++++++++++--- tests/TestHelper.php | 2 +- tests/{ => TestModels}/Admin.php | 2 +- tests/{ => TestModels}/Manager.php | 2 +- tests/{ => TestModels}/Permission.php | 2 +- tests/{ => TestModels}/Role.php | 2 +- tests/{ => TestModels}/RuntimeRole.php | 2 +- tests/{ => TestModels}/SoftDeletingUser.php | 2 +- tests/{ => TestModels}/User.php | 2 +- tests/{ => TestModels}/WildcardPermission.php | 2 +- tests/WildcardHasPermissionsTest.php | 4 +- tests/WildcardMiddlewareTest.php | 15 +----- tests/WildcardRoleTest.php | 4 +- tests/WildcardRouteTest.php | 21 +-------- 35 files changed, 108 insertions(+), 135 deletions(-) rename tests/{ => TestModels}/Admin.php (92%) rename tests/{ => TestModels}/Manager.php (95%) rename tests/{ => TestModels}/Permission.php (93%) rename tests/{ => TestModels}/Role.php (96%) rename tests/{ => TestModels}/RuntimeRole.php (74%) rename tests/{ => TestModels}/SoftDeletingUser.php (76%) rename tests/{ => TestModels}/User.php (92%) rename tests/{ => TestModels}/WildcardPermission.php (87%) diff --git a/composer.json b/composer.json index afefa994d..759ef75a5 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ }, "autoload-dev": { "psr-4": { - "Spatie\\Permission\\Test\\": "tests" + "Spatie\\Permission\\Tests\\": "tests" } }, "config": { diff --git a/tests/BladeTest.php b/tests/BladeTest.php index 21d66437f..02a899083 100644 --- a/tests/BladeTest.php +++ b/tests/BladeTest.php @@ -1,13 +1,13 @@ runMiddleware($this->permissionMiddleware, 'admin-permission', 'admin') ); } - - protected function runMiddleware($middleware, $permission, $guard = null) - { - try { - return $middleware->handle(new Request(), function () { - return (new Response())->setContent(''); - }, $permission, $guard)->status(); - } catch (UnauthorizedException $e) { - return $e->getStatusCode(); - } - } } diff --git a/tests/PermissionRegistarTest.php b/tests/PermissionRegistarTest.php index 4d07a9e82..1e52362b5 100644 --- a/tests/PermissionRegistarTest.php +++ b/tests/PermissionRegistarTest.php @@ -1,6 +1,6 @@ runMiddleware($this->roleMiddleware, 'testAdminRole', 'admin') ); } - - protected function runMiddleware($middleware, $roleName, $guard = null) - { - try { - return $middleware->handle(new Request(), function () { - return (new Response())->setContent(''); - }, $roleName, $guard)->status(); - } catch (UnauthorizedException $e) { - return $e->getStatusCode(); - } - } } diff --git a/tests/RoleOrPermissionMiddlewareTest.php b/tests/RoleOrPermissionMiddlewareTest.php index 345f0209a..a6e8147e2 100644 --- a/tests/RoleOrPermissionMiddlewareTest.php +++ b/tests/RoleOrPermissionMiddlewareTest.php @@ -1,6 +1,6 @@ assertStringEndsWith('Necessary roles or permissions are some-permission, some-role', $message); } - - protected function runMiddleware($middleware, $name, $guard = null) - { - try { - return $middleware->handle(new Request(), function () { - return (new Response())->setContent(''); - }, $name, $guard)->status(); - } catch (UnauthorizedException $e) { - return $e->getStatusCode(); - } - } } diff --git a/tests/RoleTest.php b/tests/RoleTest.php index e76582f55..7cff50d8c 100644 --- a/tests/RoleTest.php +++ b/tests/RoleTest.php @@ -1,6 +1,6 @@ getLastRouteMiddlewareFromRouter($router) ); } - - protected function getLastRouteMiddlewareFromRouter($router) - { - return last($router->getRoutes()->get())->middleware(); - } - - protected function getRouter() - { - return app('router'); - } - - protected function getRouteResponse() - { - return function () { - return (new Response())->setContent(''); - }; - } } diff --git a/tests/TeamHasPermissionsTest.php b/tests/TeamHasPermissionsTest.php index ccc3de93a..54fe106f0 100644 --- a/tests/TeamHasPermissionsTest.php +++ b/tests/TeamHasPermissionsTest.php @@ -1,6 +1,8 @@ set('auth.guards.admin', ['driver' => 'session', 'provider' => 'admins']); $app['config']->set('auth.providers.admins', ['driver' => 'eloquent', 'model' => Admin::class]); if ($this->useCustomModels) { - $app['config']->set('permission.models.permission', \Spatie\Permission\Test\Permission::class); - $app['config']->set('permission.models.role', \Spatie\Permission\Test\Role::class); + $app['config']->set('permission.models.permission', \Spatie\Permission\Tests\TestModels\Permission::class); + $app['config']->set('permission.models.role', \Spatie\Permission\Tests\TestModels\Role::class); } // Use test User model for users provider $app['config']->set('auth.providers.users.model', User::class); @@ -219,4 +223,34 @@ public function setUpRoutes(): void ]; }); } + + + ////// TEST HELPERS + public function runMiddleware($middleware, $permission, $guard = null) + { + try { + return $middleware->handle(new Request(), function () { + return (new Response())->setContent(''); + }, $permission, $guard)->status(); + } catch (UnauthorizedException $e) { + return $e->getStatusCode(); + } + } + + public function getLastRouteMiddlewareFromRouter($router) + { + return last($router->getRoutes()->get())->middleware(); + } + + public function getRouter() + { + return app('router'); + } + + public function getRouteResponse() + { + return function () { + return (new Response())->setContent(''); + }; + } } diff --git a/tests/TestHelper.php b/tests/TestHelper.php index c081d2f3d..5b8efa5cd 100644 --- a/tests/TestHelper.php +++ b/tests/TestHelper.php @@ -1,6 +1,6 @@ assertEquals(['permission.some'], $requiredPermissions); } - - protected function runMiddleware($middleware, $parameter) - { - try { - return $middleware->handle(new Request(), function () { - return (new Response())->setContent(''); - }, $parameter)->status(); - } catch (UnauthorizedException $e) { - return $e->getStatusCode(); - } - } } diff --git a/tests/WildcardRoleTest.php b/tests/WildcardRoleTest.php index 6d69c9d2c..da674c6f6 100644 --- a/tests/WildcardRoleTest.php +++ b/tests/WildcardRoleTest.php @@ -1,12 +1,12 @@ getLastRouteMiddlewareFromRouter($router) ); } - - protected function getLastRouteMiddlewareFromRouter($router) - { - return last($router->getRoutes()->get())->middleware(); - } - - protected function getRouter() - { - return app('router'); - } - - protected function getRouteResponse() - { - return function () { - return (new Response())->setContent(''); - }; - } } From 58b8a011f449f297f7872635a9fa22fb6d23cc71 Mon Sep 17 00:00:00 2001 From: drbyte Date: Thu, 23 Mar 2023 00:25:47 +0000 Subject: [PATCH 0619/1013] Fix styling --- tests/RoleTest.php | 2 +- tests/TestCase.php | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/RoleTest.php b/tests/RoleTest.php index 7cff50d8c..68d7c1aee 100644 --- a/tests/RoleTest.php +++ b/tests/RoleTest.php @@ -10,8 +10,8 @@ use Spatie\Permission\Models\Permission; use Spatie\Permission\PermissionRegistrar; use Spatie\Permission\Tests\TestModels\Admin; -use Spatie\Permission\Tests\TestModels\User; use Spatie\Permission\Tests\TestModels\RuntimeRole; +use Spatie\Permission\Tests\TestModels\User; class RoleTest extends TestCase { diff --git a/tests/TestCase.php b/tests/TestCase.php index c28ac0355..bc06463f3 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -11,9 +11,9 @@ use Orchestra\Testbench\TestCase as Orchestra; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Contracts\Role; +use Spatie\Permission\Exceptions\UnauthorizedException; use Spatie\Permission\PermissionRegistrar; use Spatie\Permission\PermissionServiceProvider; -use Spatie\Permission\Exceptions\UnauthorizedException; use Spatie\Permission\Tests\TestModels\Admin; use Spatie\Permission\Tests\TestModels\User; @@ -224,7 +224,6 @@ public function setUpRoutes(): void }); } - ////// TEST HELPERS public function runMiddleware($middleware, $permission, $guard = null) { @@ -238,9 +237,9 @@ public function runMiddleware($middleware, $permission, $guard = null) } public function getLastRouteMiddlewareFromRouter($router) - { - return last($router->getRoutes()->get())->middleware(); - } + { + return last($router->getRoutes()->get())->middleware(); + } public function getRouter() { From 8a0c4b4707c05ea05d63a5777b487effd4daf836 Mon Sep 17 00:00:00 2001 From: drbyte Date: Fri, 24 Mar 2023 04:01:47 +0000 Subject: [PATCH 0620/1013] Fix styling --- tests/CommandTest.php | 2 +- tests/TestCase.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/CommandTest.php b/tests/CommandTest.php index 0da59ee37..1ded858fe 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -143,7 +143,7 @@ public function it_can_setup_teams_upgrade() $matchingFiles = glob(database_path('migrations/*_add_teams_fields.php')); $this->assertTrue(count($matchingFiles) > 0); - $AddTeamsFields = require($matchingFiles[count($matchingFiles) - 1]); + $AddTeamsFields = require $matchingFiles[count($matchingFiles) - 1]; $AddTeamsFields->up(); $AddTeamsFields->up(); //test upgrade teams migration fresh diff --git a/tests/TestCase.php b/tests/TestCase.php index 0b7d6cfd4..a708ac196 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -188,10 +188,10 @@ private function prepareMigration() ); file_put_contents(__DIR__.'/CreatePermissionCustomTables.php', $migration); - - self::$migration = require(__DIR__.'/../database/migrations/create_permission_tables.php.stub'); - - self::$customMigration = require(__DIR__.'/CreatePermissionCustomTables.php'); + + self::$migration = require __DIR__.'/../database/migrations/create_permission_tables.php.stub'; + + self::$customMigration = require __DIR__.'/CreatePermissionCustomTables.php'; } protected function reloadPermissions() From c3122123b86e69bf370de97fac8e7bce069aca3e Mon Sep 17 00:00:00 2001 From: erikn69 Date: Tue, 21 Mar 2023 11:23:57 -0500 Subject: [PATCH 0621/1013] Return string on getPermissionClass(), getRoleClass() --- src/Models/Role.php | 6 ++-- src/PermissionRegistrar.php | 16 +++------- src/Traits/HasPermissions.php | 29 ++++++++--------- src/Traits/HasRoles.php | 14 ++++----- tests/PermissionRegistarTest.php | 53 ++++++++++++++++++++++++++++++++ 5 files changed, 78 insertions(+), 40 deletions(-) diff --git a/src/Models/Role.php b/src/Models/Role.php index b8ad70353..feb11738e 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -179,14 +179,12 @@ public function hasPermissionTo($permission): bool return $this->hasWildcardPermission($permission, $this->getDefaultGuardName()); } - $permissionClass = $this->getPermissionClass(); - if (is_string($permission)) { - $permission = $permissionClass->findByName($permission, $this->getDefaultGuardName()); + $permission = $this->getPermissionClass()::findByName($permission, $this->getDefaultGuardName()); } if (is_int($permission)) { - $permission = $permissionClass->findById($permission, $this->getDefaultGuardName()); + $permission = $this->getPermissionClass()::findById($permission, $this->getDefaultGuardName()); } if (! $this->getGuardNames()->contains($permission->guard_name)) { diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index aa5a6dcdc..2b64eed6b 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -227,12 +227,9 @@ public function getPermissions(array $params = [], bool $onlyOne = false): Colle return $permissions; } - /** - * Get an instance of the permission class. - */ - public function getPermissionClass(): Permission + public function getPermissionClass(): string { - return app($this->permissionClass); + return $this->permissionClass; } public function setPermissionClass($permissionClass) @@ -244,12 +241,9 @@ public function setPermissionClass($permissionClass) return $this; } - /** - * Get an instance of the role class. - */ - public function getRoleClass(): Role + public function getRoleClass(): string { - return app($this->roleClass); + return $this->roleClass; } public function setRoleClass($roleClass) @@ -273,7 +267,7 @@ public function getCacheStore(): Store protected function getPermissionsWithRoles(): Collection { - return $this->getPermissionClass()->select()->with('roles')->get(); + return $this->permissionClass::select()->with('roles')->get(); } /** diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 432723fbf..4160a3346 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -44,7 +44,7 @@ public static function bootHasPermissions() }); } - public function getPermissionClass() + public function getPermissionClass(): string { if (! $this->permissionClass) { $this->permissionClass = app(PermissionRegistrar::class)->getPermissionClass(); @@ -138,7 +138,7 @@ protected function convertToPermissionModels($permissions): array } $method = is_string($permission) && ! PermissionRegistrar::isUid($permission) ? 'findByName' : 'findById'; - return $this->getPermissionClass()->{$method}($permission, $this->getDefaultGuardName()); + return $this->getPermissionClass()::{$method}($permission, $this->getDefaultGuardName()); }, Arr::wrap($permissions)); } @@ -152,17 +152,15 @@ protected function convertToPermissionModels($permissions): array */ public function filterPermission($permission, $guardName = null) { - $permissionClass = $this->getPermissionClass(); - if (is_string($permission) && ! PermissionRegistrar::isUid($permission)) { - $permission = $permissionClass->findByName( + $permission = $this->getPermissionClass()::findByName( $permission, $guardName ?? $this->getDefaultGuardName() ); } if (is_int($permission) || is_string($permission)) { - $permission = $permissionClass->findById( + $permission = $this->getPermissionClass()::findById( $permission, $guardName ?? $this->getDefaultGuardName() ); @@ -205,7 +203,7 @@ protected function hasWildcardPermission($permission, $guardName = null): bool $guardName = $guardName ?? $this->getDefaultGuardName(); if (is_int($permission) || PermissionRegistrar::isUid($permission)) { - $permission = $this->getPermissionClass()->findById($permission, $guardName); + $permission = $this->getPermissionClass()::findById($permission, $guardName); } if ($permission instanceof Permission) { @@ -389,7 +387,7 @@ function ($object) use ($permissions, $model) { ); } - if (is_a($this, get_class(app(PermissionRegistrar::class)->getRoleClass()))) { + if (is_a($this, Role::class)) { $this->forgetCachedPermissions(); } @@ -421,7 +419,7 @@ public function revokePermissionTo($permission) { $this->permissions()->detach($this->getStoredPermission($permission)); - if (is_a($this, get_class(app(PermissionRegistrar::class)->getRoleClass()))) { + if (is_a($this, Role::class)) { $this->forgetCachedPermissions(); } @@ -441,23 +439,20 @@ public function getPermissionNames(): Collection */ protected function getStoredPermission($permissions) { - $permissionClass = $this->getPermissionClass(); - if (is_numeric($permissions) || PermissionRegistrar::isUid($permissions)) { - return $permissionClass->findById($permissions, $this->getDefaultGuardName()); + return $this->getPermissionClass()::findById($permissions, $this->getDefaultGuardName()); } if (is_string($permissions)) { - return $permissionClass->findByName($permissions, $this->getDefaultGuardName()); + return $this->getPermissionClass()::findByName($permissions, $this->getDefaultGuardName()); } if (is_array($permissions)) { - $permissions = array_map(function ($permission) use ($permissionClass) { - return is_a($permission, get_class($permissionClass)) ? $permission->name : $permission; + $permissions = array_map(function ($permission) { + return is_a($permission, Permission::class) ? $permission->name : $permission; }, $permissions); - return $permissionClass - ->whereIn('name', $permissions) + return $this->getPermissionClass()::whereIn('name', $permissions) ->whereIn('guard_name', $this->getGuardNames()) ->get(); } diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 7982c781c..99ec852f3 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -34,7 +34,7 @@ public static function bootHasRoles() }); } - public function getRoleClass() + public function getRoleClass(): string { if (! $this->roleClass) { $this->roleClass = app(PermissionRegistrar::class)->getRoleClass(); @@ -86,7 +86,7 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder $method = is_numeric($role) || PermissionRegistrar::isUid($role) ? 'findById' : 'findByName'; - return $this->getRoleClass()->{$method}($role, $guard ?: $this->getDefaultGuardName()); + return $this->getRoleClass()::{$method}($role, $guard ?: $this->getDefaultGuardName()); }, Arr::wrap($roles)); return $query->whereHas('roles', function (Builder $subQuery) use ($roles) { @@ -153,7 +153,7 @@ function ($object) use ($roles, $model) { ); } - if (is_a($this, get_class($this->getPermissionClass()))) { + if (is_a($this, Permission::class)) { $this->forgetCachedPermissions(); } @@ -171,7 +171,7 @@ public function removeRole($role) $this->load('roles'); - if (is_a($this, get_class($this->getPermissionClass()))) { + if (is_a($this, Permission::class)) { $this->forgetCachedPermissions(); } @@ -329,14 +329,12 @@ public function getRoleNames(): Collection protected function getStoredRole($role): Role { - $roleClass = $this->getRoleClass(); - if (is_numeric($role) || PermissionRegistrar::isUid($role)) { - return $roleClass->findById($role, $this->getDefaultGuardName()); + return $this->getRoleClass()::findById($role, $this->getDefaultGuardName()); } if (is_string($role)) { - return $roleClass->findByName($role, $this->getDefaultGuardName()); + return $this->getRoleClass()::findByName($role, $this->getDefaultGuardName()); } return $role; diff --git a/tests/PermissionRegistarTest.php b/tests/PermissionRegistarTest.php index 1e52362b5..24d027b5e 100644 --- a/tests/PermissionRegistarTest.php +++ b/tests/PermissionRegistarTest.php @@ -2,7 +2,13 @@ namespace Spatie\Permission\Tests; +use Spatie\Permission\Contracts\Permission as PermissionContract; +use Spatie\Permission\Contracts\Role as RoleContract; +use Spatie\Permission\Models\Permission as SpatiePermission; +use Spatie\Permission\Models\Role as SpatieRole; use Spatie\Permission\PermissionRegistrar; +use Spatie\Permission\Tests\TestModels\Permission as TestPermission; +use Spatie\Permission\Tests\TestModels\Role as TestRole; class PermissionRegistarTest extends TestCase { @@ -64,4 +70,51 @@ public function it_can_check_uids() $this->assertFalse(PermissionRegistrar::isUid($not_uid)); } } + + /** @test */ + public function it_can_get_permission_class() { + $this->assertSame(SpatiePermission::class, app(PermissionRegistrar::class)->getPermissionClass()); + $this->assertSame(SpatiePermission::class, get_class(app(PermissionContract::class))); + } + + /** @test */ + public function it_can_change_permission_class() { + $this->assertSame(SpatiePermission::class, config('permission.models.permission')); + $this->assertSame(SpatiePermission::class, app(PermissionRegistrar::class)->getPermissionClass()); + $this->assertSame(SpatiePermission::class, get_class(app(PermissionContract::class))); + + app(PermissionRegistrar::class)->setPermissionClass(TestPermission::class); + + $this->assertSame(TestPermission::class, config('permission.models.permission')); + $this->assertSame(TestPermission::class, app(PermissionRegistrar::class)->getPermissionClass()); + $this->assertSame(TestPermission::class, get_class(app(PermissionContract::class))); + } + + /** @test */ + public function it_can_get_role_class() { + $this->assertSame(SpatieRole::class, app(PermissionRegistrar::class)->getRoleClass()); + $this->assertSame(SpatieRole::class, get_class(app(RoleContract::class))); + } + + /** @test */ + public function it_can_change_role_class() { + $this->assertSame(SpatieRole::class, config('permission.models.role')); + $this->assertSame(SpatieRole::class, app(PermissionRegistrar::class)->getRoleClass()); + $this->assertSame(SpatieRole::class, get_class(app(RoleContract::class))); + + app(PermissionRegistrar::class)->setRoleClass(TestRole::class); + + $this->assertSame(TestRole::class, config('permission.models.role')); + $this->assertSame(TestRole::class, app(PermissionRegistrar::class)->getRoleClass()); + $this->assertSame(TestRole::class, get_class(app(RoleContract::class))); + } + + /** @test */ + public function it_can_change_team_id() { + $team_id = '00000000-0000-0000-0000-000000000000'; + + app(PermissionRegistrar::class)->setPermissionsTeamId($team_id); + + $this->assertSame($team_id, app(PermissionRegistrar::class)->getPermissionsTeamId()); + } } From 74f646081f9b387b4cb32cbaff12e732445b23af Mon Sep 17 00:00:00 2001 From: drbyte Date: Fri, 24 Mar 2023 18:47:46 +0000 Subject: [PATCH 0622/1013] Fix styling --- tests/PermissionRegistarTest.php | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/PermissionRegistarTest.php b/tests/PermissionRegistarTest.php index 24d027b5e..d5ed03547 100644 --- a/tests/PermissionRegistarTest.php +++ b/tests/PermissionRegistarTest.php @@ -72,17 +72,19 @@ public function it_can_check_uids() } /** @test */ - public function it_can_get_permission_class() { + public function it_can_get_permission_class() + { $this->assertSame(SpatiePermission::class, app(PermissionRegistrar::class)->getPermissionClass()); $this->assertSame(SpatiePermission::class, get_class(app(PermissionContract::class))); } /** @test */ - public function it_can_change_permission_class() { + public function it_can_change_permission_class() + { $this->assertSame(SpatiePermission::class, config('permission.models.permission')); $this->assertSame(SpatiePermission::class, app(PermissionRegistrar::class)->getPermissionClass()); $this->assertSame(SpatiePermission::class, get_class(app(PermissionContract::class))); - + app(PermissionRegistrar::class)->setPermissionClass(TestPermission::class); $this->assertSame(TestPermission::class, config('permission.models.permission')); @@ -91,13 +93,15 @@ public function it_can_change_permission_class() { } /** @test */ - public function it_can_get_role_class() { + public function it_can_get_role_class() + { $this->assertSame(SpatieRole::class, app(PermissionRegistrar::class)->getRoleClass()); $this->assertSame(SpatieRole::class, get_class(app(RoleContract::class))); } /** @test */ - public function it_can_change_role_class() { + public function it_can_change_role_class() + { $this->assertSame(SpatieRole::class, config('permission.models.role')); $this->assertSame(SpatieRole::class, app(PermissionRegistrar::class)->getRoleClass()); $this->assertSame(SpatieRole::class, get_class(app(RoleContract::class))); @@ -110,7 +114,8 @@ public function it_can_change_role_class() { } /** @test */ - public function it_can_change_team_id() { + public function it_can_change_team_id() + { $team_id = '00000000-0000-0000-0000-000000000000'; app(PermissionRegistrar::class)->setPermissionsTeamId($team_id); From 87fd227c522775bbe0a123f49135d0e214448345 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 25 Mar 2023 21:43:33 -0400 Subject: [PATCH 0623/1013] Specify version 6 as supporting L8-L10 --- .github/workflows/run-tests-L7.yml | 38 ------------------------------ composer.json | 12 +++++----- docs/installation-laravel.md | 3 ++- 3 files changed, 8 insertions(+), 45 deletions(-) delete mode 100644 .github/workflows/run-tests-L7.yml diff --git a/.github/workflows/run-tests-L7.yml b/.github/workflows/run-tests-L7.yml deleted file mode 100644 index 6849039e9..000000000 --- a/.github/workflows/run-tests-L7.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: "Run Tests - Older" - -on: [push, pull_request] - -jobs: - test: - - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - php: [8.0, 7.4, 7.3] - laravel: [7.*] - dependency-version: [prefer-lowest, prefer-stable] - include: - - laravel: 7.* - testbench: 5.20 - - name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv - coverage: none - - - name: Install dependencies - run: | - composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:>=4.3.4" "mockery/mockery:^1.3.2" --no-interaction --no-update - composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction - - - name: Execute tests - run: vendor/bin/phpunit diff --git a/composer.json b/composer.json index 759ef75a5..d05f7dfcb 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "spatie/laravel-permission", - "description": "Permission handling for Laravel 6.0 and up", + "description": "Permission handling for Laravel 8.0 and up", "license": "MIT", "keywords": [ "spatie", @@ -23,13 +23,13 @@ "homepage": "https://github.com/spatie/laravel-permission", "require": { "php": "^7.3|^8.0", - "illuminate/auth": "^7.0|^8.0|^9.0|^10.0", - "illuminate/container": "^7.0|^8.0|^9.0|^10.0", - "illuminate/contracts": "^7.0|^8.0|^9.0|^10.0", - "illuminate/database": "^7.0|^8.0|^9.0|^10.0" + "illuminate/auth": "^8.0|^9.0|^10.0", + "illuminate/container": "^8.0|^9.0|^10.0", + "illuminate/contracts": "^8.0|^9.0|^10.0", + "illuminate/database": "^8.0|^9.0|^10.0" }, "require-dev": { - "orchestra/testbench": "^5.0|^6.0|^7.0|^8.0", + "orchestra/testbench": "^6.0|^7.0|^8.0", "phpunit/phpunit": "^9.4", "predis/predis": "^1.1" }, diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 642ac5a3c..5e2914937 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -9,10 +9,11 @@ This package can be used with Laravel 6.0 or higher. Package | Laravel Version --------|----------- + ^6.0 | 8,9,10 ^5.8 | 7,8,9,10 ^5.7 | 7,8,9 ^5.4-^5.6 | 7,8 -5.0-5.3 | 6,7,8 + 5.0-5.3 | 6,7,8 ^4 | 6,7,8 ^3 | 5.8 From 3507a3a6727c5403a592e5a5983c5fa4f873d268 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 25 Mar 2023 21:45:13 -0400 Subject: [PATCH 0624/1013] [Docs] Add more details to Upgrade Instructions --- docs/upgrading.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index 7b9afaaf7..60436ebc6 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -3,11 +3,23 @@ title: Upgrading weight: 6 --- -### Upgrading from v2 to v3 -There are no special requirements for upgrading from v2 to v3, other than changing `^2.xx` (xx can vary) to `^3.0` in your `composer.json` and running `composer update`. Of course, your app must meet the minimum requirements as well. +ALL upgrades of this package should follow these steps: + +1. Upgrading between major versions of this package always require the usual Composer steps: + > 1. Update your `composer.json` to specify the new major version, such as `^5.0` + > 2. Then run `composer update`. + +2. Compare the `migration` file stubs in the NEW version of this package against the migrations you've already run inside your app. If necessary, create a new migration (by hand) to apply any new changes. + +3. If you have made any custom Models from this package into your own app, compare the old and new models and apply any relevant updates to your custom models. + +4. If you have overridden any methods from this package's Traits, compare the old and new traits, and apply any relevant updates to your overridden methods. + +5. Apply any version-specific special updates as outlined below... + ### Upgrading from v1 to v2 -If you're upgrading from v1 to v2, there's no built-in automatic migration/conversion of your data. +If you're upgrading from v1 to v2, there's no built-in automatic migration/conversion of your data to the new structure. You will need to carefully adapt your code and your data manually. Tip: @fabricecw prepared [a gist which may make your data migration easier](https://gist.github.com/fabricecw/58ee93dd4f99e78724d8acbb851658a4). From addbd4f9e2a0933526ec75cfd77a3ef5ef1aae36 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 25 Mar 2023 21:45:40 -0400 Subject: [PATCH 0625/1013] [Docs] Add v6 upgrade instructions --- docs/upgrading.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index 60436ebc6..c77468ea7 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -6,7 +6,7 @@ weight: 6 ALL upgrades of this package should follow these steps: 1. Upgrading between major versions of this package always require the usual Composer steps: - > 1. Update your `composer.json` to specify the new major version, such as `^5.0` + > 1. Update your `composer.json` to specify the new major version, such as `^6.0` > 2. Then run `composer update`. 2. Compare the `migration` file stubs in the NEW version of this package against the migrations you've already run inside your app. If necessary, create a new migration (by hand) to apply any new changes. @@ -17,6 +17,12 @@ ALL upgrades of this package should follow these steps: 5. Apply any version-specific special updates as outlined below... +### Upgrading to v6 +If you have overridden the `getPermissionClass()` or `getRoleClass()` methods or have custom Models, you will need to revisit those customizations. See PR #2368 for details. +eg: if you have a custom model you will need to make changes, including accessing the model using `$this->permissionClass::` syntax (eg: using `::` instead of `->`) in all the overridden methods that make use of the models. +Be sure to compare your custom models with originals to see what else may have changed. + +Also note that the migrations have been updated to anonymous-class syntax that was introduced in Laravel 8. You may optionally update your original migration files accordingly. ### Upgrading from v1 to v2 If you're upgrading from v1 to v2, there's no built-in automatic migration/conversion of your data to the new structure. From 905c1768646468c002776de1ad0879d4069e3491 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Tue, 28 Mar 2023 17:40:42 -0500 Subject: [PATCH 0626/1013] Only offer publishing when running in console --- src/PermissionServiceProvider.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index e80b292a2..f52eff86b 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -56,6 +56,10 @@ protected function offerPublishing() return; } + if (! $this->app->runningInConsole()) { + return; + } + $this->publishes([ __DIR__.'/../config/permission.php' => config_path('permission.php'), ], 'permission-config'); From 045f8de97b4993a0bd4e67e0a39d7fa2c3bbc899 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Thu, 30 Mar 2023 09:28:36 -0500 Subject: [PATCH 0627/1013] fix Role->hasPermissionTo overwrite accorde has permission trait --- src/Models/Role.php | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/Models/Role.php b/src/Models/Role.php index feb11738e..850b77df8 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -173,22 +173,16 @@ protected static function findByParam(array $params = []) * * @throws \Spatie\Permission\Exceptions\GuardDoesNotMatch */ - public function hasPermissionTo($permission): bool + public function hasPermissionTo($permission, $guardName = null): bool { - if (config('permission.enable_wildcard_permission', false)) { - return $this->hasWildcardPermission($permission, $this->getDefaultGuardName()); - } - - if (is_string($permission)) { - $permission = $this->getPermissionClass()::findByName($permission, $this->getDefaultGuardName()); - } - - if (is_int($permission)) { - $permission = $this->getPermissionClass()::findById($permission, $this->getDefaultGuardName()); + if ($this->getWildcardClass()) { + return $this->hasWildcardPermission($permission, $guardName); } + + $permission = $this->filterPermission($permission, $guardName); if (! $this->getGuardNames()->contains($permission->guard_name)) { - throw GuardDoesNotMatch::create($permission->guard_name, $this->getGuardNames()); + throw GuardDoesNotMatch::create($permission->guard_name, $guardName ?? $this->getGuardNames()); } return $this->permissions->contains($permission->getKeyName(), $permission->getKey()); From 29ec9ec3ee5b7181c6303f018eac48a1fc8e6bda Mon Sep 17 00:00:00 2001 From: erikn69 Date: Thu, 30 Mar 2023 10:08:30 -0500 Subject: [PATCH 0628/1013] getPermissionsViaRoles, hasPermissionViaRole must be used only by authenticable --- src/Traits/HasPermissions.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 4160a3346..a41c061c8 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -22,7 +22,7 @@ trait HasPermissions /** @var string */ private $permissionClass; - /** @var string */ + /** @var string|false|null */ private $wildcardClass; public static function bootHasPermissions() @@ -61,7 +61,7 @@ protected function getWildcardClass() $this->wildcardClass = false; - if (config('permission.enable_wildcard_permission', false)) { + if (config('permission.enable_wildcard_permission')) { $this->wildcardClass = config('permission.wildcard_permission', WildcardPermission::class); if (! is_subclass_of($this->wildcardClass, Wildcard::class)) { @@ -101,7 +101,7 @@ public function scopePermission(Builder $query, $permissions): Builder { $permissions = $this->convertToPermissionModels($permissions); - $rolesWithPermissions = array_unique(array_reduce($permissions, function ($result, $permission) { + $rolesWithPermissions = is_a($this, Role::class) ? []: array_unique(array_reduce($permissions, function ($result, $permission) { return array_merge($result, $permission->roles->all()); }, [])); @@ -111,7 +111,7 @@ public function scopePermission(Builder $query, $permissions): Builder $key = (new $permissionClass())->getKeyName(); $subQuery->whereIn(config('permission.table_names.permissions').".$key", \array_column($permissions, $key)); }); - if (count($rolesWithPermissions) > 0) { + if (count($rolesWithPermissions) > 0 && ! is_a($this, Role::class)) { $query->orWhereHas('roles', function (Builder $subQuery) use ($rolesWithPermissions) { $roleClass = $this->getRoleClass(); $key = (new $roleClass())->getKeyName(); @@ -287,6 +287,10 @@ public function hasAllPermissions(...$permissions): bool */ protected function hasPermissionViaRole(Permission $permission): bool { + if (is_a($this, Role::class)) { + return false; + } + return $this->hasRole($permission->roles); } @@ -309,6 +313,10 @@ public function hasDirectPermission($permission): bool */ public function getPermissionsViaRoles(): Collection { + if (is_a($this, Role::class) || is_a($this, Permission::class)) { + return collect(); + } + return $this->loadMissing('roles', 'roles.permissions') ->roles->flatMap(function ($role) { return $role->permissions; From 509a73de2a45f0d56bc16069eac088c5b626f52e Mon Sep 17 00:00:00 2001 From: erikn69 Date: Thu, 30 Mar 2023 10:53:53 -0500 Subject: [PATCH 0629/1013] minor nitpicks --- src/Commands/Show.php | 6 +++--- src/Commands/UpgradeForTeams.php | 2 +- src/PermissionServiceProvider.php | 8 +++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Commands/Show.php b/src/Commands/Show.php index bcbe07381..c80d12c1b 100644 --- a/src/Commands/Show.php +++ b/src/Commands/Show.php @@ -58,15 +58,15 @@ public function handle() } $this->table( - array_merge([ - config('permission.teams') ? $teams->prepend('')->toArray() : [], + array_merge( + isset($teams) ? $teams->prepend(new TableCell(''))->toArray() : [], $roles->keys()->map(function ($val) { $name = explode('_', $val); return $name[0]; }) ->prepend('')->toArray(), - ]), + ), $body->toArray(), $style ); diff --git a/src/Commands/UpgradeForTeams.php b/src/Commands/UpgradeForTeams.php index 70c790bc1..f42218cc7 100644 --- a/src/Commands/UpgradeForTeams.php +++ b/src/Commands/UpgradeForTeams.php @@ -35,7 +35,7 @@ public function handle() $this->line(''); - if (! $this->confirm('Proceed with the migration creation?', 'yes')) { + if (! $this->confirm('Proceed with the migration creation?', true)) { return; } diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index f52eff86b..fdf0670df 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -26,7 +26,7 @@ public function boot() $this->registerModelBindings(); $this->callAfterResolving(Gate::class, function (Gate $gate, Application $app) { - if ($this->app->config['permission.register_permission_check_method']) { + if ($this->app['config']->get('permission.register_permission_check_method')) { /** @var PermissionRegistrar $permissionLoader */ $permissionLoader = $app->get(PermissionRegistrar::class); $permissionLoader->clearPermissionsCollection(); @@ -156,6 +156,7 @@ protected function registerMacroHelpers() Route::macro('role', function ($roles = []) { $roles = implode('|', Arr::wrap($roles)); + /** @var \Illuminate\Routing\Route $this */ $this->middleware("role:$roles"); return $this; @@ -164,6 +165,7 @@ protected function registerMacroHelpers() Route::macro('permission', function ($permissions = []) { $permissions = implode('|', Arr::wrap($permissions)); + /** @var \Illuminate\Routing\Route $this */ $this->middleware("permission:$permissions"); return $this; @@ -173,13 +175,13 @@ protected function registerMacroHelpers() /** * Returns existing migration file if found, else uses the current timestamp. */ - protected function getMigrationFileName($migrationFileName): string + protected function getMigrationFileName(string $migrationFileName): string { $timestamp = date('Y_m_d_His'); $filesystem = $this->app->make(Filesystem::class); - return Collection::make($this->app->databasePath().DIRECTORY_SEPARATOR.'migrations'.DIRECTORY_SEPARATOR) + return Collection::make([$this->app->databasePath().DIRECTORY_SEPARATOR.'migrations'.DIRECTORY_SEPARATOR]) ->flatMap(function ($path) use ($filesystem, $migrationFileName) { return $filesystem->glob($path.'*_'.$migrationFileName); }) From 6dff984bee0c38cfb0103f48251569a003617214 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 30 Mar 2023 14:44:52 -0400 Subject: [PATCH 0630/1013] [Docs] Mention changelog in upgrade instructions --- docs/upgrading.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/upgrading.md b/docs/upgrading.md index c77468ea7..24a2cfc7e 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -17,6 +17,9 @@ ALL upgrades of this package should follow these steps: 5. Apply any version-specific special updates as outlined below... +6. Review the changelog, which details all the changes: https://github.com/spatie/laravel-permission/blob/main/CHANGELOG.md + + ### Upgrading to v6 If you have overridden the `getPermissionClass()` or `getRoleClass()` methods or have custom Models, you will need to revisit those customizations. See PR #2368 for details. eg: if you have a custom model you will need to make changes, including accessing the model using `$this->permissionClass::` syntax (eg: using `::` instead of `->`) in all the overridden methods that make use of the models. From b6274744d8b046ce8865d5f8a135fcd3ff93105a Mon Sep 17 00:00:00 2001 From: drbyte Date: Thu, 30 Mar 2023 19:25:34 +0000 Subject: [PATCH 0631/1013] Fix styling --- src/Models/Role.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Models/Role.php b/src/Models/Role.php index 850b77df8..7da997548 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -178,7 +178,7 @@ public function hasPermissionTo($permission, $guardName = null): bool if ($this->getWildcardClass()) { return $this->hasWildcardPermission($permission, $guardName); } - + $permission = $this->filterPermission($permission, $guardName); if (! $this->getGuardNames()->contains($permission->guard_name)) { From 56c068b09e48cb4da599d4d3c42b89c5b8ef74e0 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 30 Mar 2023 15:28:01 -0400 Subject: [PATCH 0632/1013] [Docs] more v6 upgrade info --- docs/upgrading.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index 24a2cfc7e..9ff46d95d 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -21,11 +21,13 @@ ALL upgrades of this package should follow these steps: ### Upgrading to v6 -If you have overridden the `getPermissionClass()` or `getRoleClass()` methods or have custom Models, you will need to revisit those customizations. See PR #2368 for details. +1. If you have overridden the `getPermissionClass()` or `getRoleClass()` methods or have custom Models, you will need to revisit those customizations. See PR #2368 for details. eg: if you have a custom model you will need to make changes, including accessing the model using `$this->permissionClass::` syntax (eg: using `::` instead of `->`) in all the overridden methods that make use of the models. Be sure to compare your custom models with originals to see what else may have changed. -Also note that the migrations have been updated to anonymous-class syntax that was introduced in Laravel 8. You may optionally update your original migration files accordingly. +2. If you have a custom Role model and (in the rare case that you might) have overridden the `hasPermissionTo()` method in it, you will need to update its method signature to `hasPermissionTo($permission, $guardName = null):bool`. See PR #2380. + +3. Also note that the migrations have been updated to anonymous-class syntax that was introduced in Laravel 8. You may optionally update your original migration files accordingly. ### Upgrading from v1 to v2 If you're upgrading from v1 to v2, there's no built-in automatic migration/conversion of your data to the new structure. From f893ff06490a8d1f6fdaa0316cbfb6c86848d91f Mon Sep 17 00:00:00 2001 From: drbyte Date: Thu, 30 Mar 2023 19:30:33 +0000 Subject: [PATCH 0633/1013] Fix styling --- src/Traits/HasPermissions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index a41c061c8..c1b602bea 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -101,7 +101,7 @@ public function scopePermission(Builder $query, $permissions): Builder { $permissions = $this->convertToPermissionModels($permissions); - $rolesWithPermissions = is_a($this, Role::class) ? []: array_unique(array_reduce($permissions, function ($result, $permission) { + $rolesWithPermissions = is_a($this, Role::class) ? [] : array_unique(array_reduce($permissions, function ($result, $permission) { return array_merge($result, $permission->roles->all()); }, [])); From d76450519421f5b41b3246cd2d001e6fa25112eb Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 30 Mar 2023 16:33:02 -0400 Subject: [PATCH 0634/1013] [Docs] MySQL 8 note in migrations --- docs/installation-laravel.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 5e2914937..5b410cb5c 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -46,17 +46,19 @@ Package | Laravel Version 6. NOTE: If you are using UUIDs, see the Advanced section of the docs on UUID steps, before you continue. It explains some changes you may want to make to the migrations and config file before continuing. It also mentions important considerations after extending this package's models for UUID capability. If you are going to use teams feature, you have to update your [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/main/config/permission.php) and set `'teams' => true,`, if you want to use a custom foreign key for teams you must change `team_foreign_key`. -7. **Clear your config cache**. This package requires access to the `permission` config. Generally it's bad practice to do config-caching in a development environment. If you've been caching configurations locally, clear your config cache with either of these commands: +7. NOTE: If you are using MySQL 8, look at the migration files for notes about MySQL 8 to set/limit the index key length, and edit accordingly. + +8. **Clear your config cache**. This package requires access to the `permission` config. Generally it's bad practice to do config-caching in a development environment. If you've been caching configurations locally, clear your config cache with either of these commands: php artisan optimize:clear # or php artisan config:clear -8. **Run the migrations**: After the config and migration have been published and configured, you can create the tables for this package by running: +9. **Run the migrations**: After the config and migration have been published and configured, you can create the tables for this package by running: php artisan migrate -9. **Add the necessary trait to your User model**: +10. **Add the necessary trait to your User model**: // The User model requires this trait use HasRoles; From 938422360b8ace598e3155dd57353eba0f5ef216 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Thu, 30 Mar 2023 09:45:15 -0500 Subject: [PATCH 0635/1013] fix BadMethodCallException: undefined methods hasAnyRole, hasAnyPermissions --- src/Exceptions/UnauthorizedException.php | 8 +++++ src/Middlewares/PermissionMiddleware.php | 17 +++++++---- src/Middlewares/RoleMiddleware.php | 8 ++++- .../RoleOrPermissionMiddleware.php | 8 ++++- tests/PermissionMiddlewareTest.php | 30 +++++++++++++++++++ tests/RoleMiddlewareTest.php | 15 ++++++++++ tests/RoleOrPermissionMiddlewareTest.php | 30 +++++++++++++++++++ tests/TestModels/UserWithoutHasRoles.php | 21 +++++++++++++ 8 files changed, 129 insertions(+), 8 deletions(-) create mode 100644 tests/TestModels/UserWithoutHasRoles.php diff --git a/src/Exceptions/UnauthorizedException.php b/src/Exceptions/UnauthorizedException.php index 2a270faf8..249898a75 100644 --- a/src/Exceptions/UnauthorizedException.php +++ b/src/Exceptions/UnauthorizedException.php @@ -2,6 +2,7 @@ namespace Spatie\Permission\Exceptions; +use Illuminate\Contracts\Auth\Access\Authorizable; use Symfony\Component\HttpKernel\Exception\HttpException; class UnauthorizedException extends HttpException @@ -52,6 +53,13 @@ public static function forRolesOrPermissions(array $rolesOrPermissions): self return $exception; } + public static function missingTraitHasRoles(Authorizable $user): self + { + $class = get_class($user); + + return new static(403, "Authorizable class `{$class}` must use Spatie\Permission\Traits\HasRoles trait.", null, []); + } + public static function notLoggedIn(): self { return new static(403, 'User is not logged in.', null, []); diff --git a/src/Middlewares/PermissionMiddleware.php b/src/Middlewares/PermissionMiddleware.php index 73dec2ef8..49793f5ce 100644 --- a/src/Middlewares/PermissionMiddleware.php +++ b/src/Middlewares/PermissionMiddleware.php @@ -3,28 +3,33 @@ namespace Spatie\Permission\Middlewares; use Closure; +use Illuminate\Support\Facades\Auth; use Spatie\Permission\Exceptions\UnauthorizedException; class PermissionMiddleware { public function handle($request, Closure $next, $permission, $guard = null) { - $authGuard = app('auth')->guard($guard); + $authGuard = Auth::guard($guard); if ($authGuard->guest()) { throw UnauthorizedException::notLoggedIn(); } + $user = $authGuard->user(); + + if (! method_exists($user, 'hasAnyPermission')) { + throw UnauthorizedException::missingTraitHasRoles($user); + } + $permissions = is_array($permission) ? $permission : explode('|', $permission); - foreach ($permissions as $permission) { - if ($authGuard->user()->can($permission)) { - return $next($request); - } + if (! $user->canAny($permissions)) { + throw UnauthorizedException::forPermissions($permissions); } - throw UnauthorizedException::forPermissions($permissions); + return $next($request); } } diff --git a/src/Middlewares/RoleMiddleware.php b/src/Middlewares/RoleMiddleware.php index 34e91e241..f3e972a9b 100644 --- a/src/Middlewares/RoleMiddleware.php +++ b/src/Middlewares/RoleMiddleware.php @@ -16,11 +16,17 @@ public function handle($request, Closure $next, $role, $guard = null) throw UnauthorizedException::notLoggedIn(); } + $user = $authGuard->user(); + + if (! method_exists($user, 'hasAnyRole')) { + throw UnauthorizedException::missingTraitHasRoles($user); + } + $roles = is_array($role) ? $role : explode('|', $role); - if (! $authGuard->user()->hasAnyRole($roles)) { + if (! $user->hasAnyRole($roles) && ! $user->can('')) { throw UnauthorizedException::forRoles($roles); } diff --git a/src/Middlewares/RoleOrPermissionMiddleware.php b/src/Middlewares/RoleOrPermissionMiddleware.php index b9149f2f6..5a9aaa341 100644 --- a/src/Middlewares/RoleOrPermissionMiddleware.php +++ b/src/Middlewares/RoleOrPermissionMiddleware.php @@ -15,11 +15,17 @@ public function handle($request, Closure $next, $roleOrPermission, $guard = null throw UnauthorizedException::notLoggedIn(); } + $user = $authGuard->user(); + + if (! method_exists($user, 'hasAnyRole') || ! method_exists($user, 'hasAnyPermission')) { + throw UnauthorizedException::missingTraitHasRoles($user); + } + $rolesOrPermissions = is_array($roleOrPermission) ? $roleOrPermission : explode('|', $roleOrPermission); - if (! $authGuard->user()->hasAnyRole($rolesOrPermissions) && ! $authGuard->user()->hasAnyPermission($rolesOrPermissions)) { + if (! $user->canAny($rolesOrPermissions) && ! $user->hasAnyRole($rolesOrPermissions)) { throw UnauthorizedException::forRolesOrPermissions($rolesOrPermissions); } diff --git a/tests/PermissionMiddlewareTest.php b/tests/PermissionMiddlewareTest.php index 0b5f82943..9d9659107 100644 --- a/tests/PermissionMiddlewareTest.php +++ b/tests/PermissionMiddlewareTest.php @@ -6,10 +6,12 @@ use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\Gate; use InvalidArgumentException; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Exceptions\UnauthorizedException; use Spatie\Permission\Middlewares\PermissionMiddleware; +use Spatie\Permission\Tests\TestModels\UserWithoutHasRoles; class PermissionMiddlewareTest extends TestCase { @@ -69,6 +71,21 @@ public function a_user_cannot_access_a_route_protected_by_the_permission_middlew ); } + /** @test */ + public function a_super_admin_user_can_access_a_route_protected_by_permission_middleware() + { + Auth::login($this->testUser); + + Gate::before(function ($user, $ability) { + return $user->getKey() === $this->testUser->getKey() ? true : null; + }); + + $this->assertEquals( + 200, + $this->runMiddleware($this->permissionMiddleware, 'edit-articles') + ); + } + /** @test */ public function a_user_can_access_a_route_protected_by_permission_middleware_if_have_this_permission() { @@ -100,6 +117,19 @@ public function a_user_can_access_a_route_protected_by_this_permission_middlewar ); } + /** @test */ + public function a_user_cannot_access_a_route_protected_by_the_permission_middleware_if_have_not_has_roles_trait() + { + $userWithoutHasRoles = UserWithoutHasRoles::create(['email' => 'test_not_has_roles@user.com']); + + Auth::login($userWithoutHasRoles); + + $this->assertEquals( + 403, + $this->runMiddleware($this->permissionMiddleware, 'edit-news') + ); + } + /** @test */ public function a_user_cannot_access_a_route_protected_by_the_permission_middleware_if_have_a_different_permission() { diff --git a/tests/RoleMiddlewareTest.php b/tests/RoleMiddlewareTest.php index 1109e5a4e..de9f6e844 100644 --- a/tests/RoleMiddlewareTest.php +++ b/tests/RoleMiddlewareTest.php @@ -6,9 +6,11 @@ use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\Gate; use InvalidArgumentException; use Spatie\Permission\Exceptions\UnauthorizedException; use Spatie\Permission\Middlewares\RoleMiddleware; +use Spatie\Permission\Tests\TestModels\UserWithoutHasRoles; class RoleMiddlewareTest extends TestCase { @@ -74,6 +76,19 @@ public function a_user_can_access_a_route_protected_by_this_role_middleware_if_h ); } + /** @test */ + public function a_user_cannot_access_a_route_protected_by_the_role_middleware_if_have_not_has_roles_trait() + { + $userWithoutHasRoles = UserWithoutHasRoles::create(['email' => 'test_not_has_roles@user.com']); + + Auth::login($userWithoutHasRoles); + + $this->assertEquals( + 403, + $this->runMiddleware($this->roleMiddleware, 'testRole') + ); + } + /** @test */ public function a_user_cannot_access_a_route_protected_by_the_role_middleware_if_have_a_different_role() { diff --git a/tests/RoleOrPermissionMiddlewareTest.php b/tests/RoleOrPermissionMiddlewareTest.php index a6e8147e2..20a4139be 100644 --- a/tests/RoleOrPermissionMiddlewareTest.php +++ b/tests/RoleOrPermissionMiddlewareTest.php @@ -6,9 +6,11 @@ use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\Gate; use InvalidArgumentException; use Spatie\Permission\Exceptions\UnauthorizedException; use Spatie\Permission\Middlewares\RoleOrPermissionMiddleware; +use Spatie\Permission\Tests\TestModels\UserWithoutHasRoles; class RoleOrPermissionMiddlewareTest extends TestCase { @@ -64,6 +66,34 @@ public function a_user_can_access_a_route_protected_by_permission_or_role_middle ); } + /** @test */ + public function a_super_admin_user_can_access_a_route_protected_by_permission_or_role_middleware() + { + Auth::login($this->testUser); + + Gate::before(function ($user, $ability) { + return $user->getKey() === $this->testUser->getKey() ? true : null; + }); + + $this->assertEquals( + 200, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles') + ); + } + + /** @test */ + public function a_user_can_not_access_a_route_protected_by_permission_or_role_middleware_if_have_not_has_roles_trait() + { + $userWithoutHasRoles = UserWithoutHasRoles::create(['email' => 'test_not_has_roles@user.com']); + + Auth::login($userWithoutHasRoles); + + $this->assertEquals( + 403, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles') + ); + } + /** @test */ public function a_user_can_not_access_a_route_protected_by_permission_or_role_middleware_if_have_not_this_permission_and_role() { diff --git a/tests/TestModels/UserWithoutHasRoles.php b/tests/TestModels/UserWithoutHasRoles.php new file mode 100644 index 000000000..dd9a29445 --- /dev/null +++ b/tests/TestModels/UserWithoutHasRoles.php @@ -0,0 +1,21 @@ + Date: Thu, 30 Mar 2023 21:19:40 +0000 Subject: [PATCH 0636/1013] Fix styling --- tests/PermissionMiddlewareTest.php | 2 +- tests/RoleMiddlewareTest.php | 1 - tests/RoleOrPermissionMiddlewareTest.php | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/PermissionMiddlewareTest.php b/tests/PermissionMiddlewareTest.php index 9d9659107..39a57c48b 100644 --- a/tests/PermissionMiddlewareTest.php +++ b/tests/PermissionMiddlewareTest.php @@ -77,7 +77,7 @@ public function a_super_admin_user_can_access_a_route_protected_by_permission_mi Auth::login($this->testUser); Gate::before(function ($user, $ability) { - return $user->getKey() === $this->testUser->getKey() ? true : null; + return $user->getKey() === $this->testUser->getKey() ? true : null; }); $this->assertEquals( diff --git a/tests/RoleMiddlewareTest.php b/tests/RoleMiddlewareTest.php index de9f6e844..7d532a004 100644 --- a/tests/RoleMiddlewareTest.php +++ b/tests/RoleMiddlewareTest.php @@ -6,7 +6,6 @@ use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Config; -use Illuminate\Support\Facades\Gate; use InvalidArgumentException; use Spatie\Permission\Exceptions\UnauthorizedException; use Spatie\Permission\Middlewares\RoleMiddleware; diff --git a/tests/RoleOrPermissionMiddlewareTest.php b/tests/RoleOrPermissionMiddlewareTest.php index 20a4139be..e9f628932 100644 --- a/tests/RoleOrPermissionMiddlewareTest.php +++ b/tests/RoleOrPermissionMiddlewareTest.php @@ -72,7 +72,7 @@ public function a_super_admin_user_can_access_a_route_protected_by_permission_or Auth::login($this->testUser); Gate::before(function ($user, $ability) { - return $user->getKey() === $this->testUser->getKey() ? true : null; + return $user->getKey() === $this->testUser->getKey() ? true : null; }); $this->assertEquals( From 5ed128b611c3b02176768ac3b3375979913678b0 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 30 Mar 2023 17:23:16 -0400 Subject: [PATCH 0637/1013] [Docs] v6 RoleOrPermissionMiddleware upgrade note --- docs/upgrading.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index 9ff46d95d..4f1e1b481 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -27,7 +27,9 @@ Be sure to compare your custom models with originals to see what else may have c 2. If you have a custom Role model and (in the rare case that you might) have overridden the `hasPermissionTo()` method in it, you will need to update its method signature to `hasPermissionTo($permission, $guardName = null):bool`. See PR #2380. -3. Also note that the migrations have been updated to anonymous-class syntax that was introduced in Laravel 8. You may optionally update your original migration files accordingly. +3. Also note that the migrations have been updated to anonymous-class syntax that was introduced in Laravel 8. You may optionally update your original migration files accordingly. + +4. NOTE: For consistency with the `PermissionMiddleware`, the `RoleOrPermissionMiddleware` has switched from only checking permissions provided by this package to using `canAny()` to check against any abilities registered by your application. This may have the effect of granting those other abilities (such as Super Admin) when using the `RoleOrPermissionMiddleware`, which previously would have failed silently. ### Upgrading from v1 to v2 If you're upgrading from v1 to v2, there's no built-in automatic migration/conversion of your data to the new structure. From 38d0074a2f185f685415d7f016e452b79e4383c6 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Mon, 27 Mar 2023 16:31:56 -0500 Subject: [PATCH 0638/1013] Add PHPStan workflow with fixes --- .gitattributes | 2 ++ .github/workflows/phpstan.yml | 31 ++++++++++++++++++++++ composer.json | 4 ++- phpstan-baseline.neon | 2 ++ phpstan.neon.dist | 17 ++++++++++++ src/Contracts/Permission.php | 6 +++++ src/Contracts/Role.php | 6 +++++ src/Middlewares/RoleMiddleware.php | 2 +- src/Models/Permission.php | 9 +++---- src/Models/Role.php | 19 +++++++------- src/PermissionRegistrar.php | 6 ++--- src/PermissionServiceProvider.php | 10 +++---- src/Traits/HasPermissions.php | 42 +++++++++++++++--------------- src/Traits/HasRoles.php | 18 ++++++------- 14 files changed, 118 insertions(+), 56 deletions(-) create mode 100644 .github/workflows/phpstan.yml create mode 100644 phpstan-baseline.neon create mode 100644 phpstan.neon.dist diff --git a/.gitattributes b/.gitattributes index 993e0d23c..47d2cd035 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,6 +13,8 @@ /tests export-ignore /.editorconfig export-ignore /.php_cs.dist.php export-ignore +/phpstan* export-ignore /.styleci.yml export-ignore /CHANGELOG.md export-ignore /CONTRIBUTING.md export-ignore + diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 000000000..acd8b906e --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,31 @@ +name: PHPStan + +on: + push: + paths: + - '**.php' + - 'phpstan.neon.dist' + +jobs: + phpstan: + name: phpstan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.1 + coverage: none + + - name: Install composer dependencies + uses: ramsey/composer-install@v2 + + - name: Install larastan + run: | + composer require "nunomaduro/larastan" --no-interaction --no-update + composer update --prefer-dist --no-interaction + + - name: Run PHPStan + run: ./vendor/bin/phpstan --error-format=github diff --git a/composer.json b/composer.json index d05f7dfcb..8a01fd579 100644 --- a/composer.json +++ b/composer.json @@ -63,6 +63,8 @@ } }, "scripts": { - "test": "phpunit" + "test": "phpunit", + "format": "php-cs-fixer fix --allow-risky=yes", + "analyse": "phpstan analyse" } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 000000000..364905f71 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,2 @@ +parameters: + ignoreErrors: diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 000000000..b367f0713 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,17 @@ +includes: + - ./vendor/nunomaduro/larastan/extension.neon + - phpstan-baseline.neon + +parameters: + level: 5 + paths: + - src + - config + - database/migrations/create_permission_tables.php.stub + - database/migrations/add_teams_fields.php.stub + tmpDir: build/phpstan + checkOctaneCompatibility: true + checkMissingIterableValueType: false + + ignoreErrors: + - '#Unsafe usage of new static#' diff --git a/src/Contracts/Permission.php b/src/Contracts/Permission.php index fe151d077..e54f84d21 100644 --- a/src/Contracts/Permission.php +++ b/src/Contracts/Permission.php @@ -4,6 +4,12 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; +/** + * @property int $id + * @property string $name + * @property string $guard_name + * @mixin \Spatie\Permission\Models\Permission + */ interface Permission { /** diff --git a/src/Contracts/Role.php b/src/Contracts/Role.php index 94773e4fa..9e2281428 100644 --- a/src/Contracts/Role.php +++ b/src/Contracts/Role.php @@ -4,6 +4,12 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; +/** + * @property int $id + * @property string $name + * @property string $guard_name + * @mixin \Spatie\Permission\Models\Role + */ interface Role { /** diff --git a/src/Middlewares/RoleMiddleware.php b/src/Middlewares/RoleMiddleware.php index f3e972a9b..4e81f0073 100644 --- a/src/Middlewares/RoleMiddleware.php +++ b/src/Middlewares/RoleMiddleware.php @@ -26,7 +26,7 @@ public function handle($request, Closure $next, $role, $guard = null) ? $role : explode('|', $role); - if (! $user->hasAnyRole($roles) && ! $user->can('')) { + if (! $user->hasAnyRole($roles)) { throw UnauthorizedException::forRoles($roles); } diff --git a/src/Models/Permission.php b/src/Models/Permission.php index e7b428c9d..631597fac 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -14,9 +14,6 @@ use Spatie\Permission\Traits\RefreshesPermissionCache; /** - * @property int $id - * @property string $name - * @property string $guard_name * @property ?\Illuminate\Support\Carbon $created_at * @property ?\Illuminate\Support\Carbon $updated_at */ @@ -82,7 +79,7 @@ public function users(): BelongsToMany * * @param string|null $guardName * - * @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist + * @throws PermissionDoesNotExist */ public static function findByName(string $name, $guardName = null): PermissionContract { @@ -101,7 +98,7 @@ public static function findByName(string $name, $guardName = null): PermissionCo * @param int|string $id * @param string|null $guardName * - * @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist + * @throws PermissionDoesNotExist */ public static function findById($id, $guardName = null): PermissionContract { @@ -145,7 +142,7 @@ protected static function getPermissions(array $params = [], bool $onlyOne = fal /** * Get the current cached first permission. * - * @return \Spatie\Permission\Contracts\Permission + * @return PermissionContract */ protected static function getPermission(array $params = []): ?PermissionContract { diff --git a/src/Models/Role.php b/src/Models/Role.php index 7da997548..f948b0aa3 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Spatie\Permission\Contracts\Role as RoleContract; use Spatie\Permission\Exceptions\GuardDoesNotMatch; +use Spatie\Permission\Exceptions\PermissionDoesNotExist; use Spatie\Permission\Exceptions\RoleAlreadyExists; use Spatie\Permission\Exceptions\RoleDoesNotExist; use Spatie\Permission\Guard; @@ -14,9 +15,6 @@ use Spatie\Permission\Traits\RefreshesPermissionCache; /** - * @property int $id - * @property string $name - * @property string $guard_name * @property ?\Illuminate\Support\Carbon $created_at * @property ?\Illuminate\Support\Carbon $updated_at */ @@ -89,9 +87,9 @@ public function users(): BelongsToMany * Find a role by its name and guard name. * * @param string|null $guardName - * @return \Spatie\Permission\Contracts\Role|\Spatie\Permission\Models\Role + * @return RoleContract|Role * - * @throws \Spatie\Permission\Exceptions\RoleDoesNotExist + * @throws RoleDoesNotExist */ public static function findByName(string $name, $guardName = null): RoleContract { @@ -111,7 +109,7 @@ public static function findByName(string $name, $guardName = null): RoleContract * * @param int|string $id * @param string|null $guardName - * @return \Spatie\Permission\Contracts\Role|\Spatie\Permission\Models\Role + * @return RoleContract|Role */ public static function findById($id, $guardName = null): RoleContract { @@ -130,7 +128,7 @@ public static function findById($id, $guardName = null): RoleContract * Find or create role by its name (and optionally guardName). * * @param string|null $guardName - * @return \Spatie\Permission\Contracts\Role|\Spatie\Permission\Models\Role + * @return RoleContract|Role */ public static function findOrCreate(string $name, $guardName = null): RoleContract { @@ -167,11 +165,12 @@ protected static function findByParam(array $params = []) } /** - * Determine if the user may perform the given permission. + * Determine if the role may perform the given permission. * - * @param string|Permission $permission + * @param string|int|Permission $permission + * @param string|null $guardName * - * @throws \Spatie\Permission\Exceptions\GuardDoesNotMatch + * @throws PermissionDoesNotExist|GuardDoesNotMatch */ public function hasPermissionTo($permission, $guardName = null): bool { diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 2b64eed6b..2032be779 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -13,10 +13,10 @@ class PermissionRegistrar { - /** @var \Illuminate\Contracts\Cache\Repository */ + /** @var Repository */ protected $cache; - /** @var \Illuminate\Cache\CacheManager */ + /** @var CacheManager */ protected $cacheManager; /** @var string */ @@ -25,7 +25,7 @@ class PermissionRegistrar /** @var string */ protected $roleClass; - /** @var \Illuminate\Database\Eloquent\Collection */ + /** @var Collection|null */ protected $permissions; /** @var string */ diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index fdf0670df..4cab16979 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -51,12 +51,12 @@ public function register() protected function offerPublishing() { - if (! function_exists('config_path')) { - // function not available and 'publish' not relevant in Lumen + if (! $this->app->runningInConsole()) { return; } - if (! $this->app->runningInConsole()) { + if (! function_exists('config_path')) { + // function not available and 'publish' not relevant in Lumen return; } @@ -156,7 +156,7 @@ protected function registerMacroHelpers() Route::macro('role', function ($roles = []) { $roles = implode('|', Arr::wrap($roles)); - /** @var \Illuminate\Routing\Route $this */ + /** @var Route $this */ $this->middleware("role:$roles"); return $this; @@ -165,7 +165,7 @@ protected function registerMacroHelpers() Route::macro('permission', function ($permissions = []) { $permissions = implode('|', Arr::wrap($permissions)); - /** @var \Illuminate\Routing\Route $this */ + /** @var Route $this */ $this->middleware("permission:$permissions"); return $this; diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index c1b602bea..160be5f93 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -95,7 +95,7 @@ public function permissions(): BelongsToMany /** * Scope the model query to certain permissions only. * - * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions + * @param string|int|array|Permission|Collection $permissions */ public function scopePermission(Builder $query, $permissions): Builder { @@ -122,9 +122,9 @@ public function scopePermission(Builder $query, $permissions): Builder } /** - * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions + * @param string|int|array|Permission|Collection $permissions * - * @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist + * @throws PermissionDoesNotExist */ protected function convertToPermissionModels($permissions): array { @@ -145,8 +145,8 @@ protected function convertToPermissionModels($permissions): array /** * Find a permission. * - * @param string|int|\Spatie\Permission\Contracts\Permission $permission - * @return \Spatie\Permission\Contracts\Permission + * @param string|int|Permission $permission + * @return Permission * * @throws PermissionDoesNotExist */ @@ -176,7 +176,7 @@ public function filterPermission($permission, $guardName = null) /** * Determine if the model may perform the given permission. * - * @param string|int|\Spatie\Permission\Contracts\Permission $permission + * @param string|int|Permission $permission * @param string|null $guardName * * @throws PermissionDoesNotExist @@ -195,7 +195,7 @@ public function hasPermissionTo($permission, $guardName = null): bool /** * Validates a wildcard permission against all permissions of a user. * - * @param string|int|\Spatie\Permission\Contracts\Permission $permission + * @param string|int|Permission $permission * @param string|null $guardName */ protected function hasWildcardPermission($permission, $guardName = null): bool @@ -234,7 +234,7 @@ protected function hasWildcardPermission($permission, $guardName = null): bool /** * An alias to hasPermissionTo(), but avoids throwing an exception. * - * @param string|int|\Spatie\Permission\Contracts\Permission $permission + * @param string|int|Permission $permission * @param string|null $guardName */ public function checkPermissionTo($permission, $guardName = null): bool @@ -249,7 +249,7 @@ public function checkPermissionTo($permission, $guardName = null): bool /** * Determine if the model has any of the given permissions. * - * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions + * @param string|int|array|Permission|Collection ...$permissions */ public function hasAnyPermission(...$permissions): bool { @@ -267,7 +267,7 @@ public function hasAnyPermission(...$permissions): bool /** * Determine if the model has all of the given permissions. * - * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions + * @param string|int|array|Permission|Collection ...$permissions */ public function hasAllPermissions(...$permissions): bool { @@ -297,7 +297,7 @@ protected function hasPermissionViaRole(Permission $permission): bool /** * Determine if the model has the given permission. * - * @param string|int|\Spatie\Permission\Contracts\Permission $permission + * @param string|int|Permission $permission * * @throws PermissionDoesNotExist */ @@ -341,7 +341,7 @@ public function getAllPermissions(): Collection /** * Returns permissions ids as array keys * - * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions + * @param string|int|array|Permission|Collection $permissions */ private function collectPermissions(...$permissions): array { @@ -369,7 +369,7 @@ private function collectPermissions(...$permissions): array /** * Grant the given permission(s) to a role. * - * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions + * @param string|int|array|Permission|Collection $permissions * @return $this */ public function givePermissionTo(...$permissions) @@ -405,7 +405,7 @@ function ($object) use ($permissions, $model) { /** * Remove all current permissions and set the given ones. * - * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions + * @param string|int|array|Permission|Collection $permissions * @return $this */ public function syncPermissions(...$permissions) @@ -420,7 +420,7 @@ public function syncPermissions(...$permissions) /** * Revoke the given permission(s). * - * @param \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Permission[]|string|string[] $permission + * @param Permission|Permission[]|string|string[] $permission * @return $this */ public function revokePermissionTo($permission) @@ -442,8 +442,8 @@ public function getPermissionNames(): Collection } /** - * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions - * @return \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Permission[]|\Illuminate\Support\Collection + * @param string|int|array|Permission|Collection $permissions + * @return Permission|Permission[]|Collection */ protected function getStoredPermission($permissions) { @@ -469,9 +469,9 @@ protected function getStoredPermission($permissions) } /** - * @param \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Role $roleOrPermission + * @param Permission|Role $roleOrPermission * - * @throws \Spatie\Permission\Exceptions\GuardDoesNotMatch + * @throws GuardDoesNotMatch */ protected function ensureModelSharesGuard($roleOrPermission) { @@ -501,7 +501,7 @@ public function forgetCachedPermissions() /** * Check if the model has All of the requested Direct permissions. * - * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions + * @param string|int|array|Permission|Collection ...$permissions */ public function hasAllDirectPermissions(...$permissions): bool { @@ -519,7 +519,7 @@ public function hasAllDirectPermissions(...$permissions): bool /** * Check if the model has Any of the requested Direct permissions. * - * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions + * @param string|int|array|Permission|Collection ...$permissions */ public function hasAnyDirectPermission(...$permissions): bool { diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 99ec852f3..0feca7e52 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -70,7 +70,7 @@ public function roles(): BelongsToMany /** * Scope the model query to certain roles only. * - * @param string|int|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles + * @param string|int|array|Role|Collection $roles * @param string $guard */ public function scopeRole(Builder $query, $roles, $guard = null): Builder @@ -99,7 +99,7 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder /** * Returns roles ids as array keys * - * @param array|string|int|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles + * @param array|string|int|Role|Collection $roles */ private function collectRoles(...$roles): array { @@ -127,7 +127,7 @@ private function collectRoles(...$roles): array /** * Assign the given role to the model. * - * @param array|string|int|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection ...$roles + * @param array|string|int|Role|Collection ...$roles * @return $this */ public function assignRole(...$roles) @@ -163,7 +163,7 @@ function ($object) use ($roles, $model) { /** * Revoke the given role from the model. * - * @param string|int|\Spatie\Permission\Contracts\Role $role + * @param string|int|Role $role */ public function removeRole($role) { @@ -181,7 +181,7 @@ public function removeRole($role) /** * Remove all current roles and set the given ones. * - * @param array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection|string|int ...$roles + * @param array|Role|Collection|string|int ...$roles * @return $this */ public function syncRoles(...$roles) @@ -196,7 +196,7 @@ public function syncRoles(...$roles) /** * Determine if the model has (one of) the given role(s). * - * @param string|int|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles + * @param string|int|array|Role|Collection $roles */ public function hasRole($roles, string $guard = null): bool { @@ -243,7 +243,7 @@ public function hasRole($roles, string $guard = null): bool * * Alias to hasRole() but without Guard controls * - * @param string|int|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles + * @param string|int|array|Role|Collection $roles */ public function hasAnyRole(...$roles): bool { @@ -253,7 +253,7 @@ public function hasAnyRole(...$roles): bool /** * Determine if the model has all of the given role(s). * - * @param string|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles + * @param string|array|Role|Collection $roles */ public function hasAllRoles($roles, string $guard = null): bool { @@ -287,7 +287,7 @@ public function hasAllRoles($roles, string $guard = null): bool /** * Determine if the model has exactly all of the given role(s). * - * @param string|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles + * @param string|array|Role|Collection $roles */ public function hasExactRoles($roles, string $guard = null): bool { From 5319e3accf9d0aa36ad60d423cbe222739872b35 Mon Sep 17 00:00:00 2001 From: drbyte Date: Thu, 30 Mar 2023 21:56:06 +0000 Subject: [PATCH 0639/1013] Fix styling --- src/Contracts/Permission.php | 1 + src/Contracts/Role.php | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Contracts/Permission.php b/src/Contracts/Permission.php index e54f84d21..8b3d14e18 100644 --- a/src/Contracts/Permission.php +++ b/src/Contracts/Permission.php @@ -8,6 +8,7 @@ * @property int $id * @property string $name * @property string $guard_name + * * @mixin \Spatie\Permission\Models\Permission */ interface Permission diff --git a/src/Contracts/Role.php b/src/Contracts/Role.php index 9e2281428..04009de37 100644 --- a/src/Contracts/Role.php +++ b/src/Contracts/Role.php @@ -8,6 +8,7 @@ * @property int $id * @property string $name * @property string $guard_name + * * @mixin \Spatie\Permission\Models\Role */ interface Role From bc019441d3afb47da24ea35d44bf8692f5d84430 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 30 Mar 2023 22:45:05 -0400 Subject: [PATCH 0640/1013] [docs] tidying --- docs/installation-laravel.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 5b410cb5c..da36f7869 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -7,15 +7,15 @@ weight: 4 This package can be used with Laravel 6.0 or higher. -Package | Laravel Version ---------|----------- - ^6.0 | 8,9,10 - ^5.8 | 7,8,9,10 - ^5.7 | 7,8,9 -^5.4-^5.6 | 7,8 - 5.0-5.3 | 6,7,8 - ^4 | 6,7,8 - ^3 | 5.8 +Package Version | Laravel Version +----------------|----------- + ^6.0 | 8,9,10 + ^5.8 | 7,8,9,10 + ^5.7 | 7,8,9 + ^5.4-^5.6 | 7,8 + 5.0-5.3 | 6,7,8 + ^4 | 6,7,8 + ^3 | 5.8 ## Installing From be8dcac8932b44e787563000e0e175b406b8e319 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 30 Mar 2023 22:46:33 -0400 Subject: [PATCH 0641/1013] [docs] tidying --- docs/installation-laravel.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index da36f7869..bbf7c429a 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -10,9 +10,9 @@ This package can be used with Laravel 6.0 or higher. Package Version | Laravel Version ----------------|----------- ^6.0 | 8,9,10 - ^5.8 | 7,8,9,10 + ^5.8 | 7,8,9,10 ^5.7 | 7,8,9 - ^5.4-^5.6 | 7,8 + ^5.4-^5.6 | 7,8 5.0-5.3 | 6,7,8 ^4 | 6,7,8 ^3 | 5.8 @@ -43,8 +43,8 @@ Package Version | Laravel Version php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" ``` -6. NOTE: If you are using UUIDs, see the Advanced section of the docs on UUID steps, before you continue. It explains some changes you may want to make to the migrations and config file before continuing. It also mentions important considerations after extending this package's models for UUID capability. - If you are going to use teams feature, you have to update your [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/main/config/permission.php) and set `'teams' => true,`, if you want to use a custom foreign key for teams you must change `team_foreign_key`. +6. NOTE: **If you are using UUIDs**, see the Advanced section of the docs on UUID steps, before you continue. It explains some changes you may want to make to the migrations and config file before continuing. It also mentions important considerations after extending this package's models for UUID capability. + **If you are going to use the TEAMS features**, you must update your [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/main/config/permission.php) and set `'teams' => true,`; and in your database if you want to use a custom foreign key for teams you must change `team_foreign_key`. 7. NOTE: If you are using MySQL 8, look at the migration files for notes about MySQL 8 to set/limit the index key length, and edit accordingly. From 38bf2fc0075638eb8ad2ef4aedfaecfef8176073 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 30 Mar 2023 22:47:39 -0400 Subject: [PATCH 0642/1013] [docs] Tidying --- docs/installation-laravel.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index bbf7c429a..bd77f7f68 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -9,13 +9,13 @@ This package can be used with Laravel 6.0 or higher. Package Version | Laravel Version ----------------|----------- - ^6.0 | 8,9,10 - ^5.8 | 7,8,9,10 - ^5.7 | 7,8,9 - ^5.4-^5.6 | 7,8 + ^6.0 | 8,9,10 + ^5.8 | 7,8,9,10 + ^5.7 | 7,8,9 + ^5.4-^5.6 | 7,8 5.0-5.3 | 6,7,8 - ^4 | 6,7,8 - ^3 | 5.8 + ^4 | 6,7,8 + ^3 | 5.8 ## Installing From f85eb66a79251c8582a28a40f0f7b959f6460582 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 30 Mar 2023 22:48:16 -0400 Subject: [PATCH 0643/1013] [docs] tidying --- docs/installation-laravel.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index bd77f7f68..65e20c29c 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -44,6 +44,7 @@ Package Version | Laravel Version ``` 6. NOTE: **If you are using UUIDs**, see the Advanced section of the docs on UUID steps, before you continue. It explains some changes you may want to make to the migrations and config file before continuing. It also mentions important considerations after extending this package's models for UUID capability. + **If you are going to use the TEAMS features**, you must update your [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/main/config/permission.php) and set `'teams' => true,`; and in your database if you want to use a custom foreign key for teams you must change `team_foreign_key`. 7. NOTE: If you are using MySQL 8, look at the migration files for notes about MySQL 8 to set/limit the index key length, and edit accordingly. From 23a6b12a6bbbb2da2b3dfce956e47dfce1cbd39f Mon Sep 17 00:00:00 2001 From: erikn69 Date: Thu, 30 Mar 2023 16:37:02 -0500 Subject: [PATCH 0644/1013] [tests] tidying --- tests/TestModels/Admin.php | 17 +---------------- tests/TestModels/Manager.php | 19 +------------------ tests/TestModels/User.php | 15 +-------------- 3 files changed, 3 insertions(+), 48 deletions(-) diff --git a/tests/TestModels/Admin.php b/tests/TestModels/Admin.php index dc4cf75e3..5b6334a98 100644 --- a/tests/TestModels/Admin.php +++ b/tests/TestModels/Admin.php @@ -2,22 +2,7 @@ namespace Spatie\Permission\Tests\TestModels; -use Illuminate\Auth\Authenticatable; -use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; -use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Foundation\Auth\Access\Authorizable; -use Spatie\Permission\Traits\HasRoles; - -class Admin extends Model implements AuthorizableContract, AuthenticatableContract +class Admin extends User { - use HasRoles; - use Authorizable; - use Authenticatable; - - protected $fillable = ['email']; - - public $timestamps = false; - protected $table = 'admins'; } diff --git a/tests/TestModels/Manager.php b/tests/TestModels/Manager.php index c75b0d007..3405d924e 100644 --- a/tests/TestModels/Manager.php +++ b/tests/TestModels/Manager.php @@ -2,25 +2,8 @@ namespace Spatie\Permission\Tests\TestModels; -use Illuminate\Auth\Authenticatable; -use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; -use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Foundation\Auth\Access\Authorizable; -use Spatie\Permission\Traits\HasRoles; - -class Manager extends Model implements AuthorizableContract, AuthenticatableContract +class Manager extends User { - use HasRoles; - use Authorizable; - use Authenticatable; - - protected $fillable = ['email']; - - public $timestamps = false; - - protected $table = 'users'; - // this function is added here to support the unit tests verifying it works // When present, it takes precedence over the $guard_name property. public function guardName() diff --git a/tests/TestModels/User.php b/tests/TestModels/User.php index 8398d34b9..288f5f5f2 100644 --- a/tests/TestModels/User.php +++ b/tests/TestModels/User.php @@ -2,22 +2,9 @@ namespace Spatie\Permission\Tests\TestModels; -use Illuminate\Auth\Authenticatable; -use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; -use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Foundation\Auth\Access\Authorizable; use Spatie\Permission\Traits\HasRoles; -class User extends Model implements AuthorizableContract, AuthenticatableContract +class User extends UserWithoutHasRoles { use HasRoles; - use Authorizable; - use Authenticatable; - - protected $fillable = ['email']; - - public $timestamps = false; - - protected $table = 'users'; } From 20c10e31ea68b21ecab0ad01234aebc615c48ea7 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 1 Apr 2023 19:32:55 -0400 Subject: [PATCH 0645/1013] [docs] tidying --- docs/upgrading.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/upgrading.md b/docs/upgrading.md index 4f1e1b481..7089bfc7b 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -18,6 +18,7 @@ ALL upgrades of this package should follow these steps: 5. Apply any version-specific special updates as outlined below... 6. Review the changelog, which details all the changes: https://github.com/spatie/laravel-permission/blob/main/CHANGELOG.md +and/or consult the [Release Notes](https://github.com/spatie/laravel-permission/releases) ### Upgrading to v6 From 95527721299e1dfe79d1c5b39d9a124ed2ecfd4f Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 5 Apr 2023 19:22:11 -0400 Subject: [PATCH 0646/1013] [docs] v6 note about no longer manually calling `registerPermissions()` --- docs/upgrading.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/upgrading.md b/docs/upgrading.md index 7089bfc7b..1c6c0ebc3 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -32,6 +32,9 @@ Be sure to compare your custom models with originals to see what else may have c 4. NOTE: For consistency with the `PermissionMiddleware`, the `RoleOrPermissionMiddleware` has switched from only checking permissions provided by this package to using `canAny()` to check against any abilities registered by your application. This may have the effect of granting those other abilities (such as Super Admin) when using the `RoleOrPermissionMiddleware`, which previously would have failed silently. +5. Test suites. If you have tests which manually clear the permission cache and re-register permissions, you no longer need to call `\Spatie\Permission\PermissionRegistrar::class)->registerPermissions();`. Such calls can be deleted from your tests. + + ### Upgrading from v1 to v2 If you're upgrading from v1 to v2, there's no built-in automatic migration/conversion of your data to the new structure. You will need to carefully adapt your code and your data manually. From f9b6573e3962cf06082d3dfde4db6c0dfbef6c90 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 7 Apr 2023 16:37:57 -0400 Subject: [PATCH 0647/1013] [docs] fix typo --- docs/advanced-usage/cache.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-usage/cache.md b/docs/advanced-usage/cache.md index 3b1f8160e..5a87370b9 100644 --- a/docs/advanced-usage/cache.md +++ b/docs/advanced-usage/cache.md @@ -53,7 +53,7 @@ If you wish to alter the expiration time you may do so in the `config/permission ### Cache Key The default cache key is `spatie.permission.cache`. -We recommend not changing the cache "key" name. Usually changing it is a bad idea. More likely setting the cache `prefix` is better, as mentioned above. +We recommend not changing the cache "key" name. Usually changing it is a bad idea. More likely setting the cache `prefix` is better, as mentioned below. ### Cache Identifier / Prefix From 0431448f9f9f2741c11a39d75f437be310f0ab19 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 7 Apr 2023 16:54:09 -0400 Subject: [PATCH 0648/1013] [Docs] Minor updates to testing docs --- docs/advanced-usage/testing.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/advanced-usage/testing.md b/docs/advanced-usage/testing.md index d0cd3e7fe..ac205980a 100644 --- a/docs/advanced-usage/testing.md +++ b/docs/advanced-usage/testing.md @@ -10,16 +10,18 @@ In your application's tests, if you are not seeding roles and permissions as par In your tests simply add a `setUp()` instruction to re-register the permissions, like this: ```php - public function setUp(): void + protected function setUp(): void { // first include all the normal setUp operations parent::setUp(); - // now re-register all the roles and permissions (clears cache and reloads relations) - $this->app->make(\Spatie\Permission\PermissionRegistrar::class)->registerPermissions(); + // now de-register all the roles and permissions by clearing the permission cache + $this->app->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions(); } ``` +## Clear Cache When Using Seeders + If you are using Laravel's `LazilyRefreshDatabase` trait, you most likely want to avoid seeding permissions before every test, because that would negate the use of the `LazilyRefreshDatabase` trait. To overcome this, you should wrap your seeder in an event listener for the `MigrationsEnded` event: ```php @@ -29,7 +31,7 @@ Event::listen(MigrationsEnded::class, function () { }); ``` -Note that we call `PermissionRegistrar::forgetCachedPermissions` after seeding. This is to prevent a caching issue that can occur when the database is set up after permissions have already been registered and cached. +Note that `PermissionRegistrar::forgetCachedPermissions()` is called AFTER seeding. This is to prevent a caching issue that can occur when the database is set up after permissions have already been registered and cached. ## Factories @@ -37,7 +39,4 @@ Many applications do not require using factories to create fake roles/permission However, if your application allows users to define their own roles and permissions you may wish to use Model Factories to generate roles and permissions as part of your test suite. -With Laravel 7 you can simply create a model factory using the artisan command, and then call the `factory()` helper function to invoke it as needed. - -With Laravel 8 if you want to use the class-based Model Factory features you will need to `extend` this package's `Role` and/or `Permission` model into your app's namespace, add the `HasFactory` trait to it, and define a model factory for it. Then you can use that factory in your seeders like any other factory related to your app's models. - +When using Laravel's class-based Model Factory features you will need to `extend` this package's `Role` and/or `Permission` model into your app's namespace, add the `HasFactory` trait to it, and define a model factory for it. Then you can use that factory in your seeders like any other factory related to your application's models. From 56baec6e4a9bbc6eb4dc51d9ecdd9aac8049ccfa Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 7 Apr 2023 17:10:29 -0400 Subject: [PATCH 0649/1013] [Docs] small comments on testing --- docs/advanced-usage/testing.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/advanced-usage/testing.md b/docs/advanced-usage/testing.md index ac205980a..f67ecd4a2 100644 --- a/docs/advanced-usage/testing.md +++ b/docs/advanced-usage/testing.md @@ -33,7 +33,21 @@ Event::listen(MigrationsEnded::class, function () { Note that `PermissionRegistrar::forgetCachedPermissions()` is called AFTER seeding. This is to prevent a caching issue that can occur when the database is set up after permissions have already been registered and cached. -## Factories + +## Bypassing Cache When Testing + +The caching infrastructure for this package is "always on", but when running your test suite you may wish to reduce its impact. + +Two things you might wish to explore include: + +- Change the cache driver to `array`. **Very often you will have already done this in your `phpunit.xml` configuration.** + +- Shorten cache lifetime to 1 second, by setting the config (not necessary if cache driver is set to `array`) in your test suite TestCase: + + `'permission.cache.expiration_time' = \DateInterval::createFromDateString('1 seconds')` + + +## Testing Using Factories Many applications do not require using factories to create fake roles/permissions for testing, because they use a Seeder to create specific roles and permissions that the application uses; thus tests are performed using the declared roles and permissions. From 50dc0aaf33f419b04c68b8955812baad596b2bce Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 7 Apr 2023 19:15:54 -0400 Subject: [PATCH 0650/1013] [Docs] Reviewed Demo app instructions NOTE: Tested and confirmed that these instructions are still valid with Laravel 8-10 --- docs/basic-usage/new-app.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index 449955d9d..b3f112d0b 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -9,7 +9,7 @@ If you want to just try out the features of this package you can get started wit The examples on this page are primarily added for assistance in creating a quick demo app for troubleshooting purposes, to post the repo on github for convenient sharing to collaborate or get support. -If you're new to Laravel or to any of the concepts mentioned here, you can learn more in the [Laravel documentation](https://laravel.com/docs/) and in the free videos at Laracasts such as with the [Laravel From Scratch series](https://laracasts.com/series/laravel-6-from-scratch/). +If you're new to Laravel or to any of the concepts mentioned here, you can learn more in the [Laravel documentation](https://laravel.com/docs/) and in the free videos at Laracasts such as with the [Laravel From Scratch series](https://laracasts.com/series/laravel-8-from-scratch/). ### Initial setup: From a444757340087a4f5220cf2bc097b077036577f6 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 7 Apr 2023 19:19:11 -0400 Subject: [PATCH 0651/1013] [Docs] refer to version matrix --- docs/prerequisites.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/prerequisites.md b/docs/prerequisites.md index fc64ed7a8..359276e01 100644 --- a/docs/prerequisites.md +++ b/docs/prerequisites.md @@ -5,7 +5,7 @@ weight: 3 ## Laravel Version -This package can be used in Laravel 6 or higher. +This package can be used in Laravel 6 or higher. Check the "Installing on Laravel" page for package versions compatible with various Laravel versions. ## User Model / Contract/Interface From 99443d5ed05dd4d5cc711bb64a08d3eaa8404ef8 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 7 Apr 2023 19:26:19 -0400 Subject: [PATCH 0652/1013] [Docs] update cross-link --- docs/prerequisites.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/prerequisites.md b/docs/prerequisites.md index 359276e01..b59d4cc2d 100644 --- a/docs/prerequisites.md +++ b/docs/prerequisites.md @@ -49,5 +49,5 @@ Thus in your AppServiceProvider you will need to set `Schema::defaultStringLengt ## Note for apps using UUIDs/ULIDs/GUIDs -This package expects the primary key of your `User` model to be an auto-incrementing `int`. If it is not, you may need to modify the `create_permission_tables` migration and/or modify the default configuration. See [https://spatie.be/docs/laravel-permission/v5/advanced-usage/uuid](https://spatie.be/docs/laravel-permission/v5/advanced-usage/uuid) for more information. +This package expects the primary key of your `User` model to be an auto-incrementing `int`. If it is not, you may need to modify the `create_permission_tables` migration and/or modify the default configuration. See [https://spatie.be/docs/laravel-permission/advanced-usage/uuid](https://spatie.be/docs/laravel-permission/advanced-usage/uuid) for more information. From 564104b5b19f052a3f8ce1ec7f2ec777a7d37299 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 7 Apr 2023 19:32:07 -0400 Subject: [PATCH 0653/1013] [Docs] fix link --- docs/installation-lumen.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation-lumen.md b/docs/installation-lumen.md index ff3d98455..dba274fd7 100644 --- a/docs/installation-lumen.md +++ b/docs/installation-lumen.md @@ -68,7 +68,7 @@ NOTE: Remember that Laravel's authorization layer requires that your `User` mode ### User Table NOTE: If you are working with a fresh install of Lumen, then you probably also need a migration file for your Users table. You can create your own, or you can copy a basic one from Laravel: -[https://github.com/laravel/laravel/blob/main/database/migrations/2014_10_12_000000_create_users_table.php](https://github.com/laravel/laravel/blob/main/database/migrations/2014_10_12_000000_create_users_table.php) +[https://github.com/laravel/laravel/blob/master/database/migrations/2014_10_12_000000_create_users_table.php](https://github.com/laravel/laravel/blob/master/database/migrations/2014_10_12_000000_create_users_table.php) (You will need to run `php artisan migrate` after adding this file.) From 73acbe7e9af379202b6ba49675977aaf859ec093 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 7 Apr 2023 19:34:21 -0400 Subject: [PATCH 0654/1013] [Docs] formatting --- docs/upgrading.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index 1c6c0ebc3..a543f6a56 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -3,11 +3,13 @@ title: Upgrading weight: 6 --- +# Upgrade Essentials + ALL upgrades of this package should follow these steps: 1. Upgrading between major versions of this package always require the usual Composer steps: - > 1. Update your `composer.json` to specify the new major version, such as `^6.0` - > 2. Then run `composer update`. + - Update your `composer.json` to specify the new major version, such as `^6.0` + - Then run `composer update`. 2. Compare the `migration` file stubs in the NEW version of this package against the migrations you've already run inside your app. If necessary, create a new migration (by hand) to apply any new changes. @@ -21,7 +23,7 @@ ALL upgrades of this package should follow these steps: and/or consult the [Release Notes](https://github.com/spatie/laravel-permission/releases) -### Upgrading to v6 +# Upgrading to v6 1. If you have overridden the `getPermissionClass()` or `getRoleClass()` methods or have custom Models, you will need to revisit those customizations. See PR #2368 for details. eg: if you have a custom model you will need to make changes, including accessing the model using `$this->permissionClass::` syntax (eg: using `::` instead of `->`) in all the overridden methods that make use of the models. Be sure to compare your custom models with originals to see what else may have changed. @@ -35,7 +37,7 @@ Be sure to compare your custom models with originals to see what else may have c 5. Test suites. If you have tests which manually clear the permission cache and re-register permissions, you no longer need to call `\Spatie\Permission\PermissionRegistrar::class)->registerPermissions();`. Such calls can be deleted from your tests. -### Upgrading from v1 to v2 +# Upgrading from v1 to v2 If you're upgrading from v1 to v2, there's no built-in automatic migration/conversion of your data to the new structure. You will need to carefully adapt your code and your data manually. From 1a11080a304ae147960083d3bf638a2a5ef6153d Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 7 Apr 2023 19:35:31 -0400 Subject: [PATCH 0655/1013] [Docs] formatting --- docs/upgrading.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index a543f6a56..837937440 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -3,7 +3,7 @@ title: Upgrading weight: 6 --- -# Upgrade Essentials +## Upgrade Essentials ALL upgrades of this package should follow these steps: @@ -23,7 +23,7 @@ ALL upgrades of this package should follow these steps: and/or consult the [Release Notes](https://github.com/spatie/laravel-permission/releases) -# Upgrading to v6 +## Upgrading to v6 1. If you have overridden the `getPermissionClass()` or `getRoleClass()` methods or have custom Models, you will need to revisit those customizations. See PR #2368 for details. eg: if you have a custom model you will need to make changes, including accessing the model using `$this->permissionClass::` syntax (eg: using `::` instead of `->`) in all the overridden methods that make use of the models. Be sure to compare your custom models with originals to see what else may have changed. @@ -37,7 +37,7 @@ Be sure to compare your custom models with originals to see what else may have c 5. Test suites. If you have tests which manually clear the permission cache and re-register permissions, you no longer need to call `\Spatie\Permission\PermissionRegistrar::class)->registerPermissions();`. Such calls can be deleted from your tests. -# Upgrading from v1 to v2 +## Upgrading from v1 to v2 If you're upgrading from v1 to v2, there's no built-in automatic migration/conversion of your data to the new structure. You will need to carefully adapt your code and your data manually. From 8d52451c88bc3dd4d90f231c2fb21eb634816fd7 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 7 Apr 2023 19:38:18 -0400 Subject: [PATCH 0656/1013] [Docs] minor updates --- docs/basic-usage/basic-usage.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/basic-usage/basic-usage.md b/docs/basic-usage/basic-usage.md index 28c30efd2..2db919ac1 100644 --- a/docs/basic-usage/basic-usage.md +++ b/docs/basic-usage/basic-usage.md @@ -29,28 +29,28 @@ $permission = Permission::create(['name' => 'edit articles']); ``` -A permission can be assigned to a role using 1 of these methods: +A permission can be assigned to a role using either of these methods: ```php $role->givePermissionTo($permission); $permission->assignRole($role); ``` -Multiple permissions can be synced to a role using 1 of these methods: +Multiple permissions can be synced to a role using either of these methods: ```php $role->syncPermissions($permissions); $permission->syncRoles($roles); ``` -A permission can be removed from a role using 1 of these methods: +A permission can be removed from a role using either of these methods: ```php $role->revokePermissionTo($permission); $permission->removeRole($role); ``` -If you're using multiple guards the `guard_name` attribute needs to be set as well. Read about it in the [using multiple guards](./multiple-guards) section of the readme. +If you're using multiple guards then the `guard_name` attribute must be set as well. Read about it in the [using multiple guards](./multiple-guards) section of the readme. The `HasRoles` trait adds Eloquent relationships to your models, which can be accessed directly or used as a base query: From 15539f1bd78c49698bdb9b3cd27462b92d07c747 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 7 Apr 2023 20:44:14 -0400 Subject: [PATCH 0657/1013] [Docs] Rewrite permissions-vs-roles explanation --- docs/best-practices/roles-vs-permissions.md | 29 ++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/docs/best-practices/roles-vs-permissions.md b/docs/best-practices/roles-vs-permissions.md index b7d99b01f..17bc60a91 100644 --- a/docs/best-practices/roles-vs-permissions.md +++ b/docs/best-practices/roles-vs-permissions.md @@ -3,9 +3,32 @@ title: Roles vs Permissions weight: 1 --- -It is generally best to code your app around testing against `permissions` only. (ie: when testing whether to grant access to something, in most cases it's wisest to check against a `permission`, not a `role`). That way you can always use the native Laravel `@can` and `can()` directives everywhere in your app. +Best-Practice for thinking about Roles vs Permissions is this: -Roles can still be used to group permissions for easy assignment to a user/model, and you can still use the role-based helper methods if truly necessary. But most app-related logic can usually be best controlled using the `can` methods, which allows Laravel's Gate layer to do all the heavy lifting. Sometimes certain groups of `route` rules may make best sense to group them around a `role`, but still, whenever possible, there is less overhead used if you can check against a specific `permission` instead. +**Roles** are best to only assign to **Users** in order to "**group**" people by "**sets of permissions**". + +**Permissions** are best assigned **to roles**. +The more granular/detailed your permission-names (such as separate permissions like "view document" and "edit document"), the easier it is to control access in your application. + +**Users** should *rarely* be given "direct" permissions. Best if Users inherit permissions via the Roles that they're assigned to. + +When designed this way, all the sections of your application can check for specific permissions needed to access certain features or perform certain actions AND this way you can always **use the native Laravel `@can` and `can()` directives everywhere** in your app, which allows Laravel's Gate layer to do all the heavy lifting. + +Example: it's safer to have your Views test `$user->can('view member addresses')` or `$user->can('edit document')`, INSTEAD of testing for `$user->hasRole('Editor')`. It's easier to control displaying a "section" of content vs edit/delete buttons if you have "view document" and "edit document" permissions defined. And then Writer role would get both "view" and "edit" assigned to it. And then the user would get the Writer role. + +This also allows you to treat permission names as static (only editable by developers), and then your application (almost) never needs to know anything about role names, so you could (almost) change role names at will. + +Summary: +- **users** have `roles` +- **roles** have `permissions` +- app always checks for `permissions` (as much as possible), not `roles` +- **views** check permission-names +- **policies** check permission-names +- **model policies** check permission-names +- **controller methods** check permission-names +- **middleware** check permission names, or sometimes role-names +- **routes** check permission-names, or maybe role-names if you need to code that way. + +Sometimes certain groups of `route` rules may make best sense to group them around a `role`, but still, whenever possible, there is less overhead used if you can check against a specific `permission` instead. -eg: `users` have `roles`, and `roles` have `permissions`, and your app always checks for `permissions`, not `roles`. From 3216513db86fffb7c4cdb9ae3f912ca3a786fd56 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 7 Apr 2023 20:48:24 -0400 Subject: [PATCH 0658/1013] [Docs] formatting --- docs/basic-usage/direct-permissions.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/basic-usage/direct-permissions.md b/docs/basic-usage/direct-permissions.md index cd6a49faa..c508ff9d5 100644 --- a/docs/basic-usage/direct-permissions.md +++ b/docs/basic-usage/direct-permissions.md @@ -3,6 +3,16 @@ title: Direct Permissions weight: 2 --- +## Best Practice + +It's better to assign permissions to Roles, and then assign Roles to Users. + +See https://spatie.be/docs/laravel-permission/best-practices/roles-vs-permissions for a deeper explanation. + +HOWEVER, If you have reason to directly assign individual permissions to specific users (instead of to roles assigned to those users), you can do that as described below: + +## Direct Permissions to Users + A permission can be given to any user: ```php @@ -59,8 +69,7 @@ You may also pass integers to lookup by permission id $user->hasAnyPermission(['edit articles', 1, 5]); ``` -Saved permissions will be registered with the `Illuminate\Auth\Access\Gate` class for the default guard. So you can -check if a user has a permission with Laravel's default `can` function: +Like all permissions assigned via roles, you can check if a user has a permission by using Laravel's default `can` function: ```php $user->can('edit articles'); From b04fcc451453d1f44559ca4e6669af1b7d287041 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 7 Apr 2023 20:51:29 -0400 Subject: [PATCH 0659/1013] [docs] tidying --- docs/best-practices/roles-vs-permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/best-practices/roles-vs-permissions.md b/docs/best-practices/roles-vs-permissions.md index 17bc60a91..ad94432aa 100644 --- a/docs/best-practices/roles-vs-permissions.md +++ b/docs/best-practices/roles-vs-permissions.md @@ -14,7 +14,7 @@ The more granular/detailed your permission-names (such as separate permissions l When designed this way, all the sections of your application can check for specific permissions needed to access certain features or perform certain actions AND this way you can always **use the native Laravel `@can` and `can()` directives everywhere** in your app, which allows Laravel's Gate layer to do all the heavy lifting. -Example: it's safer to have your Views test `$user->can('view member addresses')` or `$user->can('edit document')`, INSTEAD of testing for `$user->hasRole('Editor')`. It's easier to control displaying a "section" of content vs edit/delete buttons if you have "view document" and "edit document" permissions defined. And then Writer role would get both "view" and "edit" assigned to it. And then the user would get the Writer role. +Example: it's safer to have your Views test `@can('view member addresses')` or `@can('edit document')`, INSTEAD of testing for `$user->hasRole('Editor')`. It's easier to control displaying a "section" of content vs edit/delete buttons if you have "view document" and "edit document" permissions defined. And then Writer role would get both "view" and "edit" assigned to it. And then the user would get the Writer role. This also allows you to treat permission names as static (only editable by developers), and then your application (almost) never needs to know anything about role names, so you could (almost) change role names at will. From 7c72cd6ee6a7ae241bdf1ffae0aaa46180a7ba09 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 7 Apr 2023 20:57:26 -0400 Subject: [PATCH 0660/1013] [Docs] formatting --- docs/basic-usage/middleware.md | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index 8a1971877..976ae816b 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -17,19 +17,12 @@ Route::group(['middleware' => ['can:publish articles']], function () { This package comes with `RoleMiddleware`, `PermissionMiddleware` and `RoleOrPermissionMiddleware` middleware. You can add them inside your `app/Http/Kernel.php` file. -Note the differences between Laravel 10 and older versions of Laravel is the name of the `protected` property: +Note the property name difference between Laravel 10 and older versions of Laravel: -### Laravel 9 (and older) -```php -protected $routeMiddleware = [ - // ... - 'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class, - 'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class, - 'role_or_permission' => \Spatie\Permission\Middlewares\RoleOrPermissionMiddleware::class, -]; -``` -### Laravel 10 ```php +// Laravel 9 uses $routeMiddleware = [ +//protected $routeMiddleware = [ +// Laravel 10+ uses $middlewareAliases = [ protected $middlewareAliases = [ // ... 'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class, @@ -38,6 +31,8 @@ protected $middlewareAliases = [ ]; ``` +## Middleware via Routes + Then you can protect your routes using middleware rules: ```php @@ -74,6 +69,8 @@ Route::group(['middleware' => ['role_or_permission:super-admin|edit articles']], }); ``` +## Middleware with Controllers + You can protect your controllers similarly, by setting desired middleware in the constructor: ```php @@ -89,3 +86,5 @@ public function __construct() $this->middleware(['role_or_permission:super-admin|edit articles']); } ``` + +(You can use Laravel's Model Policy feature with your controller methods. See the Model Policies section of these docs.) From 7fe6044869d57d0d3f2987f69ac4a363ae896341 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 7 Apr 2023 21:02:01 -0400 Subject: [PATCH 0661/1013] [docs] wip --- docs/advanced-usage/seeding.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/advanced-usage/seeding.md b/docs/advanced-usage/seeding.md index fcff50ce7..d27bc2a1e 100644 --- a/docs/advanced-usage/seeding.md +++ b/docs/advanced-usage/seeding.md @@ -96,3 +96,5 @@ foreach ($permissionIdsByRole as $role => $permissionIds) { ); } ``` + +**CAUTION**: ANY TIME YOU DIRECTLY RUN DB QUERIES you are bypassing cache-control features. So you will need to manually flush the package cache AFTER running direct DB queries, even in a seeder. From 56c99ff8c4198a87d247e7fd32058f6caf1c6d16 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 7 Apr 2023 21:07:53 -0400 Subject: [PATCH 0662/1013] [docs] formatting --- docs/best-practices/performance.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/best-practices/performance.md b/docs/best-practices/performance.md index f2d302e68..81c40be78 100644 --- a/docs/best-practices/performance.md +++ b/docs/best-practices/performance.md @@ -3,6 +3,8 @@ title: Performance Tips weight: 10 --- +On small apps, most of the following will be moot, and unnecessary. + Often we think in terms of "roles have permissions" so we lookup a Role, and call `$role->givePermissionTo()` to indicate what users with that role are allowed to do. This is perfectly fine! @@ -12,12 +14,17 @@ you may find that things are more performant if you lookup the permission and as The end result is the same, but sometimes it runs quite a lot faster. Also, because of the way this package enforces some protections for you, on large databases you may find -that instead of creating permissions with `Permission::create([attributes])` it might be faster to -`$permission = Permission::make([attributes]); $permission->saveOrFail();` - -On small apps, most of the above will be moot, and unnecessary. +that instead of creating permissions with: +```php +Permission::create([attributes]); +``` +it might be faster (more performant) to use: +```php +$permission = Permission::make([attributes]); +$permission->saveOrFail(); +``` As always, if you choose to bypass the provided object methods for adding/removing/syncing roles and permissions by manipulating Role and Permission objects directly in the database, -you will need to manually reset the cache with the PermissionRegistrar's method for that, +**you will need to manually reset the package cache** with the PermissionRegistrar's method for that, as described in the Cache section of the docs. From fa4beb13a9ba73b3f9c1bc844891631ec8216613 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 7 Apr 2023 21:11:53 -0400 Subject: [PATCH 0663/1013] [Docs] formatting --- docs/advanced-usage/timestamps.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/advanced-usage/timestamps.md b/docs/advanced-usage/timestamps.md index 7ab577ba4..ca0b709fc 100644 --- a/docs/advanced-usage/timestamps.md +++ b/docs/advanced-usage/timestamps.md @@ -3,7 +3,7 @@ title: Timestamps weight: 10 --- -### Excluding Timestamps from JSON +## Excluding Timestamps from JSON If you want to exclude timestamps from JSON output of role/permission pivots, you can extend the Role and Permission models into your own App namespace and mark the pivot as hidden: @@ -11,10 +11,9 @@ If you want to exclude timestamps from JSON output of role/permission pivots, yo protected $hidden = ['pivot']; ``` -### Adding Timestamps to Pivots +## Adding Timestamps to Pivots If you want to add timestamps to your pivot tables, you can do it with a few steps: - update the tables by calling `$table->timestamps();` in a migration - - extend the Permission and Role models and add `->withTimestamps();` to the BelongsToMany relationshps for `roles()` and `permissions()` - - update your User models (wherever you use the HasRoles or HasPermissions traits) by adding `->withTimestamps();` to the BelongsToMany relationshps for `roles()` and `permissions()` - + - extend the `Permission` and `Role` models and add `->withTimestamps();` to the `BelongsToMany` relationshps for `roles()` and `permissions()` + - update your `User` models (wherever you use the `HasRoles` or `HasPermissions` traits) by adding `->withTimestamps();` to the `BelongsToMany` relationships for `roles()` and `permissions()` From bc9fff8cb46b13782b07331bf2ab1d13652f1c58 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 7 Apr 2023 21:19:06 -0400 Subject: [PATCH 0664/1013] [Docs] formatting --- docs/advanced-usage/custom-permission-check.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/advanced-usage/custom-permission-check.md b/docs/advanced-usage/custom-permission-check.md index f6bfb84d8..3003adda0 100644 --- a/docs/advanced-usage/custom-permission-check.md +++ b/docs/advanced-usage/custom-permission-check.md @@ -3,13 +3,13 @@ title: Custom Permission Check weight: 6 --- -By default, a method is registered on [Laravel's gate](https://laravel.com/docs/authorization). This method is responsible for checking if the user has the required permission or not. Whether a user has a permission or not is determined by checking the user's permissions stored in the database. +By default, this package registers a `Gate::before()` method call on [Laravel's gate](https://laravel.com/docs/authorization). This method is responsible for checking if the user has the required permission or not, for calls to `can()` helpers and most `model policies`. Whether a user has a permission or not is determined by checking the user's permissions stored in the database. However, in some cases, you might want to implement custom logic for checking if the user has a permission or not. Let's say that your application uses access tokens for authentication and when issuing the tokens, you add a custom claim containing all the permissions the user has. In this case, if you want to check whether the user has the required permission or not based on the permissions in your custom claim in the access token, then you need to implement your own logic for handling this. -You could, for example, create a `before` method to handle this: +You could, for example, create a `Gate::before()` method call to handle this: **app/Providers/AuthServiceProvider.php** ```php @@ -22,8 +22,9 @@ public function boot() }); } ``` -Here `hasTokenPermission` is a custom method you need to implement yourself. +Here `hasTokenPermission` is a **custom method you need to implement yourself**. ### Register Permission Check Method -By default, `register_permission_check_method` is set to `true`. -Only set this to false if you want to implement custom logic for checking permissions. \ No newline at end of file +By default, `register_permission_check_method` is set to `true`, which means this package operates using the default behavior described earlier. + +Only set this to false if you want to bypass the default operation and implement your own custom logic for checking permissions. From 02a40fecaa97ce4bcd199a31b053718ab6ae0f75 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 11 Apr 2023 02:43:19 -0400 Subject: [PATCH 0665/1013] Add BackedEnum support Fixes #1991 Note: Requires PHP 8.1+ /cc @ceejayoz /cc @edalzell --- docs/basic-usage/enums.md | 103 +++++++++++++++++++ src/Models/Role.php | 2 +- src/Traits/HasPermissions.php | 56 +++++++--- src/Traits/HasRoles.php | 30 +++++- tests/GateTest.php | 23 +++++ tests/HasPermissionsTest.php | 48 +++++++++ tests/HasRolesTest.php | 42 ++++++++ tests/TestModels/TestRolePermissionsEnum.php | 56 ++++++++++ tests/WildcardHasPermissionsTest.php | 41 ++++++++ 9 files changed, 381 insertions(+), 20 deletions(-) create mode 100644 docs/basic-usage/enums.md create mode 100644 tests/TestModels/TestRolePermissionsEnum.php diff --git a/docs/basic-usage/enums.md b/docs/basic-usage/enums.md new file mode 100644 index 000000000..b343efbf0 --- /dev/null +++ b/docs/basic-usage/enums.md @@ -0,0 +1,103 @@ +--- +title: Enums +weight: 3 +--- + +# Enum Prerequisites + +Must be using PHP 8.1 or higher. + +If you are using PHP 8.1+ you can implement Enums as native types. + +Internally, Enums implicitly implement `\BackedEnum`, which is how this package recognizes that you're passing an Enum. + + +# Code Requirements + +You can create your Enum object for use with Roles and/or Permissions. You will probably create separate Enums for Roles and for Permissions, although if your application needs are simple you might choose a single Enum for both. + +Usually the list of application Roles is much shorter than the list of Permissions, so having separate objects for them can make them easier to manage. + +Here is an example Enum for Roles. You would do similarly for Permissions. + +```php +namespace App\Enums; + +enum RolesEnum: string +{ + // case NAMEINAPP = 'name-in-database'; + + case WRITER = 'writer'; + case EDITOR = 'editor'; + case USERMANAGER = 'user-manager'; + + // extra helper to allow for greater customization of displayed values, without disclosing the name/value data directly + public function label(): string + { + return match ($this) { + static::WRITER => 'Writers', + static::EDITOR => 'Editors', + static::USERMANAGER => 'User Managers', + }; + } +} +``` + +# Using Enum names and values + +## Creating Roles/Permissions + +When creating roles/permissions, you cannot pass a Enum name directly, because Eloquent expects a string for the name. + +You must manually convert the name to its value in order to pass the correct string to Eloquent for the role/permission name. + +eg: use `RolesEnum::WRITER->value` when specifying the role/permission name + +```php + $role = app(Role::class)->findOrCreate(RolesEnum::WRITER->value, 'web'); +``` +Same with creating Permissions. + +## Authorizing using Enums + +In your application code, when checking for authorization using features of this package, you can use `MyEnum::NAME` directly in most cases, without passing `->value` to convert to a string. + +There will be times where you will need to manually fallback to adding `->value` (eg: `MyEnum::NAME->value`) when using features that aren't aware of Enum support. This will occur when you need to pass `string` values instead of an `Enum`, such as when interacting with Laravel's Gate via the `can()` methods/helpers (eg: `can`, `canAny`, etc). + +Examples: +```php +// the following are identical because `hasPermissionTo` is aware of `BackedEnum` support: +$user->hasPermissionTo(PermissionsEnum::VIEWPOSTS); +$user->hasPermissionTo(PermissionsEnum::VIEWPOSTS->value); + +// when calling Gate features, such as Model Policies, etc +$user->can(PermissionsEnum::VIEWPOSTS->value); +$model->can(PermissionsEnum::VIEWPOSTS->value); + +// Blade directives: +@can(PermissionsEnum::VIEWPOSTS->value) +``` + + +# Package methods supporting BackedEnums: +The following methods of this package support passing `BackedEnum` parameters directly: + +```php + $user->assignRole(RolesEnum::WRITER); + $user->removeRole(RolesEnum::EDITOR); + + $role->givePermissionTo(PermissionsEnum::EDITPOSTS); + $role->revokePermissionTo(PermissionsEnum::EDITPOSTS); + + $user->givePermissionTo(PermissionsEnum::EDITPOSTS); + $user->revokePermissionTo(PermissionsEnum::EDITPOSTS); + + $user->hasPermissionTo(PermissionsEnum::EDITPOSTS); + $user->hasAnyPermission([PermissionsEnum::EDITPOSTS, PermissionsEnum::VIEWPOSTS]); + $user->hasDirectPermission(PermissionsEnum::EDITPOSTS); + + $user->hasRole(RolesEnum::WRITER); + $user->hasAllRoles([RolesEnum::WRITER, RolesEnum::EDITOR]); + $user->hasExactRoles([RolesEnum::WRITER, RolesEnum::EDITOR, RolesEnum::MANAGER]); + +``` diff --git a/src/Models/Role.php b/src/Models/Role.php index f948b0aa3..1b68e99d9 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -167,7 +167,7 @@ protected static function findByParam(array $params = []) /** * Determine if the role may perform the given permission. * - * @param string|int|Permission $permission + * @param string|int|Permission|\BackedEnum $permission * @param string|null $guardName * * @throws PermissionDoesNotExist|GuardDoesNotMatch diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 160be5f93..ac561a4b6 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -95,7 +95,7 @@ public function permissions(): BelongsToMany /** * Scope the model query to certain permissions only. * - * @param string|int|array|Permission|Collection $permissions + * @param string|int|array|Permission|Collection|\BackedEnum $permissions */ public function scopePermission(Builder $query, $permissions): Builder { @@ -122,7 +122,7 @@ public function scopePermission(Builder $query, $permissions): Builder } /** - * @param string|int|array|Permission|Collection $permissions + * @param string|int|array|Permission|Collection|\BackedEnum $permissions * * @throws PermissionDoesNotExist */ @@ -136,6 +136,11 @@ protected function convertToPermissionModels($permissions): array if ($permission instanceof Permission) { return $permission; } + + if ($permission instanceof \BackedEnum) { + $permission = $permission->value; + } + $method = is_string($permission) && ! PermissionRegistrar::isUid($permission) ? 'findByName' : 'findById'; return $this->getPermissionClass()::{$method}($permission, $this->getDefaultGuardName()); @@ -145,7 +150,7 @@ protected function convertToPermissionModels($permissions): array /** * Find a permission. * - * @param string|int|Permission $permission + * @param string|int|Permission|\BackedEnum $permission * @return Permission * * @throws PermissionDoesNotExist @@ -159,6 +164,13 @@ public function filterPermission($permission, $guardName = null) ); } + if ($permission instanceof \BackedEnum) { + $permission = $this->getPermissionClass()::findByName( + $permission->value, + $guardName ?? $this->getDefaultGuardName() + ); + } + if (is_int($permission) || is_string($permission)) { $permission = $this->getPermissionClass()::findById( $permission, @@ -176,7 +188,7 @@ public function filterPermission($permission, $guardName = null) /** * Determine if the model may perform the given permission. * - * @param string|int|Permission $permission + * @param string|int|Permission|\BackedEnum $permission * @param string|null $guardName * * @throws PermissionDoesNotExist @@ -195,7 +207,7 @@ public function hasPermissionTo($permission, $guardName = null): bool /** * Validates a wildcard permission against all permissions of a user. * - * @param string|int|Permission $permission + * @param string|int|Permission|\BackedEnum $permission * @param string|null $guardName */ protected function hasWildcardPermission($permission, $guardName = null): bool @@ -210,6 +222,10 @@ protected function hasWildcardPermission($permission, $guardName = null): bool $permission = $permission->name; } + if ($permission instanceof \BackedEnum) { + $permission = $permission->value; + } + if (! is_string($permission)) { throw WildcardPermissionInvalidArgument::create(); } @@ -234,7 +250,7 @@ protected function hasWildcardPermission($permission, $guardName = null): bool /** * An alias to hasPermissionTo(), but avoids throwing an exception. * - * @param string|int|Permission $permission + * @param string|int|Permission|\BackedEnum $permission * @param string|null $guardName */ public function checkPermissionTo($permission, $guardName = null): bool @@ -249,7 +265,7 @@ public function checkPermissionTo($permission, $guardName = null): bool /** * Determine if the model has any of the given permissions. * - * @param string|int|array|Permission|Collection ...$permissions + * @param string|int|array|Permission|Collection|\BackedEnum ...$permissions */ public function hasAnyPermission(...$permissions): bool { @@ -267,7 +283,7 @@ public function hasAnyPermission(...$permissions): bool /** * Determine if the model has all of the given permissions. * - * @param string|int|array|Permission|Collection ...$permissions + * @param string|int|array|Permission|Collection|\BackedEnum ...$permissions */ public function hasAllPermissions(...$permissions): bool { @@ -297,7 +313,7 @@ protected function hasPermissionViaRole(Permission $permission): bool /** * Determine if the model has the given permission. * - * @param string|int|Permission $permission + * @param string|int|Permission|\BackedEnum $permission * * @throws PermissionDoesNotExist */ @@ -341,7 +357,7 @@ public function getAllPermissions(): Collection /** * Returns permissions ids as array keys * - * @param string|int|array|Permission|Collection $permissions + * @param string|int|array|Permission|Collection|\BackedEnum $permissions */ private function collectPermissions(...$permissions): array { @@ -369,7 +385,7 @@ private function collectPermissions(...$permissions): array /** * Grant the given permission(s) to a role. * - * @param string|int|array|Permission|Collection $permissions + * @param string|int|array|Permission|Collection|\BackedEnum $permissions * @return $this */ public function givePermissionTo(...$permissions) @@ -405,7 +421,7 @@ function ($object) use ($permissions, $model) { /** * Remove all current permissions and set the given ones. * - * @param string|int|array|Permission|Collection $permissions + * @param string|int|array|Permission|Collection|\BackedEnum $permissions * @return $this */ public function syncPermissions(...$permissions) @@ -420,7 +436,7 @@ public function syncPermissions(...$permissions) /** * Revoke the given permission(s). * - * @param Permission|Permission[]|string|string[] $permission + * @param Permission|Permission[]|string|string[]|\BackedEnum $permission * @return $this */ public function revokePermissionTo($permission) @@ -442,7 +458,7 @@ public function getPermissionNames(): Collection } /** - * @param string|int|array|Permission|Collection $permissions + * @param string|int|array|Permission|Collection|\BackedEnum $permissions * @return Permission|Permission[]|Collection */ protected function getStoredPermission($permissions) @@ -455,8 +471,16 @@ protected function getStoredPermission($permissions) return $this->getPermissionClass()::findByName($permissions, $this->getDefaultGuardName()); } + if ($permissions instanceof \BackedEnum) { + return $this->getPermissionClass()::findByName($permissions->value, $this->getDefaultGuardName()); + } + if (is_array($permissions)) { $permissions = array_map(function ($permission) { + if ($permission instanceof \BackedEnum) { + return $permission->value; + } + return is_a($permission, Permission::class) ? $permission->name : $permission; }, $permissions); @@ -501,7 +525,7 @@ public function forgetCachedPermissions() /** * Check if the model has All of the requested Direct permissions. * - * @param string|int|array|Permission|Collection ...$permissions + * @param string|int|array|Permission|Collection|\BackedEnum ...$permissions */ public function hasAllDirectPermissions(...$permissions): bool { @@ -519,7 +543,7 @@ public function hasAllDirectPermissions(...$permissions): bool /** * Check if the model has Any of the requested Direct permissions. * - * @param string|int|array|Permission|Collection ...$permissions + * @param string|int|array|Permission|Collection|\BackedEnum ...$permissions */ public function hasAnyDirectPermission(...$permissions): bool { diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 0feca7e52..5d6c70c4d 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -196,7 +196,7 @@ public function syncRoles(...$roles) /** * Determine if the model has (one of) the given role(s). * - * @param string|int|array|Role|Collection $roles + * @param string|int|array|Role|Collection|\BackedEnum $roles */ public function hasRole($roles, string $guard = null): bool { @@ -212,6 +212,12 @@ public function hasRole($roles, string $guard = null): bool : $this->roles->contains('name', $roles); } + if ($roles instanceof \BackedEnum) { + return $guard + ? $this->roles->where('guard_name', $guard)->contains('name', $roles->value) + : $this->roles->contains('name', $roles->value); + } + if (is_int($roles) || is_string($roles)) { $roleClass = $this->getRoleClass(); $key = (new $roleClass())->getKeyName(); @@ -235,7 +241,11 @@ public function hasRole($roles, string $guard = null): bool return false; } - return $roles->intersect($guard ? $this->roles->where('guard_name', $guard) : $this->roles)->isNotEmpty(); + if ($roles instanceof Collection) { + return $roles->intersect($guard ? $this->roles->where('guard_name', $guard) : $this->roles)->isNotEmpty(); + } + + throw new \TypeError('Unsupported type for $roles parameter to hasRole().'); } /** @@ -253,7 +263,7 @@ public function hasAnyRole(...$roles): bool /** * Determine if the model has all of the given role(s). * - * @param string|array|Role|Collection $roles + * @param string|array|Role|Collection|\BackedEnum $roles */ public function hasAllRoles($roles, string $guard = null): bool { @@ -269,11 +279,21 @@ public function hasAllRoles($roles, string $guard = null): bool : $this->roles->contains('name', $roles); } + if ($roles instanceof \BackedEnum) { + return $guard + ? $this->roles->where('guard_name', $guard)->contains('name', $roles->value) + : $this->roles->contains('name', $roles->value); + } + if ($roles instanceof Role) { return $this->roles->contains($roles->getKeyName(), $roles->getKey()); } $roles = collect()->make($roles)->map(function ($role) { + if ($role instanceof \BackedEnum) { + return $role->value; + } + return $role instanceof Role ? $role->name : $role; }); @@ -337,6 +357,10 @@ protected function getStoredRole($role): Role return $this->getRoleClass()::findByName($role, $this->getDefaultGuardName()); } + if ($role instanceof \BackedEnum) { + return $this->getRoleClass()::findByName($role->value, $this->getDefaultGuardName()); + } + return $role; } diff --git a/tests/GateTest.php b/tests/GateTest.php index dafbd6ad9..93359834a 100644 --- a/tests/GateTest.php +++ b/tests/GateTest.php @@ -3,6 +3,7 @@ namespace Spatie\Permission\Tests; use Illuminate\Contracts\Auth\Access\Gate; +use Spatie\Permission\Contracts\Permission; class GateTest extends TestCase { @@ -36,6 +37,28 @@ public function it_can_determine_if_a_user_has_a_direct_permission() $this->assertFalse($this->testUser->can('admin-permission')); } + /** + * @test + * + * @requires PHP >= 8.1 + */ + public function it_can_determine_if_a_user_has_a_direct_permission_using_enums() + { + $enum = TestModels\TestRolePermissionsEnum::VIEWARTICLES; + + $permission = app(Permission::class)->findOrCreate($enum->value, 'web'); + + $this->assertFalse($this->testUser->can($enum->value)); + $this->assertFalse($this->testUser->canAny([$enum->value, 'some other permission'])); + + $this->testUser->givePermissionTo($enum); + + $this->assertTrue($this->testUser->hasPermissionTo($enum)); + + $this->assertTrue($this->testUser->can($enum->value)); + $this->assertTrue($this->testUser->canAny([$enum->value, 'some other permission'])); + } + /** @test */ public function it_can_determine_if_a_user_has_a_permission_through_roles() { diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index 30c4839e6..5871ed46c 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -52,6 +52,54 @@ public function it_can_revoke_a_permission_from_a_user() $this->assertFalse($this->testUser->hasPermissionTo($this->testUserPermission)); } + /** + * @test + * + * @requires PHP >= 8.1 + */ + public function it_can_assign_and_remove_a_permission_using_enums() + { + $enum = TestModels\TestRolePermissionsEnum::VIEWARTICLES; + + $permission = app(Permission::class)->findOrCreate($enum->value, 'web'); + + $this->testUser->givePermissionTo($enum); + + $this->assertTrue($this->testUser->hasPermissionTo($enum)); + $this->assertTrue($this->testUser->hasAnyPermission($enum)); + $this->assertTrue($this->testUser->hasDirectPermission($enum)); + + $this->testUser->revokePermissionTo($enum); + + $this->assertFalse($this->testUser->hasPermissionTo($enum)); + $this->assertFalse($this->testUser->hasAnyPermission($enum)); + $this->assertFalse($this->testUser->hasDirectPermission($enum)); + } + + /** + * @test + * + * @requires PHP >= 8.1 + */ + public function it_can_scope_users_using_enums() + { + $enum1 = TestModels\TestRolePermissionsEnum::VIEWARTICLES; + $enum2 = TestModels\TestRolePermissionsEnum::EDITARTICLES; + $permission1 = app(Permission::class)->findOrCreate($enum1->value, 'web'); + $permission2 = app(Permission::class)->findOrCreate($enum2->value, 'web'); + $user1 = User::create(['email' => 'user1@test.com']); + $user2 = User::create(['email' => 'user2@test.com']); + $user1->givePermissionTo([$enum1, $enum2]); + $this->testUserRole->givePermissionTo($enum2); + $user2->assignRole('testRole'); + + $scopedUsers1 = User::permission($enum2)->get(); + $scopedUsers2 = User::permission([$enum1])->get(); + + $this->assertEquals(2, $scopedUsers1->count()); + $this->assertEquals(1, $scopedUsers2->count()); + } + /** @test */ public function it_can_scope_users_using_a_string() { diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index e1cbd0fa1..2d0c63ecc 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -38,6 +38,38 @@ public function it_can_determine_that_the_user_does_not_have_a_role() $this->assertFalse($this->testUser->hasRole($role)); } + /** + * @test + * + * @requires PHP >= 8.1 + */ + public function it_can_assign_and_remove_a_role_using_enums() + { + $enum1 = TestModels\TestRolePermissionsEnum::USERMANAGER; + $enum2 = TestModels\TestRolePermissionsEnum::WRITER; + + app(Role::class)->findOrCreate($enum1->value, 'web'); + app(Role::class)->findOrCreate($enum2->value, 'web'); + + $this->assertFalse($this->testUser->hasRole($enum1)); + $this->assertFalse($this->testUser->hasRole($enum2)); + + $this->testUser->assignRole($enum1); + $this->testUser->assignRole($enum2); + + $this->assertTrue($this->testUser->hasRole($enum1)); + $this->assertTrue($this->testUser->hasRole($enum2)); + + $this->assertTrue($this->testUser->hasAllRoles([$enum1, $enum2])); + $this->assertFalse($this->testUser->hasAllRoles([$enum1, $enum2, 'not exist'])); + + $this->assertTrue($this->testUser->hasExactRoles([$enum2, $enum1])); + + $this->testUser->removeRole($enum1); + + $this->assertFalse($this->testUser->hasRole($enum1)); + } + /** @test */ public function it_can_assign_and_remove_a_role() { @@ -575,6 +607,16 @@ public function it_returns_false_instead_of_an_exception_when_checking_against_a $this->assertFalse($this->testUser->hasAnyRole('This Role Does Not Even Exist', $this->testAdminRole)); } + /** @test */ + public function it_throws_an_exception_if_an_unsupported_type_is_passed_to_hasRoles() + { + $this->expectException(\TypeError::class); + + $this->testUser->hasRole(new class + { + }); + } + /** @test */ public function it_can_retrieve_role_names() { diff --git a/tests/TestModels/TestRolePermissionsEnum.php b/tests/TestModels/TestRolePermissionsEnum.php new file mode 100644 index 000000000..d5c73ebc1 --- /dev/null +++ b/tests/TestModels/TestRolePermissionsEnum.php @@ -0,0 +1,56 @@ +value when specifying the role/permission name + * + * In your application code, when checking for authorization, you can use MyEnum::NAME in most cases. + * You can always manually fallback to MyEnum::NAME->value when using features that aren't aware of Enum support. + * + * TestRolePermissionsEnum::USERMANAGER->name = 'USERMANAGER' + * TestRolePermissionsEnum::USERMANAGER->value = 'User Manager' <-- This is the role-name checked by this package + * TestRolePermissionsEnum::USERMANAGER->label() = 'Manage Users' + */ +enum TestRolePermissionsEnum: string +{ + // case NAME = 'value'; + // case NAMEINAPP = 'name-in-database'; + + case WRITER = 'writer'; + case EDITOR = 'editor'; + case USERMANAGER = 'user-manager'; + case ADMIN = 'administrator'; + + case VIEWARTICLES = 'view articles'; + case EDITARTICLES = 'edit articles'; + + case WildcardArticlesCreator = 'articles.edit,view,create'; + case WildcardNewsEverything = 'news.*'; + case WildcardPostsEverything = 'posts.*'; + + case WildcardPostsCreate = 'posts.create'; + case WildcardArticlesView = 'articles.view'; + case WildcardProjectsView = 'projects.view'; + + // extra helper to allow for greater customization of displayed values, without disclosing the name/value data directly + public function label(): string + { + return match ($this) { + static::WRITER => 'Writers', + static::EDITOR => 'Editors', + static::USERMANAGER => 'User Managers', + static::ADMIN => 'Admins', + + static::VIEWARTICLES => 'View Articles', + static::EDITARTICLES => 'Edit Articles', + }; + } +} diff --git a/tests/WildcardHasPermissionsTest.php b/tests/WildcardHasPermissionsTest.php index 4472bb706..873f35a96 100644 --- a/tests/WildcardHasPermissionsTest.php +++ b/tests/WildcardHasPermissionsTest.php @@ -31,6 +31,47 @@ public function it_can_check_wildcard_permission() $this->assertFalse($user1->hasPermissionTo('projects.view')); } + /** + * @test + * + * @requires PHP >= 8.1 + */ + public function it_can_assign_wildcard_permissions_using_enums() + { + app('config')->set('permission.enable_wildcard_permission', true); + + $user1 = User::create(['email' => 'user1@test.com']); + + $articlesCreator = TestModels\TestRolePermissionsEnum::WildcardArticlesCreator; + $newsEverything = TestModels\TestRolePermissionsEnum::WildcardNewsEverything; + $postsEverything = TestModels\TestRolePermissionsEnum::WildcardPostsEverything; + $postsCreate = TestModels\TestRolePermissionsEnum::WildcardPostsCreate; + + $permission1 = app(Permission::class)->findOrCreate($articlesCreator->value, 'web'); + $permission2 = app(Permission::class)->findOrCreate($newsEverything->value, 'web'); + $permission3 = app(Permission::class)->findOrCreate($postsEverything->value, 'web'); + + $user1->givePermissionTo([$permission1, $permission2, $permission3]); + + $this->assertTrue($user1->hasPermissionTo($postsCreate)); + $this->assertTrue($user1->hasPermissionTo($postsCreate->value.'.123')); + $this->assertTrue($user1->hasPermissionTo($postsEverything)); + + $this->assertTrue($user1->hasPermissionTo(TestModels\TestRolePermissionsEnum::WildcardArticlesView)); + $this->assertTrue($user1->hasAnyPermission(TestModels\TestRolePermissionsEnum::WildcardArticlesView)); + + $this->assertFalse($user1->hasPermissionTo(TestModels\TestRolePermissionsEnum::WildcardProjectsView)); + + $user1->revokePermissionTo([$permission1, $permission2, $permission3]); + + $this->assertFalse($user1->hasPermissionTo(TestModels\TestRolePermissionsEnum::WildcardPostsCreate)); + $this->assertFalse($user1->hasPermissionTo($postsCreate->value.'.123')); + $this->assertFalse($user1->hasPermissionTo(TestModels\TestRolePermissionsEnum::WildcardPostsEverything)); + + $this->assertFalse($user1->hasPermissionTo(TestModels\TestRolePermissionsEnum::WildcardArticlesView)); + $this->assertFalse($user1->hasAnyPermission(TestModels\TestRolePermissionsEnum::WildcardArticlesView)); + } + /** @test */ public function it_can_check_wildcard_permissions_via_roles() { From 017717cfb011d80cb9817bbd17bf832cd1110575 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Tue, 11 Apr 2023 16:16:35 -0500 Subject: [PATCH 0666/1013] BackedEnum tidying --- src/Traits/HasPermissions.php | 19 ++++++++----------- src/Traits/HasRoles.php | 28 ++++++++++++---------------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index ac561a4b6..52d833d6a 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -157,16 +157,13 @@ protected function convertToPermissionModels($permissions): array */ public function filterPermission($permission, $guardName = null) { - if (is_string($permission) && ! PermissionRegistrar::isUid($permission)) { - $permission = $this->getPermissionClass()::findByName( - $permission, - $guardName ?? $this->getDefaultGuardName() - ); + if ($permission instanceof \BackedEnum) { + $permission = $permission->value; } - if ($permission instanceof \BackedEnum) { + if (is_string($permission) && ! PermissionRegistrar::isUid($permission)) { $permission = $this->getPermissionClass()::findByName( - $permission->value, + $permission, $guardName ?? $this->getDefaultGuardName() ); } @@ -463,6 +460,10 @@ public function getPermissionNames(): Collection */ protected function getStoredPermission($permissions) { + if ($permissions instanceof \BackedEnum) { + $permissions = $permissions->value; + } + if (is_numeric($permissions) || PermissionRegistrar::isUid($permissions)) { return $this->getPermissionClass()::findById($permissions, $this->getDefaultGuardName()); } @@ -471,10 +472,6 @@ protected function getStoredPermission($permissions) return $this->getPermissionClass()::findByName($permissions, $this->getDefaultGuardName()); } - if ($permissions instanceof \BackedEnum) { - return $this->getPermissionClass()::findByName($permissions->value, $this->getDefaultGuardName()); - } - if (is_array($permissions)) { $permissions = array_map(function ($permission) { if ($permission instanceof \BackedEnum) { diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 5d6c70c4d..08c21a8c9 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -206,18 +206,16 @@ public function hasRole($roles, string $guard = null): bool $roles = $this->convertPipeToArray($roles); } + if ($roles instanceof \BackedEnum) { + $roles = $roles->value; + } + if (is_string($roles) && ! PermissionRegistrar::isUid($roles)) { return $guard ? $this->roles->where('guard_name', $guard)->contains('name', $roles) : $this->roles->contains('name', $roles); } - if ($roles instanceof \BackedEnum) { - return $guard - ? $this->roles->where('guard_name', $guard)->contains('name', $roles->value) - : $this->roles->contains('name', $roles->value); - } - if (is_int($roles) || is_string($roles)) { $roleClass = $this->getRoleClass(); $key = (new $roleClass())->getKeyName(); @@ -269,6 +267,10 @@ public function hasAllRoles($roles, string $guard = null): bool { $this->loadMissing('roles'); + if ($roles instanceof \BackedEnum) { + $roles = $roles->value; + } + if (is_string($roles) && false !== strpos($roles, '|')) { $roles = $this->convertPipeToArray($roles); } @@ -279,12 +281,6 @@ public function hasAllRoles($roles, string $guard = null): bool : $this->roles->contains('name', $roles); } - if ($roles instanceof \BackedEnum) { - return $guard - ? $this->roles->where('guard_name', $guard)->contains('name', $roles->value) - : $this->roles->contains('name', $roles->value); - } - if ($roles instanceof Role) { return $this->roles->contains($roles->getKeyName(), $roles->getKey()); } @@ -349,6 +345,10 @@ public function getRoleNames(): Collection protected function getStoredRole($role): Role { + if ($role instanceof \BackedEnum) { + $role = $role->value; + } + if (is_numeric($role) || PermissionRegistrar::isUid($role)) { return $this->getRoleClass()::findById($role, $this->getDefaultGuardName()); } @@ -357,10 +357,6 @@ protected function getStoredRole($role): Role return $this->getRoleClass()::findByName($role, $this->getDefaultGuardName()); } - if ($role instanceof \BackedEnum) { - return $this->getRoleClass()::findByName($role->value, $this->getDefaultGuardName()); - } - return $role; } From e0b009782f9c5becd99ec7dae17b3f91c9236b45 Mon Sep 17 00:00:00 2001 From: Ebuka Ifezue Date: Wed, 12 Apr 2023 08:13:15 +0000 Subject: [PATCH 0667/1013] Fix wrong model reference --- docs/advanced-usage/extending.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-usage/extending.md b/docs/advanced-usage/extending.md index 5fd067925..e177ba38b 100644 --- a/docs/advanced-usage/extending.md +++ b/docs/advanced-usage/extending.md @@ -6,7 +6,7 @@ weight: 4 ## Extending User Models Laravel's authorization features are available in models which implement the `Illuminate\Foundation\Auth\Access\Authorizable` trait. -By default Laravel does this in `\App\User` by extending `Illuminate\Foundation\Auth\User`, in which the trait and `Illuminate\Contracts\Auth\Access\Authorizable` contract are declared. +By default Laravel does this in `\App\Models\User` by extending `Illuminate\Foundation\Auth\User`, in which the trait and `Illuminate\Contracts\Auth\Access\Authorizable` contract are declared. If you are creating your own User models and wish Authorization features to be available, you need to implement `Illuminate\Contracts\Auth\Access\Authorizable` in one of those ways as well. From 6e0916dcbecf18e2e8372a8193695032a5dcfe2c Mon Sep 17 00:00:00 2001 From: erikn69 Date: Wed, 12 Apr 2023 08:43:51 -0500 Subject: [PATCH 0668/1013] Fix permission:show roles with underscores --- src/Commands/Show.php | 3 ++- tests/CommandTest.php | 26 +++++++++++++++----------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/Commands/Show.php b/src/Commands/Show.php index c80d12c1b..af243ba4b 100644 --- a/src/Commands/Show.php +++ b/src/Commands/Show.php @@ -62,8 +62,9 @@ public function handle() isset($teams) ? $teams->prepend(new TableCell(''))->toArray() : [], $roles->keys()->map(function ($val) { $name = explode('_', $val); + array_pop($name); - return $name[0]; + return implode('_', $name); }) ->prepend('')->toArray(), ), diff --git a/tests/CommandTest.php b/tests/CommandTest.php index 1ded858fe..923cad4e0 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -87,6 +87,9 @@ public function it_can_create_a_permission_without_duplication() /** @test */ public function it_can_show_permission_tables() { + Role::where('name', 'testRole2')->delete(); + Role::create(['name' => 'testRole_2']); + Artisan::call('permission:show'); $output = Artisan::output(); @@ -94,14 +97,13 @@ public function it_can_show_permission_tables() $this->assertTrue(strpos($output, 'Guard: web') !== false); $this->assertTrue(strpos($output, 'Guard: admin') !== false); + // | | testRole | testRole_2 | + // | edit-articles | · | · | if (method_exists($this, 'assertMatchesRegularExpression')) { - // | | testRole | testRole2 | - $this->assertMatchesRegularExpression('/\|\s+\|\s+testRole\s+\|\s+testRole2\s+\|/', $output); - - // | edit-articles | · | · | + $this->assertMatchesRegularExpression('/\|\s+\|\s+testRole\s+\|\s+testRole_2\s+\|/', $output); $this->assertMatchesRegularExpression('/\|\s+edit-articles\s+\|\s+·\s+\|\s+·\s+\|/', $output); } else { // phpUnit 9/8 - $this->assertRegExp('/\|\s+\|\s+testRole\s+\|\s+testRole2\s+\|/', $output); + $this->assertRegExp('/\|\s+\|\s+testRole\s+\|\s+testRole_2\s+\|/', $output); $this->assertRegExp('/\|\s+edit-articles\s+\|\s+·\s+\|\s+·\s+\|/', $output); } @@ -164,20 +166,22 @@ public function it_can_show_roles_by_teams() config()->set('permission.teams', true); app(\Spatie\Permission\PermissionRegistrar::class)->initializeCache(); - Role::create(['name' => 'testRoleTeam', 'team_test_id' => 1]); - Role::create(['name' => 'testRoleTeam', 'team_test_id' => 2]); // same name different team + Role::where('name', 'testRole2')->delete(); + Role::create(['name' => 'testRole_2']); + Role::create(['name' => 'testRole_Team', 'team_test_id' => 1]); + Role::create(['name' => 'testRole_Team', 'team_test_id' => 2]); // same name different team Artisan::call('permission:show'); $output = Artisan::output(); - // | | Team ID: NULL | Team ID: 1 | Team ID: 2 | - // | | testRole | testRole2 | testRoleTeam | testRoleTeam | + // | | Team ID: NULL | Team ID: 1 | Team ID: 2 | + // | | testRole | testRole_2 | testRole_Team | testRole_Team | if (method_exists($this, 'assertMatchesRegularExpression')) { $this->assertMatchesRegularExpression('/\|\s+\|\s+Team ID: NULL\s+\|\s+Team ID: 1\s+\|\s+Team ID: 2\s+\|/', $output); - $this->assertMatchesRegularExpression('/\|\s+\|\s+testRole\s+\|\s+testRole2\s+\|\s+testRoleTeam\s+\|\s+testRoleTeam\s+\|/', $output); + $this->assertMatchesRegularExpression('/\|\s+\|\s+testRole\s+\|\s+testRole_2\s+\|\s+testRole_Team\s+\|\s+testRole_Team\s+\|/', $output); } else { // phpUnit 9/8 $this->assertRegExp('/\|\s+\|\s+Team ID: NULL\s+\|\s+Team ID: 1\s+\|\s+Team ID: 2\s+\|/', $output); - $this->assertRegExp('/\|\s+\|\s+testRole\s+\|\s+testRole2\s+\|\s+testRoleTeam\s+\|\s+testRoleTeam\s+\|/', $output); + $this->assertRegExp('/\|\s+\|\s+testRole\s+\|\s+testRole_2\s+\|\s+testRole_Team\s+\|\s+testRole_Team\s+\|/', $output); } } } From 55c87477b57581b8b2b38ecbd9402e079e39f372 Mon Sep 17 00:00:00 2001 From: drbyte Date: Wed, 12 Apr 2023 19:10:38 +0000 Subject: [PATCH 0669/1013] Update CHANGELOG --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec4980c00..d07abe830 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.10.1 - 2023-04-12 + +### What's Changed + +- [V5] Fix artisan command `permission:show` output of roles with underscores by @erikn69 in https://github.com/spatie/laravel-permission/pull/2396 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.10.0...5.10.1 + ## 5.10.0 - 2023-03-22 ### What's Changed @@ -600,6 +608,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -654,6 +663,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From 6dd55adb2284d9935ec1fd6c43ca3a6b1e65ead2 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Wed, 12 Apr 2023 14:04:04 -0500 Subject: [PATCH 0670/1013] Phpstan fixes --- src/Models/Permission.php | 3 +-- src/PermissionRegistrar.php | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 631597fac..f5edcb90f 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -141,11 +141,10 @@ protected static function getPermissions(array $params = [], bool $onlyOne = fal /** * Get the current cached first permission. - * - * @return PermissionContract */ protected static function getPermission(array $params = []): ?PermissionContract { + /** @var PermissionContract|null */ return static::getPermissions($params, true)->first(); } } diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 2032be779..694865126 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -25,7 +25,7 @@ class PermissionRegistrar /** @var string */ protected $roleClass; - /** @var Collection|null */ + /** @var Collection|array|null */ protected $permissions; /** @var string */ From 7924d99d5e62b17f62acbcb4fd643caaf327e67b Mon Sep 17 00:00:00 2001 From: angeljqv <79208641+angeljqv@users.noreply.github.com> Date: Wed, 5 Apr 2023 15:46:35 -0500 Subject: [PATCH 0671/1013] Drop PHP 7.3 support --- .github/workflows/run-tests-L8.yml | 6 +----- composer.json | 5 ++--- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/run-tests-L8.yml b/.github/workflows/run-tests-L8.yml index a8e9ce842..dd9d0c446 100644 --- a/.github/workflows/run-tests-L8.yml +++ b/.github/workflows/run-tests-L8.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - php: [8.2, 8.1, 8.0, 7.4, 7.3] + php: [8.2, 8.1, 8.0, 7.4] laravel: [10.*, 9.*, 8.*] dependency-version: [prefer-lowest, prefer-stable] include: @@ -24,12 +24,8 @@ jobs: php: 8.0 - laravel: 10.* php: 7.4 - - laravel: 10.* - php: 7.3 - laravel: 9.* php: 7.4 - - laravel: 9.* - php: 7.3 name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} diff --git a/composer.json b/composer.json index 8a01fd579..9ea458849 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ ], "homepage": "https://github.com/spatie/laravel-permission", "require": { - "php": "^7.3|^8.0", + "php": "^7.4|^8.0", "illuminate/auth": "^8.0|^9.0|^10.0", "illuminate/container": "^8.0|^9.0|^10.0", "illuminate/contracts": "^8.0|^9.0|^10.0", @@ -30,8 +30,7 @@ }, "require-dev": { "orchestra/testbench": "^6.0|^7.0|^8.0", - "phpunit/phpunit": "^9.4", - "predis/predis": "^1.1" + "phpunit/phpunit": "^9.4" }, "minimum-stability": "dev", "prefer-stable": true, From 7a27aebcefc3bbcbb6febfd63cc5839c02f9ba36 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 12 Apr 2023 18:06:02 -0400 Subject: [PATCH 0672/1013] Add code for testing alternate cache stores --- tests/TestCase.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/TestCase.php b/tests/TestCase.php index a708ac196..b45718f0c 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -111,6 +111,11 @@ protected function getEnvironmentSetUp($app) $app['config']->set('auth.providers.users.model', User::class); $app['config']->set('cache.prefix', 'spatie_tests---'); + + // FOR MANUAL TESTING OF ALTERNATE CACHE STORES: + // $app['config']->set('cache.default', 'array'); + //Laravel supports: array, database, file + //requires extensions: apc, memcached, redis, dynamodb, octane } /** From 5e66ed149bfbb30bb9f7d9741e271c4a28db6946 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 12 Apr 2023 18:36:39 -0400 Subject: [PATCH 0673/1013] [Docs] Tidy display order --- docs/basic-usage/artisan.md | 2 +- docs/basic-usage/blade-directives.md | 2 +- docs/basic-usage/enums.md | 4 ++-- docs/basic-usage/middleware.md | 2 +- docs/basic-usage/multiple-guards.md | 2 +- docs/basic-usage/super-admin.md | 2 +- docs/basic-usage/teams-permissions.md | 2 +- docs/basic-usage/wildcard-permissions.md | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/basic-usage/artisan.md b/docs/basic-usage/artisan.md index 8cf340d1b..2091560de 100644 --- a/docs/basic-usage/artisan.md +++ b/docs/basic-usage/artisan.md @@ -1,6 +1,6 @@ --- title: Using artisan commands -weight: 7 +weight: 10 --- ## Creating roles and permissions with Artisan Commands diff --git a/docs/basic-usage/blade-directives.md b/docs/basic-usage/blade-directives.md index a3a4b8af1..e2c9f396a 100644 --- a/docs/basic-usage/blade-directives.md +++ b/docs/basic-usage/blade-directives.md @@ -1,6 +1,6 @@ --- title: Blade directives -weight: 4 +weight: 7 --- ## Permissions diff --git a/docs/basic-usage/enums.md b/docs/basic-usage/enums.md index b343efbf0..3f3b4ec99 100644 --- a/docs/basic-usage/enums.md +++ b/docs/basic-usage/enums.md @@ -1,11 +1,11 @@ --- title: Enums -weight: 3 +weight: 4 --- # Enum Prerequisites -Must be using PHP 8.1 or higher. +Requires PHP 8.1 or higher. If you are using PHP 8.1+ you can implement Enums as native types. diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index 976ae816b..6d2564728 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -1,6 +1,6 @@ --- title: Using a middleware -weight: 7 +weight: 11 --- ## Default Middleware diff --git a/docs/basic-usage/multiple-guards.md b/docs/basic-usage/multiple-guards.md index 27f453cb1..ef2848c9b 100644 --- a/docs/basic-usage/multiple-guards.md +++ b/docs/basic-usage/multiple-guards.md @@ -1,6 +1,6 @@ --- title: Using multiple guards -weight: 6 +weight: 9 --- When using the default Laravel auth configuration all of the core methods of this package will work out of the box, no extra configuration required. diff --git a/docs/basic-usage/super-admin.md b/docs/basic-usage/super-admin.md index 426bbc036..344a9a41e 100644 --- a/docs/basic-usage/super-admin.md +++ b/docs/basic-usage/super-admin.md @@ -1,6 +1,6 @@ --- title: Defining a Super-Admin -weight: 5 +weight: 8 --- We strongly recommend that a Super-Admin be handled by setting a global `Gate::before` or `Gate::after` rule which checks for the desired role. diff --git a/docs/basic-usage/teams-permissions.md b/docs/basic-usage/teams-permissions.md index d62fa92c8..3b2c49528 100644 --- a/docs/basic-usage/teams-permissions.md +++ b/docs/basic-usage/teams-permissions.md @@ -1,6 +1,6 @@ --- title: Teams permissions -weight: 3 +weight: 5 --- When enabled, teams permissions offers you flexible control for a variety of scenarios. The idea behind teams permissions is inspired by the default permission implementation of [Laratrust](https://laratrust.santigarcor.me/). diff --git a/docs/basic-usage/wildcard-permissions.md b/docs/basic-usage/wildcard-permissions.md index 31662e844..f13a58fa8 100644 --- a/docs/basic-usage/wildcard-permissions.md +++ b/docs/basic-usage/wildcard-permissions.md @@ -1,6 +1,6 @@ --- title: Wildcard permissions -weight: 3 +weight: 6 --- Wildcard permissions can be enabled in the permission config file: From 02ad48771f0722bbbff32aa7b5ec382f5b6dccd5 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Wed, 12 Apr 2023 17:04:04 -0500 Subject: [PATCH 0674/1013] Php 7.4 arrow function sintax --- database/migrations/add_teams_fields.php.stub | 4 - .../create_permission_tables.php.stub | 4 - src/Commands/Show.php | 26 +++--- src/Commands/UpgradeForTeams.php | 4 +- src/Guard.php | 14 +-- src/Models/Role.php | 6 +- src/PermissionRegistrar.php | 31 +++---- src/PermissionServiceProvider.php | 92 ++++++------------- src/Traits/HasPermissions.php | 15 ++- src/Traits/HasRoles.php | 8 +- src/helpers.php | 10 +- 11 files changed, 76 insertions(+), 138 deletions(-) diff --git a/database/migrations/add_teams_fields.php.stub b/database/migrations/add_teams_fields.php.stub index 9fb25ac09..b8935dd09 100644 --- a/database/migrations/add_teams_fields.php.stub +++ b/database/migrations/add_teams_fields.php.stub @@ -9,8 +9,6 @@ return new class extends Migration { /** * Run the migrations. - * - * @return void */ public function up(): void { @@ -85,8 +83,6 @@ return new class extends Migration /** * Reverse the migrations. - * - * @return void */ public function down(): void { diff --git a/database/migrations/create_permission_tables.php.stub b/database/migrations/create_permission_tables.php.stub index 5a7301abe..b865d480c 100644 --- a/database/migrations/create_permission_tables.php.stub +++ b/database/migrations/create_permission_tables.php.stub @@ -8,8 +8,6 @@ return new class extends Migration { /** * Run the migrations. - * - * @return void */ public function up(): void { @@ -122,8 +120,6 @@ return new class extends Migration /** * Reverse the migrations. - * - * @return void */ public function down(): void { diff --git a/src/Commands/Show.php b/src/Commands/Show.php index af243ba4b..8af347df3 100644 --- a/src/Commands/Show.php +++ b/src/Commands/Show.php @@ -36,25 +36,23 @@ public function handle() $roles = $roleClass::whereGuardName($guard) ->with('permissions') - ->when(config('permission.teams'), function ($q) use ($team_key) { - $q->orderBy($team_key); - }) - ->orderBy('name')->get()->mapWithKeys(function ($role) use ($team_key) { - return [$role->name.'_'.($role->$team_key ?: '') => ['permissions' => $role->permissions->pluck('id'), $team_key => $role->$team_key]]; - }); + ->when(config('permission.teams'), fn ($q) => $q->orderBy($team_key)) + ->orderBy('name')->get()->mapWithKeys(fn ($role) => + [$role->name.'_'.($role->$team_key ?: '') => ['permissions' => $role->permissions->pluck('id'), $team_key => $role->$team_key]] + ); $permissions = $permissionClass::whereGuardName($guard)->orderBy('name')->pluck('name', 'id'); - $body = $permissions->map(function ($permission, $id) use ($roles) { - return $roles->map(function (array $role_data) use ($id) { - return $role_data['permissions']->contains($id) ? ' ✔' : ' ·'; - })->prepend($permission); - }); + $body = $permissions->map(fn ($permission, $id) => + $roles->map(fn (array $role_data) => + $role_data['permissions']->contains($id) ? ' ✔' : ' ·' + )->prepend($permission) + ); if (config('permission.teams')) { - $teams = $roles->groupBy($team_key)->values()->map(function ($group, $id) { - return new TableCell('Team ID: '.($id ?: 'NULL'), ['colspan' => $group->count()]); - }); + $teams = $roles->groupBy($team_key)->values()->map(fn ($group, $id) => + new TableCell('Team ID: '.($id ?: 'NULL'), ['colspan' => $group->count()]) + ); } $this->table( diff --git a/src/Commands/UpgradeForTeams.php b/src/Commands/UpgradeForTeams.php index f42218cc7..1e1626b08 100644 --- a/src/Commands/UpgradeForTeams.php +++ b/src/Commands/UpgradeForTeams.php @@ -103,9 +103,7 @@ protected function alreadyExistingMigrations() { $matchingFiles = glob($this->getMigrationPath('*')); - return array_map(function ($path) { - return basename($path); - }, $matchingFiles); + return array_map(fn ($path) => basename($path), $matchingFiles); } /** diff --git a/src/Guard.php b/src/Guard.php index 376ffb897..83ffd4a8c 100644 --- a/src/Guard.php +++ b/src/Guard.php @@ -48,16 +48,10 @@ public static function getNames($model): Collection protected static function getConfigAuthGuards(string $class): Collection { return collect(config('auth.guards')) - ->map(function ($guard) { - if (! isset($guard['provider'])) { - return null; - } - - return config("auth.providers.{$guard['provider']}.model"); - }) - ->filter(function ($model) use ($class) { - return $class === $model; - }) + ->map(fn ($guard) => + isset($guard['provider']) ? config("auth.providers.{$guard['provider']}.model") : null + ) + ->filter(fn ($model) => $class === $model) ->keys(); } diff --git a/src/Models/Role.php b/src/Models/Role.php index 1b68e99d9..b52919593 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -150,10 +150,10 @@ protected static function findByParam(array $params = []) if (app(PermissionRegistrar::class)->teams) { $teamsKey = app(PermissionRegistrar::class)->teamsKey; - $query->where(function ($q) use ($params, $teamsKey) { + $query->where(fn ($q) => $q->whereNull($teamsKey) - ->orWhere($teamsKey, $params[$teamsKey] ?? getPermissionsTeamId()); - }); + ->orWhere($teamsKey, $params[$teamsKey] ?? getPermissionsTeamId()) + ); unset($params[$teamsKey]); } diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 694865126..184d92d71 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -70,7 +70,7 @@ public function __construct(CacheManager $cacheManager) $this->initializeCache(); } - public function initializeCache() + public function initializeCache(): void { $this->cacheExpirationTime = config('permission.cache.expiration_time') ?: \DateInterval::createFromDateString('24 hours'); @@ -109,7 +109,7 @@ protected function getCacheStoreFromConfig(): Repository * * @param int|string|\Illuminate\Database\Eloquent\Model $id */ - public function setPermissionsTeamId($id) + public function setPermissionsTeamId($id): void { if ($id instanceof \Illuminate\Database\Eloquent\Model) { $id = $id->getKey(); @@ -174,7 +174,7 @@ public function clearClassPermissions() * Load permissions from cache * This get cache and turns array into \Illuminate\Database\Eloquent\Collection */ - private function loadPermissions() + private function loadPermissions(): void { if ($this->permissions) { return; @@ -276,9 +276,8 @@ protected function getPermissionsWithRoles(): Collection private function aliasedArray($model): array { return collect(is_array($model) ? $model : $model->getAttributes())->except($this->except) - ->keyBy(function ($value, $key) { - return $this->alias[$key] ?? $key; - })->all(); + ->keyBy(fn ($value, $key) => $this->alias[$key] ?? $key) + ->all(); } /** @@ -301,7 +300,7 @@ private function aliasModelFields($newKeys = []): void /* * Make the cache smaller using an array with only required fields */ - private function getSerializedPermissionsForCache() + private function getSerializedPermissionsForCache(): array { $this->except = config('permission.cache.column_names_except', ['created_at', 'updated_at', 'deleted_at']); @@ -319,7 +318,7 @@ private function getSerializedPermissionsForCache() return ['alias' => array_flip($this->alias)] + compact('permissions', 'roles'); } - private function getSerializedRoleRelation($permission) + private function getSerializedRoleRelation($permission): array { if (! $permission->roles->count()) { return []; @@ -341,28 +340,28 @@ private function getSerializedRoleRelation($permission) ]; } - private function getHydratedPermissionCollection() + private function getHydratedPermissionCollection(): Collection { $permissionClass = $this->getPermissionClass(); $permissionInstance = new $permissionClass(); return Collection::make( - array_map(function ($item) use ($permissionInstance) { - return $permissionInstance + array_map(fn ($item) => + $permissionInstance ->newFromBuilder($this->aliasedArray(array_diff_key($item, ['r' => 0]))) - ->setRelation('roles', $this->getHydratedRoleCollection($item['r'] ?? [])); - }, $this->permissions['permissions']) + ->setRelation('roles', $this->getHydratedRoleCollection($item['r'] ?? [])) + , $this->permissions['permissions']) ); } - private function getHydratedRoleCollection(array $roles) + private function getHydratedRoleCollection(array $roles): Collection { return Collection::make(array_values( array_intersect_key($this->cachedRoles, array_flip($roles)) )); } - private function hydrateRolesCache() + private function hydrateRolesCache(): void { $roleClass = $this->getRoleClass(); $roleInstance = new $roleClass(); @@ -375,7 +374,7 @@ private function hydrateRolesCache() $this->permissions['roles'] = []; } - public static function isUid($value) + public static function isUid($value): bool { if (! is_string($value) || empty(trim($value))) { return false; diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 4cab16979..ec1dfb01b 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -44,9 +44,9 @@ public function register() 'permission' ); - $this->callAfterResolving('blade.compiler', function (BladeCompiler $bladeCompiler) { - $this->registerBladeExtensions($bladeCompiler); - }); + $this->callAfterResolving('blade.compiler', fn (BladeCompiler $bladeCompiler) => + $this->registerBladeExtensions($bladeCompiler) + ); } protected function offerPublishing() @@ -82,16 +82,12 @@ protected function registerCommands() protected function registerModelBindings() { - $this->app->bind(PermissionContract::class, function ($app) { - $config = $app->config['permission.models']; - - return $app->make($config['permission']); - }); - $this->app->bind(RoleContract::class, function ($app) { - $config = $app->config['permission.models']; - - return $app->make($config['role']); - }); + $this->app->bind(PermissionContract::class, fn ($app) => + $app->make($app->config['permission.models.permission']) + ); + $this->app->bind(RoleContract::class, fn ($app) => + $app->make($app->config['permission.models.role']) + ); } public static function bladeMethodWrapper($method, $role, $guard = null) @@ -101,50 +97,26 @@ public static function bladeMethodWrapper($method, $role, $guard = null) protected function registerBladeExtensions($bladeCompiler) { - $bladeCompiler->directive('role', function ($arguments) { - return ""; - }); - $bladeCompiler->directive('elserole', function ($arguments) { - return ""; - }); - $bladeCompiler->directive('endrole', function () { - return ''; - }); + $bladeMethodWrapper = '\\Spatie\\Permission\\PermissionServiceProvider::bladeMethodWrapper'; - $bladeCompiler->directive('hasrole', function ($arguments) { - return ""; - }); - $bladeCompiler->directive('endhasrole', function () { - return ''; - }); + $bladeCompiler->directive('role', fn ($args) => ""); + $bladeCompiler->directive('elserole', fn ($args) => ""); + $bladeCompiler->directive('endrole', fn () => ''); - $bladeCompiler->directive('hasanyrole', function ($arguments) { - return ""; - }); - $bladeCompiler->directive('endhasanyrole', function () { - return ''; - }); + $bladeCompiler->directive('hasrole', fn ($args) => ""); + $bladeCompiler->directive('endhasrole', fn () => ''); - $bladeCompiler->directive('hasallroles', function ($arguments) { - return ""; - }); - $bladeCompiler->directive('endhasallroles', function () { - return ''; - }); + $bladeCompiler->directive('hasanyrole', fn ($args) => ""); + $bladeCompiler->directive('endhasanyrole', fn () => ''); - $bladeCompiler->directive('unlessrole', function ($arguments) { - return ""; - }); - $bladeCompiler->directive('endunlessrole', function () { - return ''; - }); + $bladeCompiler->directive('hasallroles', fn ($args) => ""); + $bladeCompiler->directive('endhasallroles', fn () => '' ); - $bladeCompiler->directive('hasexactroles', function ($arguments) { - return ""; - }); - $bladeCompiler->directive('endhasexactroles', function () { - return ''; - }); + $bladeCompiler->directive('unlessrole', fn ($args) => ""); + $bladeCompiler->directive('endunlessrole', fn () => ''); + + $bladeCompiler->directive('hasexactroles', fn ($args) => ""); + $bladeCompiler->directive('endhasexactroles', fn () => ''); } protected function registerMacroHelpers() @@ -154,21 +126,13 @@ protected function registerMacroHelpers() } Route::macro('role', function ($roles = []) { - $roles = implode('|', Arr::wrap($roles)); - /** @var Route $this */ - $this->middleware("role:$roles"); - - return $this; + return $this->middleware("role:" . implode('|', Arr::wrap($roles))); }); Route::macro('permission', function ($permissions = []) { - $permissions = implode('|', Arr::wrap($permissions)); - /** @var Route $this */ - $this->middleware("permission:$permissions"); - - return $this; + return $this->middleware("permission:" . implode('|', Arr::wrap($permissions))); }); } @@ -182,9 +146,7 @@ protected function getMigrationFileName(string $migrationFileName): string $filesystem = $this->app->make(Filesystem::class); return Collection::make([$this->app->databasePath().DIRECTORY_SEPARATOR.'migrations'.DIRECTORY_SEPARATOR]) - ->flatMap(function ($path) use ($filesystem, $migrationFileName) { - return $filesystem->glob($path.'*_'.$migrationFileName); - }) + ->flatMap(fn ($path) => $filesystem->glob($path.'*_'.$migrationFileName)) ->push($this->app->databasePath()."/migrations/{$timestamp}_{$migrationFileName}") ->first(); } diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 52d833d6a..413b81386 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -22,7 +22,7 @@ trait HasPermissions /** @var string */ private $permissionClass; - /** @var string|false|null */ + /** @var string|null */ private $wildcardClass; public static function bootHasPermissions() @@ -59,7 +59,7 @@ protected function getWildcardClass() return $this->wildcardClass; } - $this->wildcardClass = false; + $this->wildcardClass = ''; if (config('permission.enable_wildcard_permission')) { $this->wildcardClass = config('permission.wildcard_permission', WildcardPermission::class); @@ -101,9 +101,9 @@ public function scopePermission(Builder $query, $permissions): Builder { $permissions = $this->convertToPermissionModels($permissions); - $rolesWithPermissions = is_a($this, Role::class) ? [] : array_unique(array_reduce($permissions, function ($result, $permission) { - return array_merge($result, $permission->roles->all()); - }, [])); + $rolesWithPermissions = is_a($this, Role::class) ? [] : array_unique(array_reduce($permissions, fn ($result, $permission) => + array_merge($result, $permission->roles->all()) + , [])); return $query->where(function (Builder $query) use ($permissions, $rolesWithPermissions) { $query->whereHas('permissions', function (Builder $subQuery) use ($permissions) { @@ -331,9 +331,8 @@ public function getPermissionsViaRoles(): Collection } return $this->loadMissing('roles', 'roles.permissions') - ->roles->flatMap(function ($role) { - return $role->permissions; - })->sort()->values(); + ->roles->flatMap(fn ($role) => $role->permissions) + ->sort()->values(); } /** diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 08c21a8c9..932e6ca25 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -321,9 +321,9 @@ public function hasExactRoles($roles, string $guard = null): bool $roles = [$roles->name]; } - $roles = collect()->make($roles)->map(function ($role) { - return $role instanceof Role ? $role->name : $role; - }); + $roles = collect()->make($roles)->map(fn ($role) => + $role instanceof Role ? $role->name : $role + ); return $this->roles->count() == $roles->count() && $this->hasAllRoles($roles, $guard); } @@ -365,7 +365,7 @@ protected function convertPipeToArray(string $pipeString) $pipeString = trim($pipeString); if (strlen($pipeString) <= 2) { - return $pipeString; + return [str_replace('|', '', $pipeString)]; } $quoteCharacter = substr($pipeString, 0, 1); diff --git a/src/helpers.php b/src/helpers.php index b29354655..120384bed 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -7,13 +7,9 @@ function getModelForGuard(string $guard) { return collect(config('auth.guards')) - ->map(function ($guard) { - if (! isset($guard['provider'])) { - return; - } - - return config("auth.providers.{$guard['provider']}.model"); - })->get($guard); + ->map(fn ($guard) => + isset($guard['provider']) ? config("auth.providers.{$guard['provider']}.model") : null + )->get($guard); } } From e5fd4dda64369e9c8d4d2965c2ef7656db0e8b65 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 12 Apr 2023 21:10:32 -0400 Subject: [PATCH 0675/1013] [Docs] Update headings syntax to show in docs website index --- docs/advanced-usage/cache.md | 7 ++++--- docs/advanced-usage/custom-permission-check.md | 9 +++++---- docs/advanced-usage/exceptions.md | 2 +- docs/advanced-usage/extending.md | 2 +- docs/advanced-usage/other.md | 2 +- docs/advanced-usage/phpstorm.md | 2 +- docs/advanced-usage/uuid.md | 12 ++++++------ docs/basic-usage/blade-directives.md | 2 +- docs/basic-usage/enums.md | 12 +++++------- docs/basic-usage/multiple-guards.md | 8 ++++---- docs/basic-usage/role-permissions.md | 4 ++-- docs/basic-usage/wildcard-permissions.md | 16 ++++++++++------ docs/installation-laravel.md | 2 +- docs/installation-lumen.md | 8 +++++--- 14 files changed, 47 insertions(+), 41 deletions(-) diff --git a/docs/advanced-usage/cache.md b/docs/advanced-usage/cache.md index 5a87370b9..fc6b5e59a 100644 --- a/docs/advanced-usage/cache.md +++ b/docs/advanced-usage/cache.md @@ -5,7 +5,7 @@ weight: 5 Role and Permission data are cached to speed up performance. -### Automatic Cache Refresh Using Built-In Functions +## Automatic Cache Refresh Using Built-In Functions When you **use the built-in functions** for manipulating roles and permissions, the cache is automatically reset for you, and relations are automatically reloaded for the current model record: @@ -31,7 +31,7 @@ $user->removeRole('writer'); $user->syncRoles(params); ``` -### Manual cache reset +## Manual cache reset To manually reset the cache for this package, you can run the following in your app code: ```php app()->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions(); @@ -43,6 +43,7 @@ php artisan permission:cache-reset ``` (This command is effectively an alias for `artisan cache:forget spatie.permission.cache` but respects the package config as well.) +## Cache Configuration Settings ### Cache Expiration Time @@ -71,7 +72,7 @@ You can configure the package to use any of the Cache Stores you've configured i In `config/permission.php` set `cache.store` to the name of any one of the `config/cache.php` stores you've defined. -#### Disabling Cache +## Disabling Cache Setting `'cache.store' => 'array'` in `config/permission.php` will effectively disable caching by this package between requests (it will only cache in-memory until the current request is completed processing, never persisting it). diff --git a/docs/advanced-usage/custom-permission-check.md b/docs/advanced-usage/custom-permission-check.md index 3003adda0..54e9b3035 100644 --- a/docs/advanced-usage/custom-permission-check.md +++ b/docs/advanced-usage/custom-permission-check.md @@ -3,8 +3,13 @@ title: Custom Permission Check weight: 6 --- +## Default Permission Check Functionality By default, this package registers a `Gate::before()` method call on [Laravel's gate](https://laravel.com/docs/authorization). This method is responsible for checking if the user has the required permission or not, for calls to `can()` helpers and most `model policies`. Whether a user has a permission or not is determined by checking the user's permissions stored in the database. +In the permission config file, `register_permission_check_method` is set to `true`, which means this package operates using the default behavior described above. Only set this to `false` if you want to bypass the default operation and implement your own custom logic for checking permissions, as described below. + +## Using Custom Permission Check Functionality + However, in some cases, you might want to implement custom logic for checking if the user has a permission or not. Let's say that your application uses access tokens for authentication and when issuing the tokens, you add a custom claim containing all the permissions the user has. In this case, if you want to check whether the user has the required permission or not based on the permissions in your custom claim in the access token, then you need to implement your own logic for handling this. @@ -24,7 +29,3 @@ public function boot() ``` Here `hasTokenPermission` is a **custom method you need to implement yourself**. -### Register Permission Check Method -By default, `register_permission_check_method` is set to `true`, which means this package operates using the default behavior described earlier. - -Only set this to false if you want to bypass the default operation and implement your own custom logic for checking permissions. diff --git a/docs/advanced-usage/exceptions.md b/docs/advanced-usage/exceptions.md index 6134ed59f..291f85feb 100644 --- a/docs/advanced-usage/exceptions.md +++ b/docs/advanced-usage/exceptions.md @@ -3,7 +3,7 @@ title: Exceptions weight: 3 --- -If you need to override exceptions thrown by this package, you can simply use normal [Laravel practices for handling exceptions](https://laravel.com/docs/8.x/errors#rendering-exceptions). +If you need to override exceptions thrown by this package, you can simply use normal [Laravel practices for handling exceptions](https://laravel.com/docs/10.x/errors#rendering-exceptions). An example is shown below for your convenience, but nothing here is specific to this package other than the name of the exception. diff --git a/docs/advanced-usage/extending.md b/docs/advanced-usage/extending.md index e177ba38b..ded3621ec 100644 --- a/docs/advanced-usage/extending.md +++ b/docs/advanced-usage/extending.md @@ -10,7 +10,7 @@ By default Laravel does this in `\App\Models\User` by extending `Illuminate\Foun If you are creating your own User models and wish Authorization features to be available, you need to implement `Illuminate\Contracts\Auth\Access\Authorizable` in one of those ways as well. -#### Child User Models +## Child User Models Due to the nature of polymorphism and Eloquent's hard-coded mapping of model names in the database, setting relationships for child models that inherit permissions of the parent can be difficult (even near impossible depending on app requirements, especially when attempting to do inverse mappings). However, one thing you might consider if you need the child model to never have its own permissions/roles but to only use its parent's permissions/roles, is to [override the `getMorphClass` method on the model](https://github.com/laravel/framework/issues/17830#issuecomment-345619085). diff --git a/docs/advanced-usage/other.md b/docs/advanced-usage/other.md index 5de498560..7fd4b2bcc 100644 --- a/docs/advanced-usage/other.md +++ b/docs/advanced-usage/other.md @@ -3,6 +3,6 @@ title: Other weight: 9 --- -Schema Diagram: +**Schema Diagram:** You can find a schema diagram at [https://drawsql.app/templates/laravel-permission](https://drawsql.app/templates/laravel-permission) diff --git a/docs/advanced-usage/phpstorm.md b/docs/advanced-usage/phpstorm.md index 23e1a7574..070e7973b 100644 --- a/docs/advanced-usage/phpstorm.md +++ b/docs/advanced-usage/phpstorm.md @@ -3,7 +3,7 @@ title: PhpStorm Interaction weight: 8 --- -# Extending PhpStorm +## Extending PhpStorm > **Note** > When using Laravel Idea plugin all directives are automatically added. diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index 299f83c64..517cf7eb5 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -10,7 +10,7 @@ If you're using UUIDs or GUIDs for your User models there are a few consideratio Since each UUID implementation approach is different, some of these may or may not benefit you. As always, your implementation may vary. -### Migrations +## Migrations You will probably want to update the `create_permission_tables.php` migration: If your User models are using `uuid` instead of `unsignedBigInteger` then you'll need to reflect the change in the migration provided by this package. Something like this would be typical, for both `model_has_permissions` and `model_has_roles` tables: @@ -61,7 +61,7 @@ OPTIONAL: If you also want the roles and permissions to use a UUID for their `id ``` -### Configuration (OPTIONAL) +## Configuration (OPTIONAL) You might want to change the pivot table field name from `model_id` to `model_uuid`, just for semantic purposes. For this, in the configuration file edit `column_names.model_morph_key`: @@ -79,7 +79,7 @@ For this, in the configuration file edit `column_names.model_morph_key`: ], - If you extend the models into your app, be sure to list those models in your configuration file. See the Extending section of the documentation and the Models section below. -### Models +## Models If you want all the role/permission objects to have a UUID instead of an integer, you will need to Extend the default Role and Permission models into your own namespace in order to set some specific properties. (See the Extending section of the docs, where it explains requirements of Extending, as well as the configuration settings you need to update.) - You likely want to set `protected $keyType = 'string';` so Laravel handles joins as strings and doesn't cast to integer. @@ -119,7 +119,7 @@ It is common to use a trait to handle the $keyType and $incrementing settings, a ``` -### User Models +## User Models > Troubleshooting tip: In the ***Prerequisites*** section of the docs we remind you that your User model must implement the `Illuminate\Contracts\Auth\Access\Authorizable` contract so that the Gate features are made available to the User object. In the default User model provided with Laravel, this is done by extending another model (aliased to `Authenticatable`), which extends the base Eloquent model. However, your app's UUID implementation may need to override that in order to set some of the properties mentioned in the Models section above. @@ -127,7 +127,7 @@ However, your app's UUID implementation may need to override that in order to se If you are running into difficulties, you may want to double-check whether your User model is doing UUIDs consistent with other parts of your app. -## REMINDER: +# REMINDER: > THIS IS NOT A FULL LESSON ON HOW TO IMPLEMENT UUIDs IN YOUR APP. @@ -135,7 +135,7 @@ Again, since each UUID implementation approach is different, some of these may o -### Packages +## Packages There are many packages offering UUID features for Eloquent models. You may want to explore whether these are of value to you in your study of implementing UUID in your applications: https://github.com/JamesHemery/laravel-uuid diff --git a/docs/basic-usage/blade-directives.md b/docs/basic-usage/blade-directives.md index e2c9f396a..51924866b 100644 --- a/docs/basic-usage/blade-directives.md +++ b/docs/basic-usage/blade-directives.md @@ -31,7 +31,7 @@ If you actually need to test for Roles, this package offers some Blade directive Optionally you can pass in the `guard` that the check will be performed on as a second argument. -#### Blade and Roles +## Blade and Roles Check for a specific role: ```php @role('writer') diff --git a/docs/basic-usage/enums.md b/docs/basic-usage/enums.md index 3f3b4ec99..dffce53f9 100644 --- a/docs/basic-usage/enums.md +++ b/docs/basic-usage/enums.md @@ -3,7 +3,7 @@ title: Enums weight: 4 --- -# Enum Prerequisites +## Enum Prerequisites Requires PHP 8.1 or higher. @@ -12,7 +12,7 @@ If you are using PHP 8.1+ you can implement Enums as native types. Internally, Enums implicitly implement `\BackedEnum`, which is how this package recognizes that you're passing an Enum. -# Code Requirements +## Code Requirements You can create your Enum object for use with Roles and/or Permissions. You will probably create separate Enums for Roles and for Permissions, although if your application needs are simple you might choose a single Enum for both. @@ -43,9 +43,7 @@ enum RolesEnum: string } ``` -# Using Enum names and values - -## Creating Roles/Permissions +## Creating Roles/Permissions using Enums When creating roles/permissions, you cannot pass a Enum name directly, because Eloquent expects a string for the name. @@ -58,7 +56,7 @@ eg: use `RolesEnum::WRITER->value` when specifying the role/permission name ``` Same with creating Permissions. -## Authorizing using Enums +### Authorizing using Enums In your application code, when checking for authorization using features of this package, you can use `MyEnum::NAME` directly in most cases, without passing `->value` to convert to a string. @@ -79,7 +77,7 @@ $model->can(PermissionsEnum::VIEWPOSTS->value); ``` -# Package methods supporting BackedEnums: +## Package methods supporting BackedEnums: The following methods of this package support passing `BackedEnum` parameters directly: ```php diff --git a/docs/basic-usage/multiple-guards.md b/docs/basic-usage/multiple-guards.md index ef2848c9b..08e44d1d2 100644 --- a/docs/basic-usage/multiple-guards.md +++ b/docs/basic-usage/multiple-guards.md @@ -7,14 +7,14 @@ When using the default Laravel auth configuration all of the core methods of thi However, when using multiple guards they will act like namespaces for your permissions and roles. Meaning every guard has its own set of permissions and roles that can be assigned to their user model. -### The Downside To Multiple Guards +## The Downside To Multiple Guards Note that this package requires you to register a permission name for each guard you want to authenticate with. So, "edit-article" would have to be created multiple times for each guard your app uses. An exception will be thrown if you try to authenticate against a non-existing permission+guard combination. Same for roles. > **Tip**: If your app uses only a single guard, but is not `web` (Laravel's default, which shows "first" in the auth config file) then change the order of your listed guards in your `config/auth.php` to list your primary guard as the default and as the first in the list of defined guards. While you're editing that file, best to remove any guards you don't use, too. -### Using permissions and roles with multiple guards +## Using permissions and roles with multiple guards When creating new permissions and roles, if no guard is specified, then the **first** defined guard in `auth.guards` config array will be used. @@ -42,12 +42,12 @@ $user->hasPermissionTo('publish articles', 'admin'); - then the `auth.defaults.guard` config (which is the user's guard if they are logged in, else the default in the file). -### Assigning permissions and roles to guard users +## Assigning permissions and roles to guard users You can use the same core methods to assign permissions and roles to users; just make sure the `guard_name` on the permission or role matches the guard of the user, otherwise a `GuardDoesNotMatch` or `Role/PermissionDoesNotExist` exception will be thrown. -### Using blade directives with multiple guards +## Using blade directives with multiple guards You can use all of the blade directives offered by this package by passing in the guard you wish to use as the second argument to the directive: diff --git a/docs/basic-usage/role-permissions.md b/docs/basic-usage/role-permissions.md index 476610957..ddfd0ce57 100644 --- a/docs/basic-usage/role-permissions.md +++ b/docs/basic-usage/role-permissions.md @@ -97,7 +97,7 @@ string or a `Spatie\Permission\Models\Permission` object. **NOTE: Permissions are inherited from roles automatically.** -### What Permissions Does A Role Have? +## What Permissions Does A Role Have? The `permissions` property on any given role returns a collection with all the related permission objects. This collection can respond to usual Eloquent Collection operations, such as count, sort, etc. @@ -174,6 +174,6 @@ the second will be a collection with the `edit article` permission and the third -### NOTE about using permission names in policies +## NOTE about using permission names in policies When calling `authorize()` for a policy method, if you have a permission named the same as one of those policy methods, your permission "name" will take precedence and not fire the policy. For this reason it may be wise to avoid naming your permissions the same as the methods in your policy. While you can define your own method names, you can read more about the defaults Laravel offers in Laravel's documentation at [Writing Policies](https://laravel.com/docs/authorization#writing-policies). diff --git a/docs/basic-usage/wildcard-permissions.md b/docs/basic-usage/wildcard-permissions.md index f13a58fa8..f1afc046d 100644 --- a/docs/basic-usage/wildcard-permissions.md +++ b/docs/basic-usage/wildcard-permissions.md @@ -3,6 +3,12 @@ title: Wildcard permissions weight: 6 --- +When enabled, wildcard permissions offers you a flexible representation for a variety of permission schemes. The idea + behind wildcard permissions is inspired by the default permission implementation of + [Apache Shiro](https://shiro.apache.org/permissions.html). + +## Enabling Wildcard Feature + Wildcard permissions can be enabled in the permission config file: ```php @@ -10,9 +16,7 @@ Wildcard permissions can be enabled in the permission config file: 'enable_wildcard_permission' => true, ``` -When enabled, wildcard permissions offers you a flexible representation for a variety of permission schemes. The idea - behind wildcard permissions is inspired by the default permission implementation of - [Apache Shiro](https://shiro.apache.org/permissions.html). +## Wildcard Syntax A wildcard permission string is made of one or more parts separated by dots (.). @@ -29,7 +33,7 @@ this is the common use-case, representing {resource}.{action}.{target}. > NOTE: You must create any wildcard permission patterns (eg: `posts.create.*`) before you can assign them or check for them. -### Using Wildcards +## Using Wildcards > ALERT: The `*` means "ALL". It does **not** mean "ANY". @@ -53,14 +57,14 @@ $user->can('posts.edit'); $user->can('posts.delete'); ``` -### Meaning of the `*` Asterisk +## Meaning of the `*` Asterisk The `*` means "ALL". It does **not** mean "ANY". Thus `can('post.*')` will only pass if the user has been assigned `post.*` explicitly. -### Subparts +## Subparts Besides the use of parts and wildcards, subparts can also be used. Subparts are divided with commas (,). This is a powerful feature that lets you create complex permission schemes. diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 65e20c29c..e1e3d5792 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -69,7 +69,7 @@ Package Version | Laravel Version . -### Default config file contents +## Default config file contents You can view the default config file contents at: diff --git a/docs/installation-lumen.md b/docs/installation-lumen.md index dba274fd7..70268977d 100644 --- a/docs/installation-lumen.md +++ b/docs/installation-lumen.md @@ -7,6 +7,8 @@ NOTE: Lumen is **not** officially supported by this package. However, the follow Lumen installation instructions can be found in the [Lumen documentation](https://lumen.laravel.com/docs/main). +## Installing + Install the permissions package via Composer: ``` bash @@ -61,15 +63,15 @@ php artisan migrate ``` --- -### User Model +## User Model NOTE: Remember that Laravel's authorization layer requires that your `User` model implement the `Illuminate\Contracts\Auth\Access\Authorizable` contract. In Lumen you will then also need to use the `Laravel\Lumen\Auth\Authorizable` trait. --- -### User Table +## User Table NOTE: If you are working with a fresh install of Lumen, then you probably also need a migration file for your Users table. You can create your own, or you can copy a basic one from Laravel: [https://github.com/laravel/laravel/blob/master/database/migrations/2014_10_12_000000_create_users_table.php](https://github.com/laravel/laravel/blob/master/database/migrations/2014_10_12_000000_create_users_table.php) (You will need to run `php artisan migrate` after adding this file.) -Remember to update your UserFactory.php to match the fields in the migration you create/copy. +Remember to update your `UserFactory.php` to match the fields in the migration you create/copy. From 33b016a3fc652dc1abb0fbdd5cad03c6e92294f5 Mon Sep 17 00:00:00 2001 From: drbyte Date: Thu, 13 Apr 2023 01:38:40 +0000 Subject: [PATCH 0676/1013] Fix styling --- src/Commands/Show.php | 10 +++------- src/Guard.php | 3 +-- src/Models/Role.php | 3 +-- src/PermissionRegistrar.php | 6 ++---- src/PermissionServiceProvider.php | 15 ++++++--------- src/Traits/HasPermissions.php | 4 +--- src/Traits/HasRoles.php | 3 +-- src/helpers.php | 3 +-- 8 files changed, 16 insertions(+), 31 deletions(-) diff --git a/src/Commands/Show.php b/src/Commands/Show.php index 8af347df3..8b41f9985 100644 --- a/src/Commands/Show.php +++ b/src/Commands/Show.php @@ -37,21 +37,17 @@ public function handle() $roles = $roleClass::whereGuardName($guard) ->with('permissions') ->when(config('permission.teams'), fn ($q) => $q->orderBy($team_key)) - ->orderBy('name')->get()->mapWithKeys(fn ($role) => - [$role->name.'_'.($role->$team_key ?: '') => ['permissions' => $role->permissions->pluck('id'), $team_key => $role->$team_key]] + ->orderBy('name')->get()->mapWithKeys(fn ($role) => [$role->name.'_'.($role->$team_key ?: '') => ['permissions' => $role->permissions->pluck('id'), $team_key => $role->$team_key]] ); $permissions = $permissionClass::whereGuardName($guard)->orderBy('name')->pluck('name', 'id'); - $body = $permissions->map(fn ($permission, $id) => - $roles->map(fn (array $role_data) => - $role_data['permissions']->contains($id) ? ' ✔' : ' ·' + $body = $permissions->map(fn ($permission, $id) => $roles->map(fn (array $role_data) => $role_data['permissions']->contains($id) ? ' ✔' : ' ·' )->prepend($permission) ); if (config('permission.teams')) { - $teams = $roles->groupBy($team_key)->values()->map(fn ($group, $id) => - new TableCell('Team ID: '.($id ?: 'NULL'), ['colspan' => $group->count()]) + $teams = $roles->groupBy($team_key)->values()->map(fn ($group, $id) => new TableCell('Team ID: '.($id ?: 'NULL'), ['colspan' => $group->count()]) ); } diff --git a/src/Guard.php b/src/Guard.php index 83ffd4a8c..c481c0640 100644 --- a/src/Guard.php +++ b/src/Guard.php @@ -48,8 +48,7 @@ public static function getNames($model): Collection protected static function getConfigAuthGuards(string $class): Collection { return collect(config('auth.guards')) - ->map(fn ($guard) => - isset($guard['provider']) ? config("auth.providers.{$guard['provider']}.model") : null + ->map(fn ($guard) => isset($guard['provider']) ? config("auth.providers.{$guard['provider']}.model") : null ) ->filter(fn ($model) => $class === $model) ->keys(); diff --git a/src/Models/Role.php b/src/Models/Role.php index b52919593..472a93847 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -150,8 +150,7 @@ protected static function findByParam(array $params = []) if (app(PermissionRegistrar::class)->teams) { $teamsKey = app(PermissionRegistrar::class)->teamsKey; - $query->where(fn ($q) => - $q->whereNull($teamsKey) + $query->where(fn ($q) => $q->whereNull($teamsKey) ->orWhere($teamsKey, $params[$teamsKey] ?? getPermissionsTeamId()) ); unset($params[$teamsKey]); diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 184d92d71..e95880154 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -346,11 +346,9 @@ private function getHydratedPermissionCollection(): Collection $permissionInstance = new $permissionClass(); return Collection::make( - array_map(fn ($item) => - $permissionInstance + array_map(fn ($item) => $permissionInstance ->newFromBuilder($this->aliasedArray(array_diff_key($item, ['r' => 0]))) - ->setRelation('roles', $this->getHydratedRoleCollection($item['r'] ?? [])) - , $this->permissions['permissions']) + ->setRelation('roles', $this->getHydratedRoleCollection($item['r'] ?? [])), $this->permissions['permissions']) ); } diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index ec1dfb01b..16e8f3287 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -44,8 +44,7 @@ public function register() 'permission' ); - $this->callAfterResolving('blade.compiler', fn (BladeCompiler $bladeCompiler) => - $this->registerBladeExtensions($bladeCompiler) + $this->callAfterResolving('blade.compiler', fn (BladeCompiler $bladeCompiler) => $this->registerBladeExtensions($bladeCompiler) ); } @@ -82,11 +81,9 @@ protected function registerCommands() protected function registerModelBindings() { - $this->app->bind(PermissionContract::class, fn ($app) => - $app->make($app->config['permission.models.permission']) + $this->app->bind(PermissionContract::class, fn ($app) => $app->make($app->config['permission.models.permission']) ); - $this->app->bind(RoleContract::class, fn ($app) => - $app->make($app->config['permission.models.role']) + $this->app->bind(RoleContract::class, fn ($app) => $app->make($app->config['permission.models.role']) ); } @@ -110,7 +107,7 @@ protected function registerBladeExtensions($bladeCompiler) $bladeCompiler->directive('endhasanyrole', fn () => ''); $bladeCompiler->directive('hasallroles', fn ($args) => ""); - $bladeCompiler->directive('endhasallroles', fn () => '' ); + $bladeCompiler->directive('endhasallroles', fn () => ''); $bladeCompiler->directive('unlessrole', fn ($args) => ""); $bladeCompiler->directive('endunlessrole', fn () => ''); @@ -127,12 +124,12 @@ protected function registerMacroHelpers() Route::macro('role', function ($roles = []) { /** @var Route $this */ - return $this->middleware("role:" . implode('|', Arr::wrap($roles))); + return $this->middleware('role:'.implode('|', Arr::wrap($roles))); }); Route::macro('permission', function ($permissions = []) { /** @var Route $this */ - return $this->middleware("permission:" . implode('|', Arr::wrap($permissions))); + return $this->middleware('permission:'.implode('|', Arr::wrap($permissions))); }); } diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 413b81386..53b7f8dbc 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -101,9 +101,7 @@ public function scopePermission(Builder $query, $permissions): Builder { $permissions = $this->convertToPermissionModels($permissions); - $rolesWithPermissions = is_a($this, Role::class) ? [] : array_unique(array_reduce($permissions, fn ($result, $permission) => - array_merge($result, $permission->roles->all()) - , [])); + $rolesWithPermissions = is_a($this, Role::class) ? [] : array_unique(array_reduce($permissions, fn ($result, $permission) => array_merge($result, $permission->roles->all()), [])); return $query->where(function (Builder $query) use ($permissions, $rolesWithPermissions) { $query->whereHas('permissions', function (Builder $subQuery) use ($permissions) { diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 932e6ca25..8fce2d4f0 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -321,8 +321,7 @@ public function hasExactRoles($roles, string $guard = null): bool $roles = [$roles->name]; } - $roles = collect()->make($roles)->map(fn ($role) => - $role instanceof Role ? $role->name : $role + $roles = collect()->make($roles)->map(fn ($role) => $role instanceof Role ? $role->name : $role ); return $this->roles->count() == $roles->count() && $this->hasAllRoles($roles, $guard); diff --git a/src/helpers.php b/src/helpers.php index 120384bed..0f765fa4e 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -7,8 +7,7 @@ function getModelForGuard(string $guard) { return collect(config('auth.guards')) - ->map(fn ($guard) => - isset($guard['provider']) ? config("auth.providers.{$guard['provider']}.model") : null + ->map(fn ($guard) => isset($guard['provider']) ? config("auth.providers.{$guard['provider']}.model") : null )->get($guard); } } From cd37572ea5ad81a077a857aa7aa3c764779f7782 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Thu, 13 Apr 2023 10:28:13 -0500 Subject: [PATCH 0677/1013] Php 7.4 typed properties --- phpstan-baseline.neon | 5 +++ src/Commands/Show.php | 11 ++++--- src/Commands/UpgradeForTeams.php | 4 +-- src/Guard.php | 3 +- src/PermissionRegistrar.php | 53 +++++++++++++------------------- src/Traits/HasPermissions.php | 26 ++++++++-------- src/Traits/HasRoles.php | 21 ++++++------- 7 files changed, 58 insertions(+), 65 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 364905f71..a1b0ec7db 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,2 +1,7 @@ parameters: ignoreErrors: + - + # PHPStan can't understand what's going on in context of Role class using Builder `when` + message: "#^Call to an undefined method Spatie\\\\Permission\\\\Models\\\\Role::getRoleClass\\(\\).$#" + count: 1 + path: src\Traits\HasPermissions.php diff --git a/src/Commands/Show.php b/src/Commands/Show.php index 8b41f9985..d4f39b003 100644 --- a/src/Commands/Show.php +++ b/src/Commands/Show.php @@ -37,17 +37,20 @@ public function handle() $roles = $roleClass::whereGuardName($guard) ->with('permissions') ->when(config('permission.teams'), fn ($q) => $q->orderBy($team_key)) - ->orderBy('name')->get()->mapWithKeys(fn ($role) => [$role->name.'_'.($role->$team_key ?: '') => ['permissions' => $role->permissions->pluck('id'), $team_key => $role->$team_key]] - ); + ->orderBy('name')->get()->mapWithKeys(fn ($role) => [ + $role->name.'_'.($role->$team_key ?: '') => ['permissions' => $role->permissions->pluck('id'), $team_key => $role->$team_key], + ]); $permissions = $permissionClass::whereGuardName($guard)->orderBy('name')->pluck('name', 'id'); - $body = $permissions->map(fn ($permission, $id) => $roles->map(fn (array $role_data) => $role_data['permissions']->contains($id) ? ' ✔' : ' ·' + $body = $permissions->map(fn ($permission, $id) => $roles->map( + fn (array $role_data) => $role_data['permissions']->contains($id) ? ' ✔' : ' ·' )->prepend($permission) ); if (config('permission.teams')) { - $teams = $roles->groupBy($team_key)->values()->map(fn ($group, $id) => new TableCell('Team ID: '.($id ?: 'NULL'), ['colspan' => $group->count()]) + $teams = $roles->groupBy($team_key)->values()->map( + fn ($group, $id) => new TableCell('Team ID: '.($id ?: 'NULL'), ['colspan' => $group->count()]) ); } diff --git a/src/Commands/UpgradeForTeams.php b/src/Commands/UpgradeForTeams.php index 1e1626b08..8d7f9004d 100644 --- a/src/Commands/UpgradeForTeams.php +++ b/src/Commands/UpgradeForTeams.php @@ -88,9 +88,7 @@ protected function getExistingMigrationsWarning(array $existingMigrations) $base = "Setup teams migration already exists.\nFollowing file was found: "; } - return $base.array_reduce($existingMigrations, function ($carry, $fileName) { - return $carry."\n - ".$fileName; - }); + return $base.array_reduce($existingMigrations, fn ($carry, $fileName) => $carry."\n - ".$fileName); } /** diff --git a/src/Guard.php b/src/Guard.php index c481c0640..b5904cc4d 100644 --- a/src/Guard.php +++ b/src/Guard.php @@ -48,8 +48,7 @@ public static function getNames($model): Collection protected static function getConfigAuthGuards(string $class): Collection { return collect(config('auth.guards')) - ->map(fn ($guard) => isset($guard['provider']) ? config("auth.providers.{$guard['provider']}.model") : null - ) + ->map(fn ($guard) => isset($guard['provider']) ? config("auth.providers.{$guard['provider']}.model") : null) ->filter(fn ($model) => $class === $model) ->keys(); } diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index e95880154..6e1574f63 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -13,50 +13,38 @@ class PermissionRegistrar { - /** @var Repository */ - protected $cache; + protected Repository $cache; - /** @var CacheManager */ - protected $cacheManager; + protected CacheManager $cacheManager; - /** @var string */ - protected $permissionClass; + protected string $permissionClass; - /** @var string */ - protected $roleClass; + protected string $roleClass; /** @var Collection|array|null */ protected $permissions; - /** @var string */ - public $pivotRole; + public string $pivotRole; - /** @var string */ - public $pivotPermission; + public string $pivotPermission; /** @var \DateInterval|int */ public $cacheExpirationTime; - /** @var bool */ - public $teams; + public bool $teams; - /** @var string */ - public $teamsKey; + public string $teamsKey; /** @var int|string */ protected $teamId = null; - /** @var string */ - public $cacheKey; + public string $cacheKey; - /** @var array */ - private $cachedRoles = []; + private array $cachedRoles = []; - /** @var array */ - private $alias = []; + private array $alias = []; - /** @var array */ - private $except = []; + private array $except = []; /** * PermissionRegistrar constructor. @@ -180,9 +168,9 @@ private function loadPermissions(): void return; } - $this->permissions = $this->cache->remember($this->cacheKey, $this->cacheExpirationTime, function () { - return $this->getSerializedPermissionsForCache(); - }); + $this->permissions = $this->cache->remember( + $this->cacheKey, $this->cacheExpirationTime, fn () => $this->getSerializedPermissionsForCache() + ); // fallback for old cache method, must be removed on next mayor version if (! isset($this->permissions['alias'])) { @@ -345,11 +333,12 @@ private function getHydratedPermissionCollection(): Collection $permissionClass = $this->getPermissionClass(); $permissionInstance = new $permissionClass(); - return Collection::make( - array_map(fn ($item) => $permissionInstance - ->newFromBuilder($this->aliasedArray(array_diff_key($item, ['r' => 0]))) - ->setRelation('roles', $this->getHydratedRoleCollection($item['r'] ?? [])), $this->permissions['permissions']) - ); + return Collection::make(array_map( + fn ($item) => $permissionInstance + ->newFromBuilder($this->aliasedArray(array_diff_key($item, ['r' => 0]))) + ->setRelation('roles', $this->getHydratedRoleCollection($item['r'] ?? [])), + $this->permissions['permissions'] + )); } private function getHydratedRoleCollection(array $roles): Collection diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 53b7f8dbc..fcd461fe1 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -19,11 +19,9 @@ trait HasPermissions { - /** @var string */ - private $permissionClass; + private ?string $permissionClass = null; - /** @var string|null */ - private $wildcardClass; + private ?string $wildcardClass = null; public static function bootHasPermissions() { @@ -101,22 +99,24 @@ public function scopePermission(Builder $query, $permissions): Builder { $permissions = $this->convertToPermissionModels($permissions); - $rolesWithPermissions = is_a($this, Role::class) ? [] : array_unique(array_reduce($permissions, fn ($result, $permission) => array_merge($result, $permission->roles->all()), [])); + $rolesWithPermissions = is_a($this, Role::class) ? [] : array_unique( + array_reduce($permissions, fn ($result, $permission) => array_merge($result, $permission->roles->all()), []) + ); - return $query->where(function (Builder $query) use ($permissions, $rolesWithPermissions) { - $query->whereHas('permissions', function (Builder $subQuery) use ($permissions) { + return $query->where(fn (Builder $query) => $query + ->whereHas('permissions', function (Builder $subQuery) use ($permissions) { $permissionClass = $this->getPermissionClass(); $key = (new $permissionClass())->getKeyName(); $subQuery->whereIn(config('permission.table_names.permissions').".$key", \array_column($permissions, $key)); - }); - if (count($rolesWithPermissions) > 0 && ! is_a($this, Role::class)) { - $query->orWhereHas('roles', function (Builder $subQuery) use ($rolesWithPermissions) { + }) + ->when(count($rolesWithPermissions) > 0, fn ($subQuery) => $subQuery + ->orWhereHas('roles', function (Builder $subQuery) use ($rolesWithPermissions) { $roleClass = $this->getRoleClass(); $key = (new $roleClass())->getKeyName(); $subQuery->whereIn(config('permission.table_names.roles').".$key", \array_column($rolesWithPermissions, $key)); - }); - } - }); + }) + ) + ); } /** diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 8fce2d4f0..9a4d9187e 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -14,8 +14,7 @@ trait HasRoles { use HasPermissions; - /** @var string */ - private $roleClass; + private ?string $roleClass = null; public static function bootHasRoles() { @@ -60,11 +59,10 @@ public function roles(): BelongsToMany return $relation; } + $teamField = config('permission.table_names.roles').'.'.app(PermissionRegistrar::class)->teamsKey; + return $relation->wherePivot(app(PermissionRegistrar::class)->teamsKey, getPermissionsTeamId()) - ->where(function ($q) { - $teamField = config('permission.table_names.roles').'.'.app(PermissionRegistrar::class)->teamsKey; - $q->whereNull($teamField)->orWhere($teamField, getPermissionsTeamId()); - }); + ->where(fn ($q) => $q->whereNull($teamField)->orWhere($teamField, getPermissionsTeamId())); } /** @@ -89,11 +87,12 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder return $this->getRoleClass()::{$method}($role, $guard ?: $this->getDefaultGuardName()); }, Arr::wrap($roles)); - return $query->whereHas('roles', function (Builder $subQuery) use ($roles) { - $roleClass = $this->getRoleClass(); - $key = (new $roleClass())->getKeyName(); - $subQuery->whereIn(config('permission.table_names.roles').".$key", \array_column($roles, $key)); - }); + $roleClass = $this->getRoleClass(); + $key = (new $roleClass())->getKeyName(); + + return $query->whereHas('roles', fn (Builder $subQuery) => $subQuery + ->whereIn(config('permission.table_names.roles').".$key", \array_column($roles, $key)) + ); } /** From cb1963a863bbd56791f26e1e6b54b92203a1d3b6 Mon Sep 17 00:00:00 2001 From: angeljqv <79208641+angeljqv@users.noreply.github.com> Date: Fri, 14 Apr 2023 10:46:01 -0500 Subject: [PATCH 0678/1013] don't add commands in web interface context --- src/PermissionServiceProvider.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 16e8f3287..7d7975b47 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -48,7 +48,7 @@ public function register() ); } - protected function offerPublishing() + protected function offerPublishing(): void { if (! $this->app->runningInConsole()) { return; @@ -68,10 +68,17 @@ protected function offerPublishing() ], 'permission-migrations'); } - protected function registerCommands() + protected function registerCommands(): void { $this->commands([ Commands\CacheReset::class, + ]); + + if (! $this->app->runningInConsole()) { + return; + } + + $this->commands([ Commands\CreateRole::class, Commands\CreatePermission::class, Commands\Show::class, @@ -79,7 +86,7 @@ protected function registerCommands() ]); } - protected function registerModelBindings() + protected function registerModelBindings(): void { $this->app->bind(PermissionContract::class, fn ($app) => $app->make($app->config['permission.models.permission']) ); @@ -87,12 +94,12 @@ protected function registerModelBindings() ); } - public static function bladeMethodWrapper($method, $role, $guard = null) + public static function bladeMethodWrapper($method, $role, $guard = null): bool { return auth($guard)->check() && auth($guard)->user()->{$method}($role); } - protected function registerBladeExtensions($bladeCompiler) + protected function registerBladeExtensions($bladeCompiler): void { $bladeMethodWrapper = '\\Spatie\\Permission\\PermissionServiceProvider::bladeMethodWrapper'; @@ -116,7 +123,7 @@ protected function registerBladeExtensions($bladeCompiler) $bladeCompiler->directive('endhasexactroles', fn () => ''); } - protected function registerMacroHelpers() + protected function registerMacroHelpers(): void { if (! method_exists(Route::class, 'macro')) { // Lumen return; From 0a4e6bfc7d90cd2f713366043dd284da92efb539 Mon Sep 17 00:00:00 2001 From: angeljqv <79208641+angeljqv@users.noreply.github.com> Date: Fri, 14 Apr 2023 09:41:16 -0500 Subject: [PATCH 0679/1013] code reformatting --- src/PermissionServiceProvider.php | 6 ++--- src/Traits/HasPermissions.php | 43 ++++++++++++++++--------------- src/Traits/HasRoles.php | 18 ++++++------- src/helpers.php | 4 +-- 4 files changed, 35 insertions(+), 36 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 16e8f3287..262309430 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -81,10 +81,8 @@ protected function registerCommands() protected function registerModelBindings() { - $this->app->bind(PermissionContract::class, fn ($app) => $app->make($app->config['permission.models.permission']) - ); - $this->app->bind(RoleContract::class, fn ($app) => $app->make($app->config['permission.models.role']) - ); + $this->app->bind(PermissionContract::class, fn ($app) => $app->make($app->config['permission.models.permission'])); + $this->app->bind(RoleContract::class, fn ($app) => $app->make($app->config['permission.models.role'])); } public static function bladeMethodWrapper($method, $role, $guard = null) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index fcd461fe1..cf07d0cdf 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -99,22 +99,23 @@ public function scopePermission(Builder $query, $permissions): Builder { $permissions = $this->convertToPermissionModels($permissions); + $permissionClass = $this->getPermissionClass(); + $permissionKey = (new $permissionClass())->getKeyName(); + $roleClass = $this->getRoleClass(); + $roleKey = (new $roleClass())->getKeyName(); + $rolesWithPermissions = is_a($this, Role::class) ? [] : array_unique( array_reduce($permissions, fn ($result, $permission) => array_merge($result, $permission->roles->all()), []) ); return $query->where(fn (Builder $query) => $query - ->whereHas('permissions', function (Builder $subQuery) use ($permissions) { - $permissionClass = $this->getPermissionClass(); - $key = (new $permissionClass())->getKeyName(); - $subQuery->whereIn(config('permission.table_names.permissions').".$key", \array_column($permissions, $key)); - }) - ->when(count($rolesWithPermissions) > 0, fn ($subQuery) => $subQuery - ->orWhereHas('roles', function (Builder $subQuery) use ($rolesWithPermissions) { - $roleClass = $this->getRoleClass(); - $key = (new $roleClass())->getKeyName(); - $subQuery->whereIn(config('permission.table_names.roles').".$key", \array_column($rolesWithPermissions, $key)); - }) + ->whereHas('permissions', fn (Builder $subQuery) => $subQuery + ->whereIn(config('permission.table_names.permissions').".$permissionKey", \array_column($permissions, $permissionKey)) + ) + ->when(count($rolesWithPermissions), fn ($whenQuery) => $whenQuery + ->orWhereHas('roles', fn (Builder $subQuery) => $subQuery + ->whereIn(config('permission.table_names.roles').".$roleKey", \array_column($rolesWithPermissions, $roleKey)) + ) ) ); } @@ -139,7 +140,7 @@ protected function convertToPermissionModels($permissions): array $permission = $permission->value; } - $method = is_string($permission) && ! PermissionRegistrar::isUid($permission) ? 'findByName' : 'findById'; + $method = is_int($permission) || PermissionRegistrar::isUid($permission) ? 'findById' : 'findByName'; return $this->getPermissionClass()::{$method}($permission, $this->getDefaultGuardName()); }, Arr::wrap($permissions)); @@ -159,15 +160,15 @@ public function filterPermission($permission, $guardName = null) $permission = $permission->value; } - if (is_string($permission) && ! PermissionRegistrar::isUid($permission)) { - $permission = $this->getPermissionClass()::findByName( + if (is_int($permission) || PermissionRegistrar::isUid($permission)) { + $permission = $this->getPermissionClass()::findById( $permission, $guardName ?? $this->getDefaultGuardName() ); } - if (is_int($permission) || is_string($permission)) { - $permission = $this->getPermissionClass()::findById( + if (is_string($permission)) { + $permission = $this->getPermissionClass()::findByName( $permission, $guardName ?? $this->getDefaultGuardName() ); @@ -209,6 +210,10 @@ protected function hasWildcardPermission($permission, $guardName = null): bool { $guardName = $guardName ?? $this->getDefaultGuardName(); + if ($permission instanceof \BackedEnum) { + $permission = $permission->value; + } + if (is_int($permission) || PermissionRegistrar::isUid($permission)) { $permission = $this->getPermissionClass()::findById($permission, $guardName); } @@ -217,10 +222,6 @@ protected function hasWildcardPermission($permission, $guardName = null): bool $permission = $permission->name; } - if ($permission instanceof \BackedEnum) { - $permission = $permission->value; - } - if (! is_string($permission)) { throw WildcardPermissionInvalidArgument::create(); } @@ -461,7 +462,7 @@ protected function getStoredPermission($permissions) $permissions = $permissions->value; } - if (is_numeric($permissions) || PermissionRegistrar::isUid($permissions)) { + if (is_int($permissions) || PermissionRegistrar::isUid($permissions)) { return $this->getPermissionClass()::findById($permissions, $this->getDefaultGuardName()); } diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 9a4d9187e..f99697235 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -82,7 +82,7 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder return $role; } - $method = is_numeric($role) || PermissionRegistrar::isUid($role) ? 'findById' : 'findByName'; + $method = is_int($role) || PermissionRegistrar::isUid($role) ? 'findById' : 'findByName'; return $this->getRoleClass()::{$method}($role, $guard ?: $this->getDefaultGuardName()); }, Arr::wrap($roles)); @@ -209,13 +209,7 @@ public function hasRole($roles, string $guard = null): bool $roles = $roles->value; } - if (is_string($roles) && ! PermissionRegistrar::isUid($roles)) { - return $guard - ? $this->roles->where('guard_name', $guard)->contains('name', $roles) - : $this->roles->contains('name', $roles); - } - - if (is_int($roles) || is_string($roles)) { + if (is_int($roles) || PermissionRegistrar::isUid($roles)) { $roleClass = $this->getRoleClass(); $key = (new $roleClass())->getKeyName(); @@ -224,6 +218,12 @@ public function hasRole($roles, string $guard = null): bool : $this->roles->contains($key, $roles); } + if (is_string($roles)) { + return $guard + ? $this->roles->where('guard_name', $guard)->contains('name', $roles) + : $this->roles->contains('name', $roles); + } + if ($roles instanceof Role) { return $this->roles->contains($roles->getKeyName(), $roles->getKey()); } @@ -347,7 +347,7 @@ protected function getStoredRole($role): Role $role = $role->value; } - if (is_numeric($role) || PermissionRegistrar::isUid($role)) { + if (is_int($role) || PermissionRegistrar::isUid($role)) { return $this->getRoleClass()::findById($role, $this->getDefaultGuardName()); } diff --git a/src/helpers.php b/src/helpers.php index 0f765fa4e..d1aae1ee2 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -7,8 +7,8 @@ function getModelForGuard(string $guard) { return collect(config('auth.guards')) - ->map(fn ($guard) => isset($guard['provider']) ? config("auth.providers.{$guard['provider']}.model") : null - )->get($guard); + ->map(fn ($guard) => isset($guard['provider']) ? config("auth.providers.{$guard['provider']}.model") : null) + ->get($guard); } } From 07af475984d644d66761a77fc234d6f349f8266b Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 18 Apr 2023 12:02:09 -0400 Subject: [PATCH 0680/1013] Update README.md minor rearrangements --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9850ee4ee..25ff759bd 100644 --- a/README.md +++ b/README.md @@ -49,13 +49,7 @@ We invest a lot of resources into creating [best in class open source packages]( We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). -### Testing - -``` bash -composer test -``` - -### Changelog +## Changelog Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. @@ -63,6 +57,12 @@ Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recen Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. +### Testing + +``` bash +composer test +``` + ### Security If you discover any security-related issues, please email [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. @@ -92,8 +92,8 @@ And a special thanks to [Caneco](https://twitter.com/caneco) for the logo ✨ ## Alternatives - [Povilas Korop](https://twitter.com/@povilaskorop) did an excellent job listing the alternatives [in an article on Laravel News](https://laravel-news.com/two-best-roles-permissions-packages). In that same article, he compares laravel-permission to [Joseph Silber](https://github.com/JosephSilber)'s [Bouncer]((https://github.com/JosephSilber/bouncer)), which in our book is also an excellent package. -- [ultraware/roles](https://github.com/ultraware/roles) takes a slightly different approach to its features. - [santigarcor/laratrust](https://github.com/santigarcor/laratrust) implements team support +- [ultraware/roles](https://github.com/ultraware/roles) (archived) takes a slightly different approach to its features. - [zizaco/entrust](https://github.com/zizaco/entrust) offers some wildcard pattern matching ## License From 32ff0612863a347fce1dfd88e6d66e995728fa18 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 06:01:17 +0000 Subject: [PATCH 0681/1013] Bump dependabot/fetch-metadata from 1.3.6 to 1.4.0 Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 1.3.6 to 1.4.0. - [Release notes](https://github.com/dependabot/fetch-metadata/releases) - [Commits](https://github.com/dependabot/fetch-metadata/compare/v1.3.6...v1.4.0) --- updated-dependencies: - dependency-name: dependabot/fetch-metadata dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/dependabot-auto-merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index f2e85e7d4..4c8e4c314 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -13,7 +13,7 @@ jobs: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v1.3.6 + uses: dependabot/fetch-metadata@v1.4.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" compat-lookup: true From d595dfff2b4a580872c969895964903a2a11f109 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Wed, 26 Apr 2023 10:55:19 -0500 Subject: [PATCH 0682/1013] Fix call to an undefined method Role::getRoleClass --- phpstan-baseline.neon | 5 ----- src/Traits/HasPermissions.php | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a1b0ec7db..364905f71 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,7 +1,2 @@ parameters: ignoreErrors: - - - # PHPStan can't understand what's going on in context of Role class using Builder `when` - message: "#^Call to an undefined method Spatie\\\\Permission\\\\Models\\\\Role::getRoleClass\\(\\).$#" - count: 1 - path: src\Traits\HasPermissions.php diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index cf07d0cdf..c0b1a1b6f 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -101,7 +101,7 @@ public function scopePermission(Builder $query, $permissions): Builder $permissionClass = $this->getPermissionClass(); $permissionKey = (new $permissionClass())->getKeyName(); - $roleClass = $this->getRoleClass(); + $roleClass = is_a($this, Role::class) ? static::class : $this->getRoleClass(); $roleKey = (new $roleClass())->getKeyName(); $rolesWithPermissions = is_a($this, Role::class) ? [] : array_unique( From ec9079aadfe20051a9ccf0a15b6c9ed453b7bca6 Mon Sep 17 00:00:00 2001 From: drbyte Date: Wed, 26 Apr 2023 16:39:50 +0000 Subject: [PATCH 0683/1013] Fix styling --- src/Commands/Show.php | 6 +++--- src/Models/Role.php | 2 +- tests/HasPermissionsTest.php | 6 +++--- tests/RouteTest.php | 14 +++++++------- tests/WildcardRouteTest.php | 10 +++++----- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Commands/Show.php b/src/Commands/Show.php index d4f39b003..0c08bd28a 100644 --- a/src/Commands/Show.php +++ b/src/Commands/Show.php @@ -44,8 +44,8 @@ public function handle() $permissions = $permissionClass::whereGuardName($guard)->orderBy('name')->pluck('name', 'id'); $body = $permissions->map(fn ($permission, $id) => $roles->map( - fn (array $role_data) => $role_data['permissions']->contains($id) ? ' ✔' : ' ·' - )->prepend($permission) + fn (array $role_data) => $role_data['permissions']->contains($id) ? ' ✔' : ' ·' + )->prepend($permission) ); if (config('permission.teams')) { @@ -63,7 +63,7 @@ public function handle() return implode('_', $name); }) - ->prepend('')->toArray(), + ->prepend('')->toArray(), ), $body->toArray(), $style diff --git a/src/Models/Role.php b/src/Models/Role.php index 472a93847..7199504d1 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -151,7 +151,7 @@ protected static function findByParam(array $params = []) $teamsKey = app(PermissionRegistrar::class)->teamsKey; $query->where(fn ($q) => $q->whereNull($teamsKey) - ->orWhere($teamsKey, $params[$teamsKey] ?? getPermissionsTeamId()) + ->orWhere($teamsKey, $params[$teamsKey] ?? getPermissionsTeamId()) ); unset($params[$teamsKey]); } diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index 5871ed46c..fdc4c94e5 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -653,7 +653,7 @@ public function it_can_check_permission_based_on_logged_in_user_guard() 'guard_name' => 'api', ])); $response = $this->actingAs($this->testUser, 'api') - ->json('GET', '/check-api-guard-permission'); + ->json('GET', '/check-api-guard-permission'); $response->assertJson([ 'status' => true, ]); @@ -674,8 +674,8 @@ public function it_can_reject_permission_based_on_logged_in_user_guard() $this->testUser->givePermissionTo($assignedPermission); $response = $this->withExceptionHandling() - ->actingAs($this->testUser, 'api') - ->json('GET', '/check-api-guard-permission'); + ->actingAs($this->testUser, 'api') + ->json('GET', '/check-api-guard-permission'); $response->assertJson([ 'status' => false, ]); diff --git a/tests/RouteTest.php b/tests/RouteTest.php index db5a6cf6b..e4843f0cb 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -10,8 +10,8 @@ public function test_role_function() $router = $this->getRouter(); $router->get('role-test', $this->getRouteResponse()) - ->name('role.test') - ->role('superadmin'); + ->name('role.test') + ->role('superadmin'); $this->assertEquals(['role:superadmin'], $this->getLastRouteMiddlewareFromRouter($router)); } @@ -22,8 +22,8 @@ public function test_permission_function() $router = $this->getRouter(); $router->get('permission-test', $this->getRouteResponse()) - ->name('permission.test') - ->permission(['edit articles', 'save articles']); + ->name('permission.test') + ->permission(['edit articles', 'save articles']); $this->assertEquals(['permission:edit articles|save articles'], $this->getLastRouteMiddlewareFromRouter($router)); } @@ -34,9 +34,9 @@ public function test_role_and_permission_function_together() $router = $this->getRouter(); $router->get('role-permission-test', $this->getRouteResponse()) - ->name('role-permission.test') - ->role('superadmin|admin') - ->permission('create user|edit user'); + ->name('role-permission.test') + ->role('superadmin|admin') + ->permission('create user|edit user'); $this->assertEquals( [ diff --git a/tests/WildcardRouteTest.php b/tests/WildcardRouteTest.php index 7292a6cb2..f56dfcb6c 100644 --- a/tests/WildcardRouteTest.php +++ b/tests/WildcardRouteTest.php @@ -12,8 +12,8 @@ public function test_permission_function() $router = $this->getRouter(); $router->get('permission-test', $this->getRouteResponse()) - ->name('permission.test') - ->permission(['articles.edit', 'articles.save']); + ->name('permission.test') + ->permission(['articles.edit', 'articles.save']); $this->assertEquals(['permission:articles.edit|articles.save'], $this->getLastRouteMiddlewareFromRouter($router)); } @@ -26,9 +26,9 @@ public function test_role_and_permission_function_together() $router = $this->getRouter(); $router->get('role-permission-test', $this->getRouteResponse()) - ->name('role-permission.test') - ->role('superadmin|admin') - ->permission('user.create|user.edit'); + ->name('role-permission.test') + ->role('superadmin|admin') + ->permission('user.create|user.edit'); $this->assertEquals( [ From 82159260ab2c38aeac34b4341d7f779aac9a1cf6 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Wed, 26 Apr 2023 15:42:11 -0500 Subject: [PATCH 0684/1013] Add database cache reset count to tests (#2415) --- tests/HasPermissionsWithCustomModelsTest.php | 18 +++++++++++++++--- tests/HasRolesWithCustomModelsTest.php | 18 +++++++++++++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/tests/HasPermissionsWithCustomModelsTest.php b/tests/HasPermissionsWithCustomModelsTest.php index 90b6abc4d..7f5ccde75 100644 --- a/tests/HasPermissionsWithCustomModelsTest.php +++ b/tests/HasPermissionsWithCustomModelsTest.php @@ -12,6 +12,18 @@ class HasPermissionsWithCustomModelsTest extends HasPermissionsTest /** @var bool */ protected $useCustomModels = true; + /** @var int */ + protected $resetDatabaseQuery = 0; + + protected function getEnvironmentSetUp($app) + { + parent::getEnvironmentSetUp($app); + + if ($app['config']->get('cache.default') == 'database') { + $this->resetDatabaseQuery = 1; + } + } + /** @test */ public function it_can_use_custom_model_permission() { @@ -75,7 +87,7 @@ public function it_doesnt_detach_roles_when_soft_deleting() $this->testUserPermission->delete(); DB::disableQueryLog(); - $this->assertSame(1, count(DB::getQueryLog())); + $this->assertSame(1 + $this->resetDatabaseQuery, count(DB::getQueryLog())); $permission = Permission::onlyTrashed()->find($this->testUserPermission->getKey()); @@ -91,7 +103,7 @@ public function it_doesnt_detach_users_when_soft_deleting() $this->testUserPermission->delete(); DB::disableQueryLog(); - $this->assertSame(1, count(DB::getQueryLog())); + $this->assertSame(1 + $this->resetDatabaseQuery, count(DB::getQueryLog())); $permission = Permission::onlyTrashed()->find($this->testUserPermission->getKey()); @@ -109,7 +121,7 @@ public function it_does_detach_roles_and_users_when_force_deleting() $this->testUserPermission->forceDelete(); DB::disableQueryLog(); - $this->assertSame(3, count(DB::getQueryLog())); //avoid detach permissions on permissions + $this->assertSame(3 + $this->resetDatabaseQuery, count(DB::getQueryLog())); //avoid detach permissions on permissions $permission = Permission::withTrashed()->find($permission_id); diff --git a/tests/HasRolesWithCustomModelsTest.php b/tests/HasRolesWithCustomModelsTest.php index bb3ddce32..659f04db7 100644 --- a/tests/HasRolesWithCustomModelsTest.php +++ b/tests/HasRolesWithCustomModelsTest.php @@ -10,6 +10,18 @@ class HasRolesWithCustomModelsTest extends HasRolesTest /** @var bool */ protected $useCustomModels = true; + /** @var int */ + protected $resetDatabaseQuery = 0; + + protected function getEnvironmentSetUp($app) + { + parent::getEnvironmentSetUp($app); + + if ($app['config']->get('cache.default') == 'database') { + $this->resetDatabaseQuery = 1; + } + } + /** @test */ public function it_can_use_custom_model_role() { @@ -25,7 +37,7 @@ public function it_doesnt_detach_permissions_when_soft_deleting() $this->testUserRole->delete(); DB::disableQueryLog(); - $this->assertSame(1, count(DB::getQueryLog())); + $this->assertSame(1 + $this->resetDatabaseQuery, count(DB::getQueryLog())); $role = Role::onlyTrashed()->find($this->testUserRole->getKey()); @@ -41,7 +53,7 @@ public function it_doesnt_detach_users_when_soft_deleting() $this->testUserRole->delete(); DB::disableQueryLog(); - $this->assertSame(1, count(DB::getQueryLog())); + $this->assertSame(1 + $this->resetDatabaseQuery, count(DB::getQueryLog())); $role = Role::onlyTrashed()->find($this->testUserRole->getKey()); @@ -59,7 +71,7 @@ public function it_does_detach_permissions_and_users_when_force_deleting() $this->testUserRole->forceDelete(); DB::disableQueryLog(); - $this->assertSame(3, count(DB::getQueryLog())); + $this->assertSame(3 + $this->resetDatabaseQuery, count(DB::getQueryLog())); $role = Role::withTrashed()->find($role_id); From f9446b85263246907c76a810944c8aa8668b98ee Mon Sep 17 00:00:00 2001 From: erikn69 Date: Wed, 26 Apr 2023 15:43:26 -0500 Subject: [PATCH 0685/1013] Remove force loading model relationships (#2412) --- src/Traits/HasPermissions.php | 6 +++--- src/Traits/HasRoles.php | 6 +++--- tests/CacheTest.php | 11 ++++++----- tests/HasPermissionsTest.php | 4 ++-- tests/HasRolesTest.php | 4 ++-- tests/TeamHasPermissionsTest.php | 12 ++---------- tests/TeamHasRolesTest.php | 4 ---- 7 files changed, 18 insertions(+), 29 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index c0b1a1b6f..174c254fe 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -391,7 +391,7 @@ public function givePermissionTo(...$permissions) if ($model->exists) { $this->permissions()->sync($permissions, false); - $model->load('permissions'); + $model->unsetRelation('permissions'); } else { $class = \get_class($model); @@ -401,7 +401,7 @@ function ($object) use ($permissions, $model) { return; } $model->permissions()->sync($permissions, false); - $model->load('permissions'); + $model->unsetRelation('permissions'); } ); } @@ -442,7 +442,7 @@ public function revokePermissionTo($permission) $this->forgetCachedPermissions(); } - $this->load('permissions'); + $this->unsetRelation('permissions'); return $this; } diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index f99697235..419f5189a 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -137,7 +137,7 @@ public function assignRole(...$roles) if ($model->exists) { $this->roles()->sync($roles, false); - $model->load('roles'); + $model->unsetRelation('roles'); } else { $class = \get_class($model); @@ -147,7 +147,7 @@ function ($object) use ($roles, $model) { return; } $model->roles()->sync($roles, false); - $model->load('roles'); + $model->unsetRelation('roles'); } ); } @@ -168,7 +168,7 @@ public function removeRole($role) { $this->roles()->detach($this->getStoredRole($role)); - $this->load('roles'); + $this->unsetRelation('roles'); if (is_a($this, Permission::class)) { $this->forgetCachedPermissions(); diff --git a/tests/CacheTest.php b/tests/CacheTest.php index 2205e298c..ba8ca6a1e 100644 --- a/tests/CacheTest.php +++ b/tests/CacheTest.php @@ -18,8 +18,6 @@ class CacheTest extends TestCase protected $cache_run_count = 2; // roles lookup, permissions lookup - protected $cache_relations_count = 1; - protected $registrar; protected function setUp(): void @@ -199,10 +197,11 @@ public function has_permission_to_should_use_the_cache() { $this->testUserRole->givePermissionTo(['edit-articles', 'edit-news', 'Edit News']); $this->testUser->assignRole('testRole'); + $this->testUser->loadMissing('roles', 'permissions'); // load relations $this->resetQueryCount(); $this->assertTrue($this->testUser->hasPermissionTo('edit-articles')); - $this->assertQueryCount($this->cache_init_count + $this->cache_load_count + $this->cache_run_count + $this->cache_relations_count); + $this->assertQueryCount($this->cache_init_count + $this->cache_load_count + $this->cache_run_count); $this->resetQueryCount(); $this->assertTrue($this->testUser->hasPermissionTo('edit-news')); @@ -224,10 +223,11 @@ public function the_cache_should_differentiate_by_guard_name() $this->testUserRole->givePermissionTo(['edit-articles', 'web']); $this->testUser->assignRole('testRole'); + $this->testUser->loadMissing('roles', 'permissions'); // load relations $this->resetQueryCount(); $this->assertTrue($this->testUser->hasPermissionTo('edit-articles', 'web')); - $this->assertQueryCount($this->cache_init_count + $this->cache_load_count + $this->cache_run_count + $this->cache_relations_count); + $this->assertQueryCount($this->cache_init_count + $this->cache_load_count + $this->cache_run_count); $this->resetQueryCount(); $this->assertFalse($this->testUser->hasPermissionTo('edit-articles', 'admin')); @@ -239,6 +239,7 @@ public function get_all_permissions_should_use_the_cache() { $this->testUserRole->givePermissionTo($expected = ['edit-articles', 'edit-news']); $this->testUser->assignRole('testRole'); + $this->testUser->loadMissing('roles.permissions', 'permissions'); // load relations $this->resetQueryCount(); $this->registrar->getPermissions(); @@ -248,7 +249,7 @@ public function get_all_permissions_should_use_the_cache() $actual = $this->testUser->getAllPermissions()->pluck('name')->sort()->values(); $this->assertEquals($actual, collect($expected)); - $this->assertQueryCount(2); + $this->assertQueryCount(0); } /** @test */ diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index fdc4c94e5..c876294f9 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -591,7 +591,7 @@ public function calling_givePermissionTo_before_saving_object_doesnt_interfere_w $this->assertTrue($user2->fresh()->hasPermissionTo('edit-articles')); $this->assertFalse($user2->fresh()->hasPermissionTo('edit-news')); - $this->assertSame(4, count(DB::getQueryLog())); //avoid unnecessary sync + $this->assertSame(3, count(DB::getQueryLog())); //avoid unnecessary sync } /** @test */ @@ -613,7 +613,7 @@ public function calling_syncPermissions_before_saving_object_doesnt_interfere_wi $this->assertTrue($user2->fresh()->hasPermissionTo('edit-articles')); $this->assertFalse($user2->fresh()->hasPermissionTo('edit-news')); - $this->assertSame(4, count(DB::getQueryLog())); //avoid unnecessary sync + $this->assertSame(3, count(DB::getQueryLog())); //avoid unnecessary sync } /** @test */ diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 2d0c63ecc..b984aeb4d 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -303,7 +303,7 @@ public function calling_syncRoles_before_saving_object_doesnt_interfere_with_oth $this->assertTrue($user2->fresh()->hasRole('testRole2')); $this->assertFalse($user2->fresh()->hasRole('testRole')); - $this->assertSame(4, count(DB::getQueryLog())); //avoid unnecessary sync + $this->assertSame(3, count(DB::getQueryLog())); //avoid unnecessary sync } /** @test */ @@ -325,7 +325,7 @@ public function calling_assignRole_before_saving_object_doesnt_interfere_with_ot $this->assertTrue($admin_user->fresh()->hasRole('testRole2')); $this->assertFalse($admin_user->fresh()->hasRole('testRole')); - $this->assertSame(4, count(DB::getQueryLog())); //avoid unnecessary sync + $this->assertSame(3, count(DB::getQueryLog())); //avoid unnecessary sync } /** @test */ diff --git a/tests/TeamHasPermissionsTest.php b/tests/TeamHasPermissionsTest.php index 54fe106f0..5c9826b15 100644 --- a/tests/TeamHasPermissionsTest.php +++ b/tests/TeamHasPermissionsTest.php @@ -13,11 +13,9 @@ class TeamHasPermissionsTest extends HasPermissionsTest public function it_can_assign_same_and_different_permission_on_same_user_on_different_teams() { setPermissionsTeamId(1); - $this->testUser->load('permissions'); $this->testUser->givePermissionTo('edit-articles', 'edit-news'); setPermissionsTeamId(2); - $this->testUser->load('permissions'); $this->testUser->givePermissionTo('edit-articles', 'edit-blog'); setPermissionsTeamId(1); @@ -45,18 +43,15 @@ public function it_can_list_all_the_coupled_permissions_both_directly_and_via_ro $this->testUserRole->givePermissionTo('edit-articles'); setPermissionsTeamId(1); - $this->testUser->load('permissions'); $this->testUser->assignRole('testRole'); $this->testUser->givePermissionTo('edit-news'); setPermissionsTeamId(2); - $this->testUser->load('permissions'); $this->testUser->assignRole('testRole'); $this->testUser->givePermissionTo('edit-blog'); setPermissionsTeamId(1); - $this->testUser->load('roles'); - $this->testUser->load('permissions'); + $this->testUser->load('roles', 'permissions'); $this->assertEquals( collect(['edit-articles', 'edit-news']), @@ -64,8 +59,7 @@ public function it_can_list_all_the_coupled_permissions_both_directly_and_via_ro ); setPermissionsTeamId(2); - $this->testUser->load('roles'); - $this->testUser->load('permissions'); + $this->testUser->load('roles', 'permissions'); $this->assertEquals( collect(['edit-articles', 'edit-blog']), @@ -77,11 +71,9 @@ public function it_can_list_all_the_coupled_permissions_both_directly_and_via_ro public function it_can_sync_or_remove_permission_without_detach_on_different_teams() { setPermissionsTeamId(1); - $this->testUser->load('permissions'); $this->testUser->syncPermissions('edit-articles', 'edit-news'); setPermissionsTeamId(2); - $this->testUser->load('permissions'); $this->testUser->syncPermissions('edit-articles', 'edit-blog'); setPermissionsTeamId(1); diff --git a/tests/TeamHasRolesTest.php b/tests/TeamHasRolesTest.php index ca40292ea..e67010009 100644 --- a/tests/TeamHasRolesTest.php +++ b/tests/TeamHasRolesTest.php @@ -50,11 +50,9 @@ public function it_can_assign_same_and_different_roles_on_same_user_different_te $this->assertNotNull($testRole4NoTeam); setPermissionsTeamId(1); - $this->testUser->load('roles'); $this->testUser->assignRole('testRole', 'testRole2'); setPermissionsTeamId(2); - $this->testUser->load('roles'); $this->testUser->assignRole('testRole', 'testRole3'); setPermissionsTeamId(1); @@ -91,11 +89,9 @@ public function it_can_sync_or_remove_roles_without_detach_on_different_teams() app(Role::class)->create(['name' => 'testRole3', 'team_test_id' => 2]); setPermissionsTeamId(1); - $this->testUser->load('roles'); $this->testUser->syncRoles('testRole', 'testRole2'); setPermissionsTeamId(2); - $this->testUser->load('roles'); $this->testUser->syncRoles('testRole', 'testRole3'); setPermissionsTeamId(1); From 9f18371600277317260c14d296e01732710bc0b2 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Thu, 27 Apr 2023 12:33:47 -0500 Subject: [PATCH 0686/1013] Alternate cache driver on tests (#2416) --- .github/workflows/test-cache-drivers.yml | 67 ++++++++++++++++++++++++ tests/TestCase.php | 1 + 2 files changed, 68 insertions(+) create mode 100644 .github/workflows/test-cache-drivers.yml diff --git a/.github/workflows/test-cache-drivers.yml b/.github/workflows/test-cache-drivers.yml new file mode 100644 index 000000000..ab49f2cf2 --- /dev/null +++ b/.github/workflows/test-cache-drivers.yml @@ -0,0 +1,67 @@ +name: "Run Tests - Cache Drivers" + +on: [push, pull_request] + +jobs: + cache: + + runs-on: ubuntu-latest + + services: + redis: + image: redis + ports: + - 6379/tcp + options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3 + + strategy: + fail-fast: false + + name: Cache Drivers + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv, memcache + coverage: none + + - name: Install dependencies + run: | + composer require "predis/predis" --no-interaction --no-update + composer update --prefer-stable --prefer-dist --no-interaction + + - name: Execute tests - memcached cache driver + run: | + vendor/bin/phpunit + env: + CACHE_DRIVER: memcached + + - name: Execute tests - redis cache driver + run: | + vendor/bin/phpunit + env: + CACHE_DRIVER: redis + REDIS_PORT: ${{ job.services.redis.ports['6379'] }} + + - name: Execute tests - database cache driver + run: | + vendor/bin/phpunit + env: + CACHE_DRIVER: database + + - name: Execute tests - file cache driver + run: | + vendor/bin/phpunit + env: + CACHE_DRIVER: file + + - name: Execute tests - array cache driver + run: | + vendor/bin/phpunit + env: + CACHE_DRIVER: array diff --git a/tests/TestCase.php b/tests/TestCase.php index b45718f0c..456964ec1 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -111,6 +111,7 @@ protected function getEnvironmentSetUp($app) $app['config']->set('auth.providers.users.model', User::class); $app['config']->set('cache.prefix', 'spatie_tests---'); + $app['config']->set('cache.default', getenv('CACHE_DRIVER') ?: 'array'); // FOR MANUAL TESTING OF ALTERNATE CACHE STORES: // $app['config']->set('cache.default', 'array'); From 2bc6a501d1a0ed448e78a02d5104edefaa97c092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hasan=20Ek=C5=9Fi?= Date: Fri, 28 Apr 2023 12:49:40 +0300 Subject: [PATCH 0687/1013] Fix command name (#2418) --- docs/basic-usage/teams-permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/teams-permissions.md b/docs/basic-usage/teams-permissions.md index 3b2c49528..a97136e8c 100644 --- a/docs/basic-usage/teams-permissions.md +++ b/docs/basic-usage/teams-permissions.md @@ -9,7 +9,7 @@ When enabled, teams permissions offers you flexible control for a variety of sce NOTE: These configuration changes must be made **before** performing the migration when first installing the package. -If you have already run the migration and want to upgrade your implementation, you can run the artisan console command `php artisan spatie-permission:setup-teams`, to create a new migration file named [xxxx_xx_xx_xx_add_teams_fields.php](https://github.com/spatie/laravel-permission/blob/main/database/migrations/add_teams_fields.php.stub) and then run `php artisan migrate` to upgrade your database tables. +If you have already run the migration and want to upgrade your implementation, you can run the artisan console command `php artisan permission:setup-teams`, to create a new migration file named [xxxx_xx_xx_xx_add_teams_fields.php](https://github.com/spatie/laravel-permission/blob/main/database/migrations/add_teams_fields.php.stub) and then run `php artisan migrate` to upgrade your database tables. Teams permissions can be enabled in the permission config file: From 61cacc007c64aa3364709568c8f9e8c332db15cf Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 28 Apr 2023 18:04:56 -0400 Subject: [PATCH 0688/1013] [Docs] Update v6 upgrade instructions --- docs/upgrading.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index 837937440..7a9e463d1 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -30,11 +30,17 @@ Be sure to compare your custom models with originals to see what else may have c 2. If you have a custom Role model and (in the rare case that you might) have overridden the `hasPermissionTo()` method in it, you will need to update its method signature to `hasPermissionTo($permission, $guardName = null):bool`. See PR #2380. -3. Also note that the migrations have been updated to anonymous-class syntax that was introduced in Laravel 8. You may optionally update your original migration files accordingly. +3. Migrations. If you have old migrations you might get the following error: + + Error: Access to undeclared static property Spatie\Permission\PermissionRegistrar::$pivotPermission + +To fix this, update your migration file associated with this package. + +Also note that the migrations have been updated to anonymous-class syntax that was introduced in Laravel 8. 4. NOTE: For consistency with the `PermissionMiddleware`, the `RoleOrPermissionMiddleware` has switched from only checking permissions provided by this package to using `canAny()` to check against any abilities registered by your application. This may have the effect of granting those other abilities (such as Super Admin) when using the `RoleOrPermissionMiddleware`, which previously would have failed silently. -5. Test suites. If you have tests which manually clear the permission cache and re-register permissions, you no longer need to call `\Spatie\Permission\PermissionRegistrar::class)->registerPermissions();`. Such calls can be deleted from your tests. +5. Test suites. If you have tests which manually clear the permission cache and re-register permissions, you no longer need to call `\Spatie\Permission\PermissionRegistrar::class)->registerPermissions();`. Such calls MUST be deleted from your tests. ## Upgrading from v1 to v2 From 88cf4f98af7a93b010269249fea847a2e8ad9e85 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 28 Apr 2023 18:32:33 -0400 Subject: [PATCH 0689/1013] [Docs] note: requires v6 --- docs/basic-usage/enums.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/basic-usage/enums.md b/docs/basic-usage/enums.md index dffce53f9..2de63642b 100644 --- a/docs/basic-usage/enums.md +++ b/docs/basic-usage/enums.md @@ -5,6 +5,8 @@ weight: 4 ## Enum Prerequisites +Requires `version 6` of this package. + Requires PHP 8.1 or higher. If you are using PHP 8.1+ you can implement Enums as native types. From 1f04549fe7153a62ceb60d1cca49bdb36a5dcaba Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 28 Apr 2023 19:16:41 -0400 Subject: [PATCH 0690/1013] [Docs] Update v6 upgrade instructions --- docs/upgrading.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index 7a9e463d1..2b3c892e3 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -32,11 +32,11 @@ Be sure to compare your custom models with originals to see what else may have c 3. Migrations. If you have old migrations you might get the following error: - Error: Access to undeclared static property Spatie\Permission\PermissionRegistrar::$pivotPermission + `Error: Access to undeclared static property Spatie\Permission\PermissionRegistrar::$pivotPermission` -To fix this, update your migration file associated with this package. + To fix this, update your migration file associated with this package. -Also note that the migrations have been updated to anonymous-class syntax that was introduced in Laravel 8. + Also note that the migrations have been updated to anonymous-class syntax that was introduced in Laravel 8. 4. NOTE: For consistency with the `PermissionMiddleware`, the `RoleOrPermissionMiddleware` has switched from only checking permissions provided by this package to using `canAny()` to check against any abilities registered by your application. This may have the effect of granting those other abilities (such as Super Admin) when using the `RoleOrPermissionMiddleware`, which previously would have failed silently. From 47867f02e10bbb201012bab47f69858e7794e140 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 28 Apr 2023 21:21:06 -0400 Subject: [PATCH 0691/1013] [Docs] Update v6 upgrade instructions --- docs/upgrading.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index 2b3c892e3..9642d8326 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -32,18 +32,33 @@ Be sure to compare your custom models with originals to see what else may have c 3. Migrations. If you have old migrations you might get the following error: - `Error: Access to undeclared static property Spatie\Permission\PermissionRegistrar::$pivotPermission` + `Error: Access to undeclared static property Spatie\Permission\PermissionRegistrar::$pivotPermission` To fix this, update your migration file associated with this package. Also note that the migrations have been updated to anonymous-class syntax that was introduced in Laravel 8. -4. NOTE: For consistency with the `PermissionMiddleware`, the `RoleOrPermissionMiddleware` has switched from only checking permissions provided by this package to using `canAny()` to check against any abilities registered by your application. This may have the effect of granting those other abilities (such as Super Admin) when using the `RoleOrPermissionMiddleware`, which previously would have failed silently. +4. NOTE: For consistency with `PermissionMiddleware`, the `RoleOrPermissionMiddleware` has switched from only checking permissions provided by this package to using `canAny()` to check against any abilities registered by your application. This may have the effect of granting those other abilities (such as Super Admin) when using the `RoleOrPermissionMiddleware`, which previously would have failed silently. 5. Test suites. If you have tests which manually clear the permission cache and re-register permissions, you no longer need to call `\Spatie\Permission\PermissionRegistrar::class)->registerPermissions();`. Such calls MUST be deleted from your tests. +## Upgrading from v4 to v5 + +Follow the instructions described in "Essentials" above. + +## Upgrading from v3 to v4 + +Update `composer.json` as described in "Essentials" above. + +## Upgrading from v2 to v3 + +Update `composer.json` as described in "Essentials" above. + + ## Upgrading from v1 to v2 +There were significant database and code changes between v1 to v2. + If you're upgrading from v1 to v2, there's no built-in automatic migration/conversion of your data to the new structure. You will need to carefully adapt your code and your data manually. From 2a9fd80679e45b603ecae5a56109dce46c28bf01 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 28 Apr 2023 21:32:52 -0400 Subject: [PATCH 0692/1013] [Docs] Update v6 upgrade instructions --- docs/upgrading.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index 9642d8326..de7e487eb 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -7,19 +7,21 @@ weight: 6 ALL upgrades of this package should follow these steps: -1. Upgrading between major versions of this package always require the usual Composer steps: +1. Composer. Upgrading between major versions of this package always require the usual Composer steps: - Update your `composer.json` to specify the new major version, such as `^6.0` - Then run `composer update`. -2. Compare the `migration` file stubs in the NEW version of this package against the migrations you've already run inside your app. If necessary, create a new migration (by hand) to apply any new changes. +2. Migrations. Compare the `migration` file stubs in the NEW version of this package against the migrations you've already run inside your app. If necessary, create a new migration (by hand) to apply any new database changes. -3. If you have made any custom Models from this package into your own app, compare the old and new models and apply any relevant updates to your custom models. +3. Config file. Incorporate any changes to the permission.php config file, updating your existing file. (It may be easiest to make a backup copy of your existing file, re-publish it from this package, and then re-make your customizations to it.) -4. If you have overridden any methods from this package's Traits, compare the old and new traits, and apply any relevant updates to your overridden methods. +3. Models. If you have made any custom Models from this package into your own app, compare the old and new models and apply any relevant updates to your custom models. + +4. Custom Methods/Traits. If you have overridden any methods from this package's Traits, compare the old and new traits, and apply any relevant updates to your overridden methods. 5. Apply any version-specific special updates as outlined below... -6. Review the changelog, which details all the changes: https://github.com/spatie/laravel-permission/blob/main/CHANGELOG.md +6. Review the changelog, which details all the changes: [CHANGELOG](https://github.com/spatie/laravel-permission/blob/main/CHANGELOG.md) and/or consult the [Release Notes](https://github.com/spatie/laravel-permission/releases) @@ -30,13 +32,13 @@ Be sure to compare your custom models with originals to see what else may have c 2. If you have a custom Role model and (in the rare case that you might) have overridden the `hasPermissionTo()` method in it, you will need to update its method signature to `hasPermissionTo($permission, $guardName = null):bool`. See PR #2380. -3. Migrations. If you have old migrations you might get the following error: - - `Error: Access to undeclared static property Spatie\Permission\PermissionRegistrar::$pivotPermission` +3. Migrations. Migrations have changed in 2 ways: + - The migrations have been updated to anonymous-class syntax that was introduced in Laravel 8. + - Some structural coding changes in the registrar class changed the way we extracted configuration settings in the migration files. + - THEREFORE: you will need to upgrade your migrations, especially if you get the following error: - To fix this, update your migration file associated with this package. + `Error: Access to undeclared static property Spatie\Permission\PermissionRegistrar::$pivotPermission` - Also note that the migrations have been updated to anonymous-class syntax that was introduced in Laravel 8. 4. NOTE: For consistency with `PermissionMiddleware`, the `RoleOrPermissionMiddleware` has switched from only checking permissions provided by this package to using `canAny()` to check against any abilities registered by your application. This may have the effect of granting those other abilities (such as Super Admin) when using the `RoleOrPermissionMiddleware`, which previously would have failed silently. From c17c0e20c8808e5bb0d4052f04faadd8403330d2 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 28 Apr 2023 21:39:30 -0400 Subject: [PATCH 0693/1013] [Docs] Update v6 upgrade instructions --- docs/upgrading.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index de7e487eb..3ae0b6728 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -26,20 +26,21 @@ and/or consult the [Release Notes](https://github.com/spatie/laravel-permission/ ## Upgrading to v6 +There are a few breaking-changes when upgrading to v6. + 1. If you have overridden the `getPermissionClass()` or `getRoleClass()` methods or have custom Models, you will need to revisit those customizations. See PR #2368 for details. eg: if you have a custom model you will need to make changes, including accessing the model using `$this->permissionClass::` syntax (eg: using `::` instead of `->`) in all the overridden methods that make use of the models. Be sure to compare your custom models with originals to see what else may have changed. 2. If you have a custom Role model and (in the rare case that you might) have overridden the `hasPermissionTo()` method in it, you will need to update its method signature to `hasPermissionTo($permission, $guardName = null):bool`. See PR #2380. -3. Migrations. Migrations have changed in 2 ways: - - The migrations have been updated to anonymous-class syntax that was introduced in Laravel 8. - - Some structural coding changes in the registrar class changed the way we extracted configuration settings in the migration files. - - THEREFORE: you will need to upgrade your migrations, especially if you get the following error: +3. Migrations have changed in a few ways: + 1. The migrations have been updated to anonymous-class syntax that was introduced in Laravel 8. + 2. Some structural coding changes in the registrar class changed the way we extracted configuration settings in the migration files. + 3. THEREFORE: you will need to upgrade your migrations, especially if you get the following error: `Error: Access to undeclared static property Spatie\Permission\PermissionRegistrar::$pivotPermission` - 4. NOTE: For consistency with `PermissionMiddleware`, the `RoleOrPermissionMiddleware` has switched from only checking permissions provided by this package to using `canAny()` to check against any abilities registered by your application. This may have the effect of granting those other abilities (such as Super Admin) when using the `RoleOrPermissionMiddleware`, which previously would have failed silently. 5. Test suites. If you have tests which manually clear the permission cache and re-register permissions, you no longer need to call `\Spatie\Permission\PermissionRegistrar::class)->registerPermissions();`. Such calls MUST be deleted from your tests. From 38a9942497d8f24b2dc2e1f178256efba9a665cf Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 28 Apr 2023 21:43:26 -0400 Subject: [PATCH 0694/1013] [Docs] Update v6 upgrade instructions --- docs/upgrading.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index 3ae0b6728..5c74d4dc9 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -34,10 +34,7 @@ Be sure to compare your custom models with originals to see what else may have c 2. If you have a custom Role model and (in the rare case that you might) have overridden the `hasPermissionTo()` method in it, you will need to update its method signature to `hasPermissionTo($permission, $guardName = null):bool`. See PR #2380. -3. Migrations have changed in a few ways: - 1. The migrations have been updated to anonymous-class syntax that was introduced in Laravel 8. - 2. Some structural coding changes in the registrar class changed the way we extracted configuration settings in the migration files. - 3. THEREFORE: you will need to upgrade your migrations, especially if you get the following error: +3. Migrations will need to be upgraded. (They have been updated to anonymous-class syntax that was introduced in Laravel 8, AND some structural coding changes in the registrar class changed the way we extracted configuration settings in the migration files.) If you had not customized it from the original then replacing the contents of the file should be straightforward. Usually the only customization is if you've switched to UUIDs or customized MySQL index name lengths. If you get the following error, it means your migration file needs upgrading: `Error: Access to undeclared static property Spatie\Permission\PermissionRegistrar::$pivotPermission` From 10f04e72ba74e3c1e1300459b6761614213af9fe Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 28 Apr 2023 21:46:24 -0400 Subject: [PATCH 0695/1013] [Docs] Update v6 upgrade instructions --- docs/upgrading.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index 5c74d4dc9..28d431eb8 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -25,8 +25,8 @@ ALL upgrades of this package should follow these steps: and/or consult the [Release Notes](https://github.com/spatie/laravel-permission/releases) -## Upgrading to v6 -There are a few breaking-changes when upgrading to v6. +## Upgrading from v5 to v6 +There are a few breaking-changes when upgrading to v6, but most of them won't affect you unless you have been customizing things. 1. If you have overridden the `getPermissionClass()` or `getRoleClass()` methods or have custom Models, you will need to revisit those customizations. See PR #2368 for details. eg: if you have a custom model you will need to make changes, including accessing the model using `$this->permissionClass::` syntax (eg: using `::` instead of `->`) in all the overridden methods that make use of the models. @@ -34,9 +34,8 @@ Be sure to compare your custom models with originals to see what else may have c 2. If you have a custom Role model and (in the rare case that you might) have overridden the `hasPermissionTo()` method in it, you will need to update its method signature to `hasPermissionTo($permission, $guardName = null):bool`. See PR #2380. -3. Migrations will need to be upgraded. (They have been updated to anonymous-class syntax that was introduced in Laravel 8, AND some structural coding changes in the registrar class changed the way we extracted configuration settings in the migration files.) If you had not customized it from the original then replacing the contents of the file should be straightforward. Usually the only customization is if you've switched to UUIDs or customized MySQL index name lengths. If you get the following error, it means your migration file needs upgrading: - - `Error: Access to undeclared static property Spatie\Permission\PermissionRegistrar::$pivotPermission` +3. Migrations will need to be upgraded. (They have been updated to anonymous-class syntax that was introduced in Laravel 8, AND some structural coding changes in the registrar class changed the way we extracted configuration settings in the migration files.) If you had not customized it from the original then replacing the contents of the file should be straightforward. Usually the only customization is if you've switched to UUIDs or customized MySQL index name lengths. +If you get the following error, it means your migration file needs upgrading: `Error: Access to undeclared static property Spatie\Permission\PermissionRegistrar::$pivotPermission` 4. NOTE: For consistency with `PermissionMiddleware`, the `RoleOrPermissionMiddleware` has switched from only checking permissions provided by this package to using `canAny()` to check against any abilities registered by your application. This may have the effect of granting those other abilities (such as Super Admin) when using the `RoleOrPermissionMiddleware`, which previously would have failed silently. From 76079fd81f585129a7fbcc4ad3418027f658230e Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 1 May 2023 15:24:03 -0400 Subject: [PATCH 0696/1013] Code formatting --- src/PermissionServiceProvider.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 49580a4de..639e7faa7 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -44,8 +44,7 @@ public function register() 'permission' ); - $this->callAfterResolving('blade.compiler', fn (BladeCompiler $bladeCompiler) => $this->registerBladeExtensions($bladeCompiler) - ); + $this->callAfterResolving('blade.compiler', fn (BladeCompiler $bladeCompiler) => $this->registerBladeExtensions($bladeCompiler)); } protected function offerPublishing(): void From 2df70da68a84e8530abfc99c286286a522a4f347 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Tue, 2 May 2023 16:09:26 -0500 Subject: [PATCH 0697/1013] [V6] Use attach instead of sync on traits (#2420) * Use attach instead of sync on traits * Test touch on syncRoles, syncPermissions --- src/Traits/HasPermissions.php | 13 +++++++----- src/Traits/HasRoles.php | 13 +++++++----- tests/HasPermissionsTest.php | 16 +++++++++++++-- tests/HasPermissionsWithCustomModelsTest.php | 21 ++++++++++++++++++++ tests/HasRolesTest.php | 16 +++++++++++++-- tests/HasRolesWithCustomModelsTest.php | 21 ++++++++++++++++++++ tests/TestModels/Admin.php | 2 ++ 7 files changed, 88 insertions(+), 14 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 174c254fe..8d94ebcd3 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -370,8 +370,7 @@ private function collectPermissions(...$permissions): array $this->ensureModelSharesGuard($permission); - $array[$permission->getKey()] = app(PermissionRegistrar::class)->teams && ! is_a($this, Role::class) ? - [app(PermissionRegistrar::class)->teamsKey => getPermissionsTeamId()] : []; + $array[] = $permission->getKey(); return $array; }, []); @@ -388,19 +387,23 @@ public function givePermissionTo(...$permissions) $permissions = $this->collectPermissions($permissions); $model = $this->getModel(); + $teamPivot = app(PermissionRegistrar::class)->teams && ! is_a($this, Role::class) ? + [app(PermissionRegistrar::class)->teamsKey => getPermissionsTeamId()] : []; if ($model->exists) { - $this->permissions()->sync($permissions, false); + $currentPermissions = $this->permissions()->get()->map(fn ($permission) => $permission->getKey())->toArray(); + + $this->permissions()->attach(array_diff($permissions, $currentPermissions), $teamPivot); $model->unsetRelation('permissions'); } else { $class = \get_class($model); $class::saved( - function ($object) use ($permissions, $model) { + function ($object) use ($permissions, $model, $teamPivot) { if ($model->getKey() != $object->getKey()) { return; } - $model->permissions()->sync($permissions, false); + $model->permissions()->attach($permissions, $teamPivot); $model->unsetRelation('permissions'); } ); diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 419f5189a..cad1aebd6 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -116,8 +116,7 @@ private function collectRoles(...$roles): array $this->ensureModelSharesGuard($role); - $array[$role->getKey()] = app(PermissionRegistrar::class)->teams && ! is_a($this, Permission::class) ? - [app(PermissionRegistrar::class)->teamsKey => getPermissionsTeamId()] : []; + $array[] = $role->getKey(); return $array; }, []); @@ -134,19 +133,23 @@ public function assignRole(...$roles) $roles = $this->collectRoles($roles); $model = $this->getModel(); + $teamPivot = app(PermissionRegistrar::class)->teams && ! is_a($this, Permission::class) ? + [app(PermissionRegistrar::class)->teamsKey => getPermissionsTeamId()] : []; if ($model->exists) { - $this->roles()->sync($roles, false); + $currentRoles = $this->roles()->get()->map(fn ($role) => $role->getKey())->toArray(); + + $this->roles()->attach(array_diff($roles, $currentRoles), $teamPivot); $model->unsetRelation('roles'); } else { $class = \get_class($model); $class::saved( - function ($object) use ($roles, $model) { + function ($object) use ($roles, $model, $teamPivot) { if ($model->getKey() != $object->getKey()) { return; } - $model->roles()->sync($roles, false); + $model->roles()->attach($roles, $teamPivot); $model->unsetRelation('roles'); } ); diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index c876294f9..c062325e1 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -572,6 +572,18 @@ public function it_can_sync_permissions_to_a_model_that_is_not_persisted() $this->assertTrue($user->fresh()->hasPermissionTo('edit-articles')); } + /** @test */ + public function it_does_not_run_unnecessary_sqls_when_assigning_new_permissions() + { + $permission2 = app(Permission::class)->where('name', ['edit-news'])->first(); + + DB::enableQueryLog(); + $this->testUser->syncPermissions($this->testUserPermission, $permission2); + DB::disableQueryLog(); + + $this->assertSame(3, count(DB::getQueryLog())); //avoid unnecessary sqls + } + /** @test */ public function calling_givePermissionTo_before_saving_object_doesnt_interfere_with_other_objects() { @@ -591,7 +603,7 @@ public function calling_givePermissionTo_before_saving_object_doesnt_interfere_w $this->assertTrue($user2->fresh()->hasPermissionTo('edit-articles')); $this->assertFalse($user2->fresh()->hasPermissionTo('edit-news')); - $this->assertSame(3, count(DB::getQueryLog())); //avoid unnecessary sync + $this->assertSame(2, count(DB::getQueryLog())); //avoid unnecessary sync } /** @test */ @@ -613,7 +625,7 @@ public function calling_syncPermissions_before_saving_object_doesnt_interfere_wi $this->assertTrue($user2->fresh()->hasPermissionTo('edit-articles')); $this->assertFalse($user2->fresh()->hasPermissionTo('edit-news')); - $this->assertSame(3, count(DB::getQueryLog())); //avoid unnecessary sync + $this->assertSame(2, count(DB::getQueryLog())); //avoid unnecessary sync } /** @test */ diff --git a/tests/HasPermissionsWithCustomModelsTest.php b/tests/HasPermissionsWithCustomModelsTest.php index 7f5ccde75..60aa481e2 100644 --- a/tests/HasPermissionsWithCustomModelsTest.php +++ b/tests/HasPermissionsWithCustomModelsTest.php @@ -2,8 +2,10 @@ namespace Spatie\Permission\Tests; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; use Spatie\Permission\PermissionRegistrar; +use Spatie\Permission\Tests\TestModels\Admin; use Spatie\Permission\Tests\TestModels\Permission; use Spatie\Permission\Tests\TestModels\User; @@ -129,4 +131,23 @@ public function it_does_detach_roles_and_users_when_force_deleting() $this->assertEquals(0, DB::table(config('permission.table_names.role_has_permissions'))->where('permission_test_id', $permission_id)->count()); $this->assertEquals(0, DB::table(config('permission.table_names.model_has_permissions'))->where('permission_test_id', $permission_id)->count()); } + + /** @test */ + public function it_should_touch_when_assigning_new_permissions() + { + Carbon::setTestNow('2021-07-19 10:13:14'); + + $user = Admin::create(['email' => 'user1@test.com']); + $permission1 = Permission::create(['name' => 'edit-news', 'guard_name' => 'admin']); + $permission2 = Permission::create(['name' => 'edit-blog', 'guard_name' => 'admin']); + + $this->assertSame('2021-07-19 10:13:14', $permission1->updated_at->format('Y-m-d H:i:s')); + + Carbon::setTestNow('2021-07-20 19:13:14'); + + $user->syncPermissions([$permission1->getKey(), $permission2->getKey()]); + + $this->assertSame('2021-07-20 19:13:14', $permission1->refresh()->updated_at->format('Y-m-d H:i:s')); + $this->assertSame('2021-07-20 19:13:14', $permission2->refresh()->updated_at->format('Y-m-d H:i:s')); + } } diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index b984aeb4d..ed04757ed 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -284,6 +284,18 @@ public function it_will_sync_roles_to_a_model_that_is_not_persisted() $this->assertTrue($user->hasRole($this->testUserRole)); } + /** @test */ + public function it_does_not_run_unnecessary_sqls_when_assigning_new_roles() + { + $role2 = app(Role::class)->where('name', ['testRole2'])->first(); + + DB::enableQueryLog(); + $this->testUser->syncRoles($this->testUserRole, $role2); + DB::disableQueryLog(); + + $this->assertSame(3, count(DB::getQueryLog())); //avoid unnecessary sqls + } + /** @test */ public function calling_syncRoles_before_saving_object_doesnt_interfere_with_other_objects() { @@ -303,7 +315,7 @@ public function calling_syncRoles_before_saving_object_doesnt_interfere_with_oth $this->assertTrue($user2->fresh()->hasRole('testRole2')); $this->assertFalse($user2->fresh()->hasRole('testRole')); - $this->assertSame(3, count(DB::getQueryLog())); //avoid unnecessary sync + $this->assertSame(2, count(DB::getQueryLog())); //avoid unnecessary sync } /** @test */ @@ -325,7 +337,7 @@ public function calling_assignRole_before_saving_object_doesnt_interfere_with_ot $this->assertTrue($admin_user->fresh()->hasRole('testRole2')); $this->assertFalse($admin_user->fresh()->hasRole('testRole')); - $this->assertSame(3, count(DB::getQueryLog())); //avoid unnecessary sync + $this->assertSame(2, count(DB::getQueryLog())); //avoid unnecessary sync } /** @test */ diff --git a/tests/HasRolesWithCustomModelsTest.php b/tests/HasRolesWithCustomModelsTest.php index 659f04db7..b306676af 100644 --- a/tests/HasRolesWithCustomModelsTest.php +++ b/tests/HasRolesWithCustomModelsTest.php @@ -2,7 +2,9 @@ namespace Spatie\Permission\Tests; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; +use Spatie\Permission\Tests\TestModels\Admin; use Spatie\Permission\Tests\TestModels\Role; class HasRolesWithCustomModelsTest extends HasRolesTest @@ -79,4 +81,23 @@ public function it_does_detach_permissions_and_users_when_force_deleting() $this->assertEquals(0, DB::table(config('permission.table_names.role_has_permissions'))->where('role_test_id', $role_id)->count()); $this->assertEquals(0, DB::table(config('permission.table_names.model_has_roles'))->where('role_test_id', $role_id)->count()); } + + /** @test */ + public function it_should_touch_when_assigning_new_roles() + { + Carbon::setTestNow('2021-07-19 10:13:14'); + + $user = Admin::create(['email' => 'user1@test.com']); + $role1 = app(Role::class)->create(['name' => 'testRoleInWebGuard', 'guard_name' => 'admin']); + $role2 = app(Role::class)->create(['name' => 'testRoleInWebGuard1', 'guard_name' => 'admin']); + + $this->assertSame('2021-07-19 10:13:14', $role1->updated_at->format('Y-m-d H:i:s')); + + Carbon::setTestNow('2021-07-20 19:13:14'); + + $user->syncRoles([$role1->getKey(), $role2->getKey()]); + + $this->assertSame('2021-07-20 19:13:14', $role1->refresh()->updated_at->format('Y-m-d H:i:s')); + $this->assertSame('2021-07-20 19:13:14', $role2->refresh()->updated_at->format('Y-m-d H:i:s')); + } } diff --git a/tests/TestModels/Admin.php b/tests/TestModels/Admin.php index 5b6334a98..c2aa1c85f 100644 --- a/tests/TestModels/Admin.php +++ b/tests/TestModels/Admin.php @@ -5,4 +5,6 @@ class Admin extends User { protected $table = 'admins'; + + protected $touches = ['roles', 'permissions']; } From e64d2f9fd510dc10174d66d170f1eb09efa45530 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Tue, 2 May 2023 17:49:48 -0500 Subject: [PATCH 0698/1013] fewer sqls in syncRoles, syncPermissions (#2423) --- src/Traits/HasPermissions.php | 10 ++++++---- src/Traits/HasRoles.php | 10 ++++++---- tests/HasPermissionsTest.php | 2 +- tests/HasRolesTest.php | 6 +++++- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 8d94ebcd3..6c821ffdc 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -391,7 +391,7 @@ public function givePermissionTo(...$permissions) [app(PermissionRegistrar::class)->teamsKey => getPermissionsTeamId()] : []; if ($model->exists) { - $currentPermissions = $this->permissions()->get()->map(fn ($permission) => $permission->getKey())->toArray(); + $currentPermissions = $this->permissions->map(fn ($permission) => $permission->getKey())->toArray(); $this->permissions()->attach(array_diff($permissions, $currentPermissions), $teamPivot); $model->unsetRelation('permissions'); @@ -424,9 +424,11 @@ function ($object) use ($permissions, $model, $teamPivot) { */ public function syncPermissions(...$permissions) { - $this->collectPermissions($permissions); - - $this->permissions()->detach(); + if ($this->getModel()->exists) { + $this->collectPermissions($permissions); + $this->permissions()->detach(); + $this->setRelation('permissions', collect()); + } return $this->givePermissionTo($permissions); } diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index cad1aebd6..ab637a795 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -137,7 +137,7 @@ public function assignRole(...$roles) [app(PermissionRegistrar::class)->teamsKey => getPermissionsTeamId()] : []; if ($model->exists) { - $currentRoles = $this->roles()->get()->map(fn ($role) => $role->getKey())->toArray(); + $currentRoles = $this->roles->map(fn ($role) => $role->getKey())->toArray(); $this->roles()->attach(array_diff($roles, $currentRoles), $teamPivot); $model->unsetRelation('roles'); @@ -188,9 +188,11 @@ public function removeRole($role) */ public function syncRoles(...$roles) { - $this->collectRoles($roles); - - $this->roles()->detach(); + if ($this->getModel()->exists) { + $this->collectRoles($roles); + $this->roles()->detach(); + $this->setRelation('roles', collect()); + } return $this->assignRole($roles); } diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index c062325e1..e5c9a4c85 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -581,7 +581,7 @@ public function it_does_not_run_unnecessary_sqls_when_assigning_new_permissions( $this->testUser->syncPermissions($this->testUserPermission, $permission2); DB::disableQueryLog(); - $this->assertSame(3, count(DB::getQueryLog())); //avoid unnecessary sqls + $this->assertSame(2, count(DB::getQueryLog())); //avoid unnecessary sqls } /** @test */ diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index ed04757ed..1f86ff47d 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -282,6 +282,10 @@ public function it_will_sync_roles_to_a_model_that_is_not_persisted() $user->save(); $this->assertTrue($user->hasRole($this->testUserRole)); + + $user->syncRoles([$this->testUserRole]); + $this->assertTrue($user->hasRole($this->testUserRole)); + $this->assertTrue($user->fresh()->hasRole($this->testUserRole)); } /** @test */ @@ -293,7 +297,7 @@ public function it_does_not_run_unnecessary_sqls_when_assigning_new_roles() $this->testUser->syncRoles($this->testUserRole, $role2); DB::disableQueryLog(); - $this->assertSame(3, count(DB::getQueryLog())); //avoid unnecessary sqls + $this->assertSame(2, count(DB::getQueryLog())); //avoid unnecessary sqls } /** @test */ From 587ba9cd2c91c4cb4f15f862d9538585f84831dc Mon Sep 17 00:00:00 2001 From: Jesper Noordsij <45041769+jnoordsij@users.noreply.github.com> Date: Sun, 7 May 2023 20:21:11 +0200 Subject: [PATCH 0699/1013] Add middleware using static method (#2424) * Allow static creation of middlewares with using method * Add tests for creation of middleware with static using method * Add example about static middleware methods to docs --- docs/basic-usage/middleware.md | 30 ++++++++++++++++++- src/Middlewares/PermissionMiddleware.php | 15 ++++++++++ src/Middlewares/RoleMiddleware.php | 15 ++++++++++ .../RoleOrPermissionMiddleware.php | 15 ++++++++++ tests/PermissionMiddlewareTest.php | 17 +++++++++++ tests/RoleMiddlewareTest.php | 17 +++++++++++ tests/RoleOrPermissionMiddlewareTest.php | 17 +++++++++++ tests/TestCase.php | 22 +++++++------- 8 files changed, 136 insertions(+), 12 deletions(-) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index 6d2564728..48a5ceee1 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -13,9 +13,18 @@ Route::group(['middleware' => ['can:publish articles']], function () { }); ``` +In Laravel v10.9 and up, you can also call this middleware with a static method. + +```php +Route::group(['middleware' => [\Illuminate\Auth\Middleware\Authorize::using('publish articles')]], function () { + // +}); +``` + ## Package Middleware -This package comes with `RoleMiddleware`, `PermissionMiddleware` and `RoleOrPermissionMiddleware` middleware. You can add them inside your `app/Http/Kernel.php` file. +This package comes with `RoleMiddleware`, `PermissionMiddleware` and `RoleOrPermissionMiddleware` middleware. +You can add them inside your `app/Http/Kernel.php` file to be able to use them through aliases. Note the property name difference between Laravel 10 and older versions of Laravel: @@ -88,3 +97,22 @@ public function __construct() ``` (You can use Laravel's Model Policy feature with your controller methods. See the Model Policies section of these docs.) + +## Use middleware static methods + +All of the middlewares can also be applied by calling the static `using` method, +which accepts either a `|`-separated string or an array as input. + +```php +Route::group(['middleware' => [\Spatie\Permission\Middlewares\RoleMiddleware::using('super-admin')]], function () { + // +}); + +Route::group(['middleware' => [\Spatie\Permission\Middlewares\PermissionMiddleware::using('publish articles|edit articles')]], function () { + // +}); + +Route::group(['middleware' => [\Spatie\Permission\Middlewares\RoleOrPermissionMiddleware::using(['super-admin', 'edit articles'])]], function () { + // +}); +``` diff --git a/src/Middlewares/PermissionMiddleware.php b/src/Middlewares/PermissionMiddleware.php index 49793f5ce..ca37cb672 100644 --- a/src/Middlewares/PermissionMiddleware.php +++ b/src/Middlewares/PermissionMiddleware.php @@ -32,4 +32,19 @@ public function handle($request, Closure $next, $permission, $guard = null) return $next($request); } + + /** + * Specify the permission and guard for the middleware. + * + * @param array|string $permission + * @param string|null $guard + * @return string + */ + public static function using($permission, $guard = null) + { + $permissionString = is_string($permission) ? $permission : implode('|', $permission); + $args = is_null($guard) ? $permissionString : "$permissionString,$guard"; + + return static::class.':'.$args; + } } diff --git a/src/Middlewares/RoleMiddleware.php b/src/Middlewares/RoleMiddleware.php index 4e81f0073..ae9232cd5 100644 --- a/src/Middlewares/RoleMiddleware.php +++ b/src/Middlewares/RoleMiddleware.php @@ -32,4 +32,19 @@ public function handle($request, Closure $next, $role, $guard = null) return $next($request); } + + /** + * Specify the role and guard for the middleware. + * + * @param array|string $role + * @param string|null $guard + * @return string + */ + public static function using($role, $guard = null) + { + $roleString = is_string($role) ? $role : implode('|', $role); + $args = is_null($guard) ? $roleString : "$roleString,$guard"; + + return static::class.':'.$args; + } } diff --git a/src/Middlewares/RoleOrPermissionMiddleware.php b/src/Middlewares/RoleOrPermissionMiddleware.php index 5a9aaa341..52675a43f 100644 --- a/src/Middlewares/RoleOrPermissionMiddleware.php +++ b/src/Middlewares/RoleOrPermissionMiddleware.php @@ -31,4 +31,19 @@ public function handle($request, Closure $next, $roleOrPermission, $guard = null return $next($request); } + + /** + * Specify the role or permission and guard for the middleware. + * + * @param array|string $roleOrPermission + * @param string|null $guard + * @return string + */ + public static function using($roleOrPermission, $guard = null) + { + $roleOrPermissionString = is_string($roleOrPermission) ? $roleOrPermission : implode('|', $roleOrPermission); + $args = is_null($guard) ? $roleOrPermissionString : "$roleOrPermissionString,$guard"; + + return static::class.':'.$args; + } } diff --git a/tests/PermissionMiddlewareTest.php b/tests/PermissionMiddlewareTest.php index 39a57c48b..d8831c309 100644 --- a/tests/PermissionMiddlewareTest.php +++ b/tests/PermissionMiddlewareTest.php @@ -254,4 +254,21 @@ public function user_can_access_permission_with_guard_admin_while_login_using_ad $this->runMiddleware($this->permissionMiddleware, 'admin-permission', 'admin') ); } + + /** @test */ + public function the_middleware_can_be_created_with_static_using_method() + { + $this->assertSame( + 'Spatie\Permission\Middlewares\PermissionMiddleware:edit-articles', + PermissionMiddleware::using('edit-articles') + ); + $this->assertEquals( + 'Spatie\Permission\Middlewares\PermissionMiddleware:edit-articles,my-guard', + PermissionMiddleware::using('edit-articles', 'my-guard') + ); + $this->assertEquals( + 'Spatie\Permission\Middlewares\PermissionMiddleware:edit-articles|edit-news', + PermissionMiddleware::using(['edit-articles', 'edit-news']) + ); + } } diff --git a/tests/RoleMiddlewareTest.php b/tests/RoleMiddlewareTest.php index 7d532a004..a28c2de07 100644 --- a/tests/RoleMiddlewareTest.php +++ b/tests/RoleMiddlewareTest.php @@ -204,4 +204,21 @@ public function user_can_access_role_with_guard_admin_while_login_using_admin_gu $this->runMiddleware($this->roleMiddleware, 'testAdminRole', 'admin') ); } + + /** @test */ + public function the_middleware_can_be_created_with_static_using_method() + { + $this->assertSame( + 'Spatie\Permission\Middlewares\RoleMiddleware:testAdminRole', + RoleMiddleware::using('testAdminRole') + ); + $this->assertEquals( + 'Spatie\Permission\Middlewares\RoleMiddleware:testAdminRole,my-guard', + RoleMiddleware::using('testAdminRole', 'my-guard') + ); + $this->assertEquals( + 'Spatie\Permission\Middlewares\RoleMiddleware:testAdminRole|anotherRole', + RoleMiddleware::using(['testAdminRole', 'anotherRole']) + ); + } } diff --git a/tests/RoleOrPermissionMiddlewareTest.php b/tests/RoleOrPermissionMiddlewareTest.php index e9f628932..24367e51f 100644 --- a/tests/RoleOrPermissionMiddlewareTest.php +++ b/tests/RoleOrPermissionMiddlewareTest.php @@ -194,4 +194,21 @@ public function the_required_permissions_or_roles_can_be_displayed_in_the_except $this->assertStringEndsWith('Necessary roles or permissions are some-permission, some-role', $message); } + + /** @test */ + public function the_middleware_can_be_created_with_static_using_method() + { + $this->assertSame( + 'Spatie\Permission\Middlewares\RoleOrPermissionMiddleware:edit-articles', + RoleOrPermissionMiddleware::using('edit-articles') + ); + $this->assertEquals( + 'Spatie\Permission\Middlewares\RoleOrPermissionMiddleware:edit-articles,my-guard', + RoleOrPermissionMiddleware::using('edit-articles', 'my-guard') + ); + $this->assertEquals( + 'Spatie\Permission\Middlewares\RoleOrPermissionMiddleware:edit-articles|testAdminRole', + RoleOrPermissionMiddleware::using(['edit-articles', 'testAdminRole']) + ); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 456964ec1..3fe775b08 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -243,15 +243,15 @@ public function getLastRouteMiddlewareFromRouter($router) return last($router->getRoutes()->get())->middleware(); } - public function getRouter() - { - return app('router'); - } - - public function getRouteResponse() - { - return function () { - return (new Response())->setContent(''); - }; - } + public function getRouter() + { + return app('router'); + } + + public function getRouteResponse() + { + return function () { + return (new Response())->setContent(''); + }; + } } From 563794f2309fee43f8d56d993bd0085655a0889b Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 7 May 2023 18:43:39 -0400 Subject: [PATCH 0700/1013] type declaration --- src/WildcardPermission.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/WildcardPermission.php b/src/WildcardPermission.php index f8946a5c5..50a741dd8 100644 --- a/src/WildcardPermission.php +++ b/src/WildcardPermission.php @@ -11,10 +11,10 @@ class WildcardPermission implements Wildcard /** @var string */ public const WILDCARD_TOKEN = '*'; - /** @var string */ + /** @var non-empty-string */ public const PART_DELIMITER = '.'; - /** @var string */ + /** @var non-empty-string */ public const SUBPART_DELIMITER = ','; /** @var string */ From 3d40339fb96b8997be94c6d850cbbbe1553e416d Mon Sep 17 00:00:00 2001 From: erikn69 Date: Fri, 26 May 2023 21:58:34 -0500 Subject: [PATCH 0701/1013] Update PHPDocs for IDE autocompletion (#2437) --- src/Models/Permission.php | 10 ++++++++++ src/Models/Role.php | 12 +++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index f5edcb90f..21ce630ea 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -34,6 +34,11 @@ public function __construct(array $attributes = []) $this->table = config('permission.table_names.permissions') ?: parent::getTable(); } + /** + * @return PermissionContract|Permission + * + * @throws PermissionAlreadyExists + */ public static function create(array $attributes = []) { $attributes['guard_name'] = $attributes['guard_name'] ?? Guard::getDefaultName(static::class); @@ -78,6 +83,7 @@ public function users(): BelongsToMany * Find a permission by its name (and optionally guardName). * * @param string|null $guardName + * @return PermissionContract|Permission * * @throws PermissionDoesNotExist */ @@ -97,6 +103,7 @@ public static function findByName(string $name, $guardName = null): PermissionCo * * @param int|string $id * @param string|null $guardName + * @return PermissionContract|Permission * * @throws PermissionDoesNotExist */ @@ -116,6 +123,7 @@ public static function findById($id, $guardName = null): PermissionContract * Find or create permission by its name (and optionally guardName). * * @param string|null $guardName + * @return PermissionContract|Permission */ public static function findOrCreate(string $name, $guardName = null): PermissionContract { @@ -141,6 +149,8 @@ protected static function getPermissions(array $params = [], bool $onlyOne = fal /** * Get the current cached first permission. + * + * @return PermissionContract|Permission|null */ protected static function getPermission(array $params = []): ?PermissionContract { diff --git a/src/Models/Role.php b/src/Models/Role.php index 7199504d1..025147864 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -35,6 +35,11 @@ public function __construct(array $attributes = []) $this->table = config('permission.table_names.roles') ?: parent::getTable(); } + /** + * @return RoleContract|Role + * + * @throws RoleAlreadyExists + */ public static function create(array $attributes = []) { $attributes['guard_name'] = $attributes['guard_name'] ?? Guard::getDefaultName(static::class); @@ -143,7 +148,12 @@ public static function findOrCreate(string $name, $guardName = null): RoleContra return $role; } - protected static function findByParam(array $params = []) + /** + * Finds a role based on an array of parameters. + * + * @return RoleContract|Role|null + */ + protected static function findByParam(array $params = []): ?RoleContract { $query = static::query(); From def2e7382650ba7dd78d5aa2b253ff934831e398 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 May 2023 05:59:44 +0000 Subject: [PATCH 0702/1013] Bump aglipanci/laravel-pint-action from 2.2.0 to 2.3.0 Bumps [aglipanci/laravel-pint-action](https://github.com/aglipanci/laravel-pint-action) from 2.2.0 to 2.3.0. - [Release notes](https://github.com/aglipanci/laravel-pint-action/releases) - [Commits](https://github.com/aglipanci/laravel-pint-action/compare/2.2.0...2.3.0) --- updated-dependencies: - dependency-name: aglipanci/laravel-pint-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/fix-php-code-style-issues.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/fix-php-code-style-issues.yml b/.github/workflows/fix-php-code-style-issues.yml index f76938275..49d14b77e 100644 --- a/.github/workflows/fix-php-code-style-issues.yml +++ b/.github/workflows/fix-php-code-style-issues.yml @@ -16,7 +16,7 @@ jobs: ref: ${{ github.head_ref }} - name: Fix PHP code style issues - uses: aglipanci/laravel-pint-action@2.2.0 + uses: aglipanci/laravel-pint-action@2.3.0 - name: Commit changes uses: stefanzweifel/git-auto-commit-action@v4 From badd36c57d77b695d9aec2a3435dc17804139a10 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 May 2023 05:59:49 +0000 Subject: [PATCH 0703/1013] Bump dependabot/fetch-metadata from 1.4.0 to 1.5.1 Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 1.4.0 to 1.5.1. - [Release notes](https://github.com/dependabot/fetch-metadata/releases) - [Commits](https://github.com/dependabot/fetch-metadata/compare/v1.4.0...v1.5.1) --- updated-dependencies: - dependency-name: dependabot/fetch-metadata dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/dependabot-auto-merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 4c8e4c314..3f60ce0f6 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -13,7 +13,7 @@ jobs: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v1.4.0 + uses: dependabot/fetch-metadata@v1.5.1 with: github-token: "${{ secrets.GITHUB_TOKEN }}" compat-lookup: true From 6e1d309e6c6f0c109a66190be5f296927fae0bdb Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 15 Jun 2023 13:34:22 -0400 Subject: [PATCH 0704/1013] Note the requirement for foreign-key support --- docs/prerequisites.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/prerequisites.md b/docs/prerequisites.md index b59d4cc2d..beb6c87a8 100644 --- a/docs/prerequisites.md +++ b/docs/prerequisites.md @@ -7,6 +7,10 @@ weight: 3 This package can be used in Laravel 6 or higher. Check the "Installing on Laravel" page for package versions compatible with various Laravel versions. +## Database with foreign-key relationship capability + +This package depends on cascading delete rules to enforce database integrity, so foreign-key support is required by your database engine. + ## User Model / Contract/Interface This package uses Laravel's Gate layer to provide Authorization capabilities. From c382b0dda7906e7c3abbda226bfb7f66fb1a39c9 Mon Sep 17 00:00:00 2001 From: Dan Harrin Date: Sat, 17 Jun 2023 18:12:47 +0100 Subject: [PATCH 0705/1013] fix: Wildcard permissions algorithm performance (#2445) * New wildcard algorithm * Fix issue with multipart wildcard captures * Refactor to use existing WildcardPermission class --- src/Contracts/Wildcard.php | 11 +-- src/PermissionRegistrar.php | 26 +++++++ src/Traits/HasPermissions.php | 38 ++++++----- src/WildcardPermission.php | 125 ++++++++++++++++++---------------- 4 files changed, 122 insertions(+), 78 deletions(-) diff --git a/src/Contracts/Wildcard.php b/src/Contracts/Wildcard.php index d2aabd8f8..68f15be44 100644 --- a/src/Contracts/Wildcard.php +++ b/src/Contracts/Wildcard.php @@ -2,10 +2,13 @@ namespace Spatie\Permission\Contracts; +use Illuminate\Database\Eloquent\Model; + interface Wildcard { - /** - * @param string|Wildcard $permission - */ - public function implies($permission): bool; + public function __construct(Model $record); + + public function getIndex(): array; + + public function implies(string $permission, string $guardName, array $index): bool; } diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 6e1574f63..be1a0aa12 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -8,8 +8,11 @@ use Illuminate\Contracts\Cache\Repository; use Illuminate\Contracts\Cache\Store; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Str; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Contracts\Role; +use Spatie\Permission\Exceptions\WildcardPermissionNotProperlyFormatted; class PermissionRegistrar { @@ -46,6 +49,8 @@ class PermissionRegistrar private array $except = []; + private array $wildcardPermissionsIndex = []; + /** * PermissionRegistrar constructor. */ @@ -134,10 +139,22 @@ public function registerPermissions(Gate $gate): bool public function forgetCachedPermissions() { $this->permissions = null; + $this->forgetWildcardPermissionIndex(); return $this->cache->forget($this->cacheKey); } + public function forgetWildcardPermissionIndex(?Model $record = null): void + { + if ($record) { + unset($this->wildcardPermissionsIndex[get_class($record)][$record->getKey()]); + + return; + } + + $this->wildcardPermissionsIndex = []; + } + /** * Clear already loaded permissions collection. * This is only intended to be called by the PermissionServiceProvider on boot, @@ -148,6 +165,15 @@ public function clearPermissionsCollection(): void $this->permissions = null; } + public function getWildcardPermissionIndex(Model $record): array + { + if (isset($this->wildcardPermissionsIndex[get_class($record)][$record->getKey()])) { + return $this->wildcardPermissionsIndex[get_class($record)][$record->getKey()]; + } + + return $this->wildcardPermissionsIndex[get_class($record)][$record->getKey()] = app($record->getWildcardClass(), ['record' => $record])->getIndex(); + } + /** * @deprecated * diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 6c821ffdc..b20953246 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Support\Arr; use Illuminate\Support\Collection; +use Illuminate\Support\Str; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Contracts\Role; use Spatie\Permission\Contracts\Wildcard; @@ -13,9 +14,11 @@ use Spatie\Permission\Exceptions\PermissionDoesNotExist; use Spatie\Permission\Exceptions\WildcardPermissionInvalidArgument; use Spatie\Permission\Exceptions\WildcardPermissionNotImplementsContract; +use Spatie\Permission\Exceptions\WildcardPermissionNotProperlyFormatted; use Spatie\Permission\Guard; use Spatie\Permission\PermissionRegistrar; use Spatie\Permission\WildcardPermission; +use Spatie\Permission\WildcardPermissionIndexChecker; trait HasPermissions { @@ -23,6 +26,8 @@ trait HasPermissions private ?string $wildcardClass = null; + private array $wildcardPermissionsIndex; + public static function bootHasPermissions() { static::deleting(function ($model) { @@ -51,7 +56,7 @@ public function getPermissionClass(): string return $this->permissionClass; } - protected function getWildcardClass() + public function getWildcardClass() { if (! is_null($this->wildcardClass)) { return $this->wildcardClass; @@ -226,21 +231,11 @@ protected function hasWildcardPermission($permission, $guardName = null): bool throw WildcardPermissionInvalidArgument::create(); } - $WildcardPermissionClass = $this->getWildcardClass(); - - foreach ($this->getAllPermissions() as $userPermission) { - if ($guardName !== $userPermission->guard_name) { - continue; - } - - $userPermission = new $WildcardPermissionClass($userPermission->name); - - if ($userPermission->implies($permission)) { - return true; - } - } - - return false; + return app($this->getWildcardClass(), ['record' => $this])->implies( + $permission, + $guardName, + app(PermissionRegistrar::class)->getWildcardPermissionIndex($this), + ); } /** @@ -413,9 +408,18 @@ function ($object) use ($permissions, $model, $teamPivot) { $this->forgetCachedPermissions(); } + $this->forgetWildcardPermissionIndex(); + return $this; } + public function forgetWildcardPermissionIndex(): void + { + app(PermissionRegistrar::class)->forgetWildcardPermissionIndex( + is_a($this, Role::class) ? null : $this, + ); + } + /** * Remove all current permissions and set the given ones. * @@ -447,6 +451,8 @@ public function revokePermissionTo($permission) $this->forgetCachedPermissions(); } + $this->forgetWildcardPermissionIndex(); + $this->unsetRelation('permissions'); return $this; diff --git a/src/WildcardPermission.php b/src/WildcardPermission.php index 50a741dd8..2c14f868c 100644 --- a/src/WildcardPermission.php +++ b/src/WildcardPermission.php @@ -2,7 +2,9 @@ namespace Spatie\Permission; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; +use Illuminate\Support\Str; use Spatie\Permission\Contracts\Wildcard; use Spatie\Permission\Exceptions\WildcardPermissionNotProperlyFormatted; @@ -17,94 +19,101 @@ class WildcardPermission implements Wildcard /** @var non-empty-string */ public const SUBPART_DELIMITER = ','; - /** @var string */ - protected $permission; + protected Model $record; - /** @var Collection */ - protected $parts; + public function __construct(Model $record) + { + $this->record = $record; + } - public function __construct(string $permission) + public function getIndex(): array { - $this->permission = $permission; - $this->parts = collect(); + $index = []; + + foreach ($this->record->getAllPermissions() as $permission) { + $index[$permission->guard_name] = $this->buildIndex( + $index[$permission->guard_name] ?? [], + explode(static::PART_DELIMITER, $permission->name), + $permission->name, + ); + } - $this->setParts(); + return $index; } - /** - * @param string|WildcardPermission $permission - */ - public function implies($permission): bool + protected function buildIndex(array $index, array $parts, string $permission): array { - if (is_string($permission)) { - $permission = new static($permission); - } + if (empty($parts)) { + $index[null] = true; - $otherParts = $permission->getParts(); + return $index; + } - $i = 0; - $partsCount = $this->getParts()->count(); - foreach ($otherParts as $otherPart) { - if ($partsCount - 1 < $i) { - return true; - } + $part = array_shift($parts); - if (! $this->parts->get($i)->contains(static::WILDCARD_TOKEN) - && ! $this->containsAll($this->parts->get($i), $otherPart)) { - return false; - } + if (blank($part)) { + throw WildcardPermissionNotProperlyFormatted::create($permission); + } - $i++; + if (! Str::contains($part, static::SUBPART_DELIMITER)) { + $index[$part] = $this->buildIndex( + $index[$part] ?? [], + $parts, + $permission, + ); } - for ($i; $i < $partsCount; $i++) { - if (! $this->parts->get($i)->contains(static::WILDCARD_TOKEN)) { - return false; + $subParts = explode(static::SUBPART_DELIMITER, $part); + + foreach ($subParts as $subPart) { + if (blank($subPart)) { + throw WildcardPermissionNotProperlyFormatted::create($permission); } + + $index[$subPart] = $this->buildIndex( + $index[$subPart] ?? [], + $parts, + $permission, + ); } - return true; + return $index; } - protected function containsAll(Collection $part, Collection $otherPart): bool + public function implies(string $permission, string $guardName, array $index): bool { - foreach ($otherPart->toArray() as $item) { - if (! $part->contains($item)) { - return false; - } + if (! array_key_exists($guardName, $index)) { + return false; } - return true; - } + $permission = explode(static::PART_DELIMITER, $permission); - public function getParts(): Collection - { - return $this->parts; + return $this->checkIndex($permission, $index[$guardName]); } - /** - * Sets the different parts and subparts from permission string. - */ - protected function setParts(): void + protected function checkIndex(array $permission, array $index): bool { - if (empty($this->permission) || $this->permission == null) { - throw WildcardPermissionNotProperlyFormatted::create($this->permission); + if (array_key_exists(strval(null), $index)) { + return true; } - $parts = collect(explode(static::PART_DELIMITER, $this->permission)); - - $parts->each(function ($item, $key) { - $subParts = collect(explode(static::SUBPART_DELIMITER, $item)); + if (empty($permission)) { + return false; + } - if ($subParts->isEmpty() || $subParts->contains('')) { - throw WildcardPermissionNotProperlyFormatted::create($this->permission); - } + $firstPermission = array_shift($permission); - $this->parts->add($subParts); - }); + if ( + array_key_exists($firstPermission, $index) && + $this->checkIndex($permission, $index[$firstPermission]) + ) { + return true; + } - if ($this->parts->isEmpty()) { - throw WildcardPermissionNotProperlyFormatted::create($this->permission); + if (array_key_exists(static::WILDCARD_TOKEN, $index)) { + return $this->checkIndex($permission, $index[static::WILDCARD_TOKEN]); } + + return false; } } From c73bac8103d72355ed0c12b101e8a4f59ef70fa3 Mon Sep 17 00:00:00 2001 From: drbyte Date: Sat, 17 Jun 2023 17:13:21 +0000 Subject: [PATCH 0706/1013] Fix styling --- src/PermissionRegistrar.php | 2 -- src/Traits/HasPermissions.php | 3 --- src/WildcardPermission.php | 1 - 3 files changed, 6 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index be1a0aa12..d94530a2a 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -9,10 +9,8 @@ use Illuminate\Contracts\Cache\Store; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Str; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Contracts\Role; -use Spatie\Permission\Exceptions\WildcardPermissionNotProperlyFormatted; class PermissionRegistrar { diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index b20953246..6b6c8c91e 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -6,7 +6,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Support\Arr; use Illuminate\Support\Collection; -use Illuminate\Support\Str; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Contracts\Role; use Spatie\Permission\Contracts\Wildcard; @@ -14,11 +13,9 @@ use Spatie\Permission\Exceptions\PermissionDoesNotExist; use Spatie\Permission\Exceptions\WildcardPermissionInvalidArgument; use Spatie\Permission\Exceptions\WildcardPermissionNotImplementsContract; -use Spatie\Permission\Exceptions\WildcardPermissionNotProperlyFormatted; use Spatie\Permission\Guard; use Spatie\Permission\PermissionRegistrar; use Spatie\Permission\WildcardPermission; -use Spatie\Permission\WildcardPermissionIndexChecker; trait HasPermissions { diff --git a/src/WildcardPermission.php b/src/WildcardPermission.php index 2c14f868c..a4287187f 100644 --- a/src/WildcardPermission.php +++ b/src/WildcardPermission.php @@ -3,7 +3,6 @@ namespace Spatie\Permission; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Collection; use Illuminate\Support\Str; use Spatie\Permission\Contracts\Wildcard; use Spatie\Permission\Exceptions\WildcardPermissionNotProperlyFormatted; From 7e16464d67899872e235a8b7ef6d51352389ca20 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 17 Jun 2023 14:45:03 -0400 Subject: [PATCH 0707/1013] Update foreign-key relationship details --- docs/prerequisites.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/prerequisites.md b/docs/prerequisites.md index beb6c87a8..fd50f9929 100644 --- a/docs/prerequisites.md +++ b/docs/prerequisites.md @@ -7,10 +7,6 @@ weight: 3 This package can be used in Laravel 6 or higher. Check the "Installing on Laravel" page for package versions compatible with various Laravel versions. -## Database with foreign-key relationship capability - -This package depends on cascading delete rules to enforce database integrity, so foreign-key support is required by your database engine. - ## User Model / Contract/Interface This package uses Laravel's Gate layer to provide Authorization capabilities. @@ -55,3 +51,8 @@ Thus in your AppServiceProvider you will need to set `Schema::defaultStringLengt This package expects the primary key of your `User` model to be an auto-incrementing `int`. If it is not, you may need to modify the `create_permission_tables` migration and/or modify the default configuration. See [https://spatie.be/docs/laravel-permission/advanced-usage/uuid](https://spatie.be/docs/laravel-permission/advanced-usage/uuid) for more information. +## Database foreign-key relationship support + +To enforce database integrity, this package uses foreign-key relationships with cascading deletes. This prevents data mismatch situations if database records are manipulated outside of this package. If your database engine does not support foreign-key relationships, then you will have to alter the migration files accordingly. + +This package does its own detaching of pivot records when deletes are called using provided package methods, so if your database does not support foreign keys then as long as you only use method calls provided by this package for managing related records, there should not be data integrity issues. From adb5923ef5168614f8e16947233e4225a701ac13 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 17 Jun 2023 18:09:21 -0400 Subject: [PATCH 0708/1013] Code formatting (rearrange order) --- src/PermissionRegistrar.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index d94530a2a..ffb12d87e 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -153,16 +153,6 @@ public function forgetWildcardPermissionIndex(?Model $record = null): void $this->wildcardPermissionsIndex = []; } - /** - * Clear already loaded permissions collection. - * This is only intended to be called by the PermissionServiceProvider on boot, - * so that long-running instances like Swoole don't keep old data in memory. - */ - public function clearPermissionsCollection(): void - { - $this->permissions = null; - } - public function getWildcardPermissionIndex(Model $record): array { if (isset($this->wildcardPermissionsIndex[get_class($record)][$record->getKey()])) { @@ -172,6 +162,16 @@ public function getWildcardPermissionIndex(Model $record): array return $this->wildcardPermissionsIndex[get_class($record)][$record->getKey()] = app($record->getWildcardClass(), ['record' => $record])->getIndex(); } + /** + * Clear already-loaded permissions collection. + * This is only intended to be called by the PermissionServiceProvider on boot, + * so that long-running instances like Octane or Swoole don't keep old data in memory. + */ + public function clearPermissionsCollection(): void + { + $this->permissions = null; + } + /** * @deprecated * From c92aa332982fb0c93351402b3c7d0e5b71e97487 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 17 Jun 2023 19:07:54 -0400 Subject: [PATCH 0709/1013] Suppress phpstan errors in Wildcard implementation Ref #2445 See comments in PR 2445 --- phpstan.neon.dist | 3 +++ 1 file changed, 3 insertions(+) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index b367f0713..f4b16051f 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -15,3 +15,6 @@ parameters: ignoreErrors: - '#Unsafe usage of new static#' + # wildcard permissions: + - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Model::getWildcardClass#' + - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Model::getAllPermissions#' From f7a90059ce703f390632d6925d5ab48c235a9ae1 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 17 Jun 2023 19:28:38 -0400 Subject: [PATCH 0710/1013] [Docs] More v6 notes --- docs/upgrading.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index 28d431eb8..fd5e7b6e5 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -35,11 +35,13 @@ Be sure to compare your custom models with originals to see what else may have c 2. If you have a custom Role model and (in the rare case that you might) have overridden the `hasPermissionTo()` method in it, you will need to update its method signature to `hasPermissionTo($permission, $guardName = null):bool`. See PR #2380. 3. Migrations will need to be upgraded. (They have been updated to anonymous-class syntax that was introduced in Laravel 8, AND some structural coding changes in the registrar class changed the way we extracted configuration settings in the migration files.) If you had not customized it from the original then replacing the contents of the file should be straightforward. Usually the only customization is if you've switched to UUIDs or customized MySQL index name lengths. -If you get the following error, it means your migration file needs upgrading: `Error: Access to undeclared static property Spatie\Permission\PermissionRegistrar::$pivotPermission` +**If you get the following error, it means your migration file needs upgrading: `Error: Access to undeclared static property Spatie\Permission\PermissionRegistrar::$pivotPermission`** 4. NOTE: For consistency with `PermissionMiddleware`, the `RoleOrPermissionMiddleware` has switched from only checking permissions provided by this package to using `canAny()` to check against any abilities registered by your application. This may have the effect of granting those other abilities (such as Super Admin) when using the `RoleOrPermissionMiddleware`, which previously would have failed silently. -5. Test suites. If you have tests which manually clear the permission cache and re-register permissions, you no longer need to call `\Spatie\Permission\PermissionRegistrar::class)->registerPermissions();`. Such calls MUST be deleted from your tests. +5. In the unlikely event that you have customized the Wildcard Permissions feature by extending the `WildcardPermission` model, please note that the public interface has changed and you will need to update your extended model with the new method signatures. + +6. Test suites. If you have tests which manually clear the permission cache and re-register permissions, you no longer need to call `\Spatie\Permission\PermissionRegistrar::class)->registerPermissions();`. Such calls MUST be deleted from your tests. ## Upgrading from v4 to v5 From 4475f05f2e1cb3fae51b2fb066815789b6110a4d Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 17 Jun 2023 21:38:44 -0400 Subject: [PATCH 0711/1013] [Docs] Update upgrade docs --- docs/upgrading.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index fd5e7b6e5..f44ec1172 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -15,13 +15,13 @@ ALL upgrades of this package should follow these steps: 3. Config file. Incorporate any changes to the permission.php config file, updating your existing file. (It may be easiest to make a backup copy of your existing file, re-publish it from this package, and then re-make your customizations to it.) -3. Models. If you have made any custom Models from this package into your own app, compare the old and new models and apply any relevant updates to your custom models. +4. Models. If you have made any custom Models from this package into your own app, compare the old and new models and apply any relevant updates to your custom models. -4. Custom Methods/Traits. If you have overridden any methods from this package's Traits, compare the old and new traits, and apply any relevant updates to your overridden methods. +5. Custom Methods/Traits. If you have overridden any methods from this package's Traits, compare the old and new traits, and apply any relevant updates to your overridden methods. -5. Apply any version-specific special updates as outlined below... +6. Apply any version-specific special updates as outlined below... -6. Review the changelog, which details all the changes: [CHANGELOG](https://github.com/spatie/laravel-permission/blob/main/CHANGELOG.md) +7. Review the changelog, which details all the changes: [CHANGELOG](https://github.com/spatie/laravel-permission/blob/main/CHANGELOG.md) and/or consult the [Release Notes](https://github.com/spatie/laravel-permission/releases) From cf670092d9e69e7538b2b0f1ef05f5cbf6af3386 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 26 Jun 2023 13:41:46 -0400 Subject: [PATCH 0712/1013] Example for adding 'description' fields --- docs/advanced-usage/extending.md | 47 +++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/docs/advanced-usage/extending.md b/docs/advanced-usage/extending.md index ded3621ec..3a86d0b52 100644 --- a/docs/advanced-usage/extending.md +++ b/docs/advanced-usage/extending.md @@ -3,6 +3,43 @@ title: Extending weight: 4 --- +## Adding fields to your models +You can add your own migrations to make changes to the role/permission tables, as you would for adding/changing fields in any other tables in your Laravel project. + +Following that, you can add any necessary logic for interacting with those fields into your custom/extended Models. + +Here is an example of adding a 'description' field to your Permissions and Roles tables: + +```sh +php artisan make:migration add_description_to_permissions_tables +``` +And in the migration file: +```php +public function up() +{ + Schema::table('permissions', function (Blueprint $table) { + $table->string('description')->nullable(); + }); + Schema::table('roles', function (Blueprint $table) { + $table->string('description')->nullable(); + }); +} +``` + +Semi-Related article: [Adding Extra Fields To Pivot Table](https://quickadminpanel.com/blog/laravel-belongstomany-add-extra-fields-to-pivot-table/) (video) + +## Adding a description to roles and permissions +A common question is "how do I add a description for my roles or permissions?". + +By default, a 'description' field is not included in this package, to keep the model memory usage low, because not every app has a need for displayed descriptions. + +But you are free to add it yourself if you wish. You can use the example above. + +### Multiple Language Descriptions + +If you need your 'description' to support multiple languages, simply use Laravel's built-in language features. You might prefer to rename the 'description' field in these migration examples from 'description' to 'description_key' for clarity. + + ## Extending User Models Laravel's authorization features are available in models which implement the `Illuminate\Foundation\Auth\Access\Authorizable` trait. @@ -58,13 +95,3 @@ In the rare case that you have need to REPLACE the existing `Role` or `Permissio - Your `Permission` model needs to implement the `Spatie\Permission\Contracts\Permission` contract - You need to update `config/permission.php` to specify your namespaced model - -## Adding fields to your models -You can add your own migrations to make changes to the role/permission tables, as you would for adding/changing fields in any other tables in your Laravel project. - -Following that, you can add any necessary logic for interacting with those fields into your custom/extended Models. - -Related article: [Adding Extra Fields To Pivot Table](https://quickadminpanel.com/blog/laravel-belongstomany-add-extra-fields-to-pivot-table/) (video) - - - From d8f46bea1d43607829d229e75e661da084089d85 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 29 Jun 2023 17:54:57 -0400 Subject: [PATCH 0713/1013] [Docs] Formatting Traded backticks for square-brackets so auto-generated index renders full section titles (otherwise it was truncating them, leading to confusion) --- docs/prerequisites.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/prerequisites.md b/docs/prerequisites.md index fd50f9929..78773b379 100644 --- a/docs/prerequisites.md +++ b/docs/prerequisites.md @@ -29,13 +29,13 @@ class User extends Authenticatable } ``` -## Must not have a `role` or `roles` property, nor a `roles()` method +## Must not have a [role] or [roles] property, nor a [roles()] method -Additionally, your `User` model/object MUST NOT have a `role` or `roles` property (or field in the database), nor a `roles()` method on it. Those will interfere with the properties and methods added by the `HasRoles` trait provided by this package, thus causing unexpected outcomes when this package's methods are used to inspect roles and permissions. +Your `User` model/object MUST NOT have a `role` or `roles` property (or field in the database by that name), nor a `roles()` method on it. Those will interfere with the properties and methods added by the `HasRoles` trait provided by this package, thus causing unexpected outcomes when this package's methods are used to inspect roles and permissions. -## Must not have a `permission` or `permissions` property, nor a `permissions()` method +## Must not have a [permission] or [permissions] property, nor a [permissions()] method -Similarly, your `User` model/object MUST NOT have a `permission` or `permissions` property (or field in the database), nor a `permissions()` method on it. Those will interfere with the properties and methods added by the `HasPermissions` trait provided by this package (which is invoked via the `HasRoles` trait). +Your `User` model/object MUST NOT have a `permission` or `permissions` property (or field in the database by that name), nor a `permissions()` method on it. Those will interfere with the properties and methods added by the `HasPermissions` trait provided by this package (which is invoked via the `HasRoles` trait). ## Config file From c0c5e2ed4d19734f39c9e0f2066343e6ff3bec2e Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 2 Jul 2023 21:02:53 -0400 Subject: [PATCH 0714/1013] [Docs] Add headings --- docs/basic-usage/basic-usage.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/basic-usage/basic-usage.md b/docs/basic-usage/basic-usage.md index 2db919ac1..56bdd04d8 100644 --- a/docs/basic-usage/basic-usage.md +++ b/docs/basic-usage/basic-usage.md @@ -3,6 +3,7 @@ title: Basic Usage weight: 1 --- +## Add The Trait First, add the `Spatie\Permission\Traits\HasRoles` trait to your `User` model(s): ```php @@ -17,6 +18,7 @@ class User extends Authenticatable } ``` +## Create A Permission This package allows for users to be associated with permissions and roles. Every role is associated with multiple permissions. A `Role` and a `Permission` are regular Eloquent models. They require a `name` and can be created like this: @@ -28,7 +30,7 @@ $role = Role::create(['name' => 'writer']); $permission = Permission::create(['name' => 'edit articles']); ``` - +## Assign A Permission To A Role A permission can be assigned to a role using either of these methods: ```php @@ -36,6 +38,7 @@ $role->givePermissionTo($permission); $permission->assignRole($role); ``` +## Sync Permissions To A Role Multiple permissions can be synced to a role using either of these methods: ```php @@ -43,6 +46,7 @@ $role->syncPermissions($permissions); $permission->syncRoles($roles); ``` +## Remove Permission From A Role A permission can be removed from a role using either of these methods: ```php @@ -50,8 +54,10 @@ $role->revokePermissionTo($permission); $permission->removeRole($role); ``` +## Guard Name If you're using multiple guards then the `guard_name` attribute must be set as well. Read about it in the [using multiple guards](./multiple-guards) section of the readme. +## Get Permissions For A User The `HasRoles` trait adds Eloquent relationships to your models, which can be accessed directly or used as a base query: ```php @@ -68,6 +74,7 @@ $permissions = $user->getAllPermissions(); $roles = $user->getRoleNames(); // Returns a collection ``` +## Scopes The `HasRoles` trait also adds a `role` scope to your models to scope the query to certain roles or permissions: ```php @@ -85,7 +92,7 @@ $users = User::permission('edit articles')->get(); // Returns only users with th The scope can accept a string, a `\Spatie\Permission\Models\Permission` object or an `\Illuminate\Support\Collection` object. -### Eloquent +## Eloquent Calls Since Role and Permission models are extended from Eloquent models, basic Eloquent calls can be used as well: ```php @@ -96,3 +103,10 @@ $users_without_any_roles = User::doesntHave('roles')->get(); $all_roles_except_a_and_b = Role::whereNotIn('name', ['role A', 'role B'])->get(); ``` +## Counting Users Having A Role +One way to count all users who have a certain role is by filtering the collection of all Users with their Roles: +```php +$superAdminCount = User::with('roles')->get()->filter( + fn ($user) => $user->roles->where('name', 'Super Admin')->toArray() +)->count(); +``` From e7d8cfb7da3263c6c8d70089536538ae69bf6681 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Jul 2023 05:37:40 +0000 Subject: [PATCH 0715/1013] Bump dependabot/fetch-metadata from 1.5.1 to 1.6.0 Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 1.5.1 to 1.6.0. - [Release notes](https://github.com/dependabot/fetch-metadata/releases) - [Commits](https://github.com/dependabot/fetch-metadata/compare/v1.5.1...v1.6.0) --- updated-dependencies: - dependency-name: dependabot/fetch-metadata dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/dependabot-auto-merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 3f60ce0f6..60183c521 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -13,7 +13,7 @@ jobs: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v1.5.1 + uses: dependabot/fetch-metadata@v1.6.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" compat-lookup: true From 816d83729bd4d4c0039e63553d57e333bad488a6 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Tue, 4 Jul 2023 08:37:50 -0500 Subject: [PATCH 0716/1013] Fix Eloquent Strictness on `permission:show` Command (#2458) --- src/Commands/Show.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Commands/Show.php b/src/Commands/Show.php index 0c08bd28a..418daaa65 100644 --- a/src/Commands/Show.php +++ b/src/Commands/Show.php @@ -20,6 +20,7 @@ public function handle() { $permissionClass = app(PermissionContract::class); $roleClass = app(RoleContract::class); + $teamsEnabled = config('permission.teams'); $team_key = config('permission.column_names.team_foreign_key'); $style = $this->argument('style') ?? 'default'; @@ -36,9 +37,12 @@ public function handle() $roles = $roleClass::whereGuardName($guard) ->with('permissions') - ->when(config('permission.teams'), fn ($q) => $q->orderBy($team_key)) + ->when($teamsEnabled, fn ($q) => $q->orderBy($team_key)) ->orderBy('name')->get()->mapWithKeys(fn ($role) => [ - $role->name.'_'.($role->$team_key ?: '') => ['permissions' => $role->permissions->pluck('id'), $team_key => $role->$team_key], + $role->name.'_'.($teamsEnabled ? ($role->$team_key ?: '') : '') => [ + 'permissions' => $role->permissions->pluck('id'), + $team_key => $teamsEnabled ? $role->$team_key : null, + ], ]); $permissions = $permissionClass::whereGuardName($guard)->orderBy('name')->pluck('name', 'id'); @@ -48,7 +52,7 @@ public function handle() )->prepend($permission) ); - if (config('permission.teams')) { + if ($teamsEnabled) { $teams = $roles->groupBy($team_key)->values()->map( fn ($group, $id) => new TableCell('Team ID: '.($id ?: 'NULL'), ['colspan' => $group->count()]) ); From bb1faba77a3b581331690bee0ee1deb6b7dae9ca Mon Sep 17 00:00:00 2001 From: drbyte Date: Tue, 4 Jul 2023 13:38:17 +0000 Subject: [PATCH 0717/1013] Fix styling --- tests/HasRolesTest.php | 18 +++++++++--------- tests/TestModels/TestRolePermissionsEnum.php | 12 ++++++------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 1f86ff47d..49a775ee3 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -623,15 +623,15 @@ public function it_returns_false_instead_of_an_exception_when_checking_against_a $this->assertFalse($this->testUser->hasAnyRole('This Role Does Not Even Exist', $this->testAdminRole)); } - /** @test */ - public function it_throws_an_exception_if_an_unsupported_type_is_passed_to_hasRoles() - { - $this->expectException(\TypeError::class); - - $this->testUser->hasRole(new class - { - }); - } + /** @test */ + public function it_throws_an_exception_if_an_unsupported_type_is_passed_to_hasRoles() + { + $this->expectException(\TypeError::class); + + $this->testUser->hasRole(new class + { + }); + } /** @test */ public function it_can_retrieve_role_names() diff --git a/tests/TestModels/TestRolePermissionsEnum.php b/tests/TestModels/TestRolePermissionsEnum.php index d5c73ebc1..858b7f937 100644 --- a/tests/TestModels/TestRolePermissionsEnum.php +++ b/tests/TestModels/TestRolePermissionsEnum.php @@ -44,13 +44,13 @@ enum TestRolePermissionsEnum: string public function label(): string { return match ($this) { - static::WRITER => 'Writers', - static::EDITOR => 'Editors', - static::USERMANAGER => 'User Managers', - static::ADMIN => 'Admins', + self::WRITER => 'Writers', + self::EDITOR => 'Editors', + self::USERMANAGER => 'User Managers', + self::ADMIN => 'Admins', - static::VIEWARTICLES => 'View Articles', - static::EDITARTICLES => 'Edit Articles', + self::VIEWARTICLES => 'View Articles', + self::EDITARTICLES => 'Edit Articles', }; } } From b70c71d42fb5c39e74411d0b3e3bdb2073ebd412 Mon Sep 17 00:00:00 2001 From: drbyte Date: Tue, 4 Jul 2023 13:51:45 +0000 Subject: [PATCH 0718/1013] Update CHANGELOG --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d07abe830..1a9a38279 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.10.2 - 2023-07-04 + +### What's Changed + +- Fix Eloquent Strictness on `permission:show` Command by @erikn69 in https://github.com/spatie/laravel-permission/pull/2457 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.10.1...5.10.2 + ## 5.10.1 - 2023-04-12 ### What's Changed @@ -609,6 +617,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -664,6 +673,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From c22493f98c3a198ea8f1951afcddc3cc28cb01d5 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 12 Jul 2023 15:06:05 -0400 Subject: [PATCH 0719/1013] Tidying some tests --- tests/HasRolesTest.php | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 49a775ee3..8dafa0e7b 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -395,7 +395,6 @@ public function it_can_scope_users_using_an_array() $user2->assignRole('testRole2'); $scopedUsers1 = User::role([$this->testUserRole])->get(); - $scopedUsers2 = User::role(['testRole', 'testRole2'])->get(); $this->assertEquals(1, $scopedUsers1->count()); @@ -407,16 +406,13 @@ public function it_can_scope_users_using_an_array_of_ids_and_names() { $user1 = User::create(['email' => 'user1@test.com']); $user2 = User::create(['email' => 'user2@test.com']); - $user1->assignRole($this->testUserRole); - $user2->assignRole('testRole2'); - $roleName = $this->testUserRole->name; - - $otherRoleId = app(Role::class)->findByName('testRole2')->getKey(); + $firstAssignedRoleName = $this->testUserRole->name; + $secondAssignedRoleId = app(Role::class)->findByName('testRole2')->getKey(); - $scopedUsers = User::role([$roleName, $otherRoleId])->get(); + $scopedUsers = User::role([$firstAssignedRoleName, $secondAssignedRoleId])->get(); $this->assertEquals(2, $scopedUsers->count()); } @@ -465,9 +461,9 @@ public function it_can_scope_against_a_specific_guard() $this->assertEquals(1, $scopedUsers1->count()); - $user3 = Admin::create(['email' => 'user1@test.com']); - $user4 = Admin::create(['email' => 'user1@test.com']); - $user5 = Admin::create(['email' => 'user2@test.com']); + $user3 = Admin::create(['email' => 'user3@test.com']); + $user4 = Admin::create(['email' => 'user4@test.com']); + $user5 = Admin::create(['email' => 'user5@test.com']); $testAdminRole2 = app(Role::class)->create(['name' => 'testAdminRole2', 'guard_name' => 'admin']); $user3->assignRole($this->testAdminRole); $user4->assignRole($this->testAdminRole); From 01bcb2fdae6ae7aaba9ce95769efdd010c5ae4a1 Mon Sep 17 00:00:00 2001 From: drbyte Date: Wed, 12 Jul 2023 19:06:42 +0000 Subject: [PATCH 0720/1013] Fix styling --- src/PermissionRegistrar.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index ffb12d87e..bc434e571 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -142,7 +142,7 @@ public function forgetCachedPermissions() return $this->cache->forget($this->cacheKey); } - public function forgetWildcardPermissionIndex(?Model $record = null): void + public function forgetWildcardPermissionIndex(Model $record = null): void { if ($record) { unset($this->wildcardPermissionsIndex[get_class($record)][$record->getKey()]); From bee67cbdc9708ca55b98dc48a1d1adba17e22530 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 13 Jul 2023 01:45:18 -0400 Subject: [PATCH 0721/1013] Update descriptions in default config file --- config/permission.php | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/config/permission.php b/config/permission.php index 5b6e184c3..c23acb9ce 100644 --- a/config/permission.php +++ b/config/permission.php @@ -98,39 +98,42 @@ /* * When set to true, the method for checking permissions will be registered on the gate. - * Set this to false, if you want to implement custom logic for checking permissions. + * Set this to false if you want to implement custom logic for checking permissions. */ 'register_permission_check_method' => true, /* - * When set to true the package implements teams using the 'team_foreign_key'. If you want - * the migrations to register the 'team_foreign_key', you must set this to true - * before doing the migration. If you already did the migration then you must make a new - * migration to also add 'team_foreign_key' to 'roles', 'model_has_roles', and - * 'model_has_permissions'(view the latest version of package's migration file) + * Teams Feature. + * When set to true the package implements teams using the 'team_foreign_key'. + * If you want the migrations to register the 'team_foreign_key', you must + * set this to true before doing the migration. + * If you already did the migration then you must make a new migration to also + * add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions' + * (view the latest version of this package's migration file) */ 'teams' => false, /* - * When set to true, the required permission names are added to the exception - * message. This could be considered an information leak in some contexts, so - * the default setting is false here for optimum safety. + * When set to true, the required permission names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. */ 'display_permission_in_exception' => false, /* - * When set to true, the required role names are added to the exception - * message. This could be considered an information leak in some contexts, so - * the default setting is false here for optimum safety. + * When set to true, the required role names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. */ 'display_role_in_exception' => false, /* * By default wildcard permission lookups are disabled. + * See documentation to understand supported syntax. */ 'enable_wildcard_permission' => false, From ed0de82322489520a426bcdd647e9df41e60393f Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 13 Jul 2023 22:55:46 -0400 Subject: [PATCH 0722/1013] Add withoutRole and withoutPermission scopes (#2463) This allows the inverse of the prior scopes to now allow finding users that do-not-have the specified role or permission Ref: #1037 --- docs/basic-usage/basic-usage.md | 10 ++- src/Traits/HasPermissions.php | 31 +++++++ src/Traits/HasRoles.php | 30 +++++++ tests/HasPermissionsTest.php | 61 ++++++++++++-- tests/HasRolesTest.php | 138 ++++++++++++++++++++++++++++++++ tests/TeamHasRolesTest.php | 4 + 6 files changed, 264 insertions(+), 10 deletions(-) diff --git a/docs/basic-usage/basic-usage.md b/docs/basic-usage/basic-usage.md index 56bdd04d8..ea0d9e172 100644 --- a/docs/basic-usage/basic-usage.md +++ b/docs/basic-usage/basic-usage.md @@ -75,18 +75,20 @@ $roles = $user->getRoleNames(); // Returns a collection ``` ## Scopes -The `HasRoles` trait also adds a `role` scope to your models to scope the query to certain roles or permissions: +The `HasRoles` trait also adds `role` and `withoutRole` scopes to your models to scope the query to certain roles or permissions: ```php $users = User::role('writer')->get(); // Returns only users with the role 'writer' +$nonEditors = User::withoutRole('editor')->get(); // Returns only users without the role 'editor' ``` -The `role` scope can accept a string, a `\Spatie\Permission\Models\Role` object or an `\Illuminate\Support\Collection` object. +The `role` and `withoutRole` scopes can accept a string, a `\Spatie\Permission\Models\Role` object or an `\Illuminate\Support\Collection` object. -The same trait also adds a scope to only get users that have a certain permission. +The same trait also adds scopes to only get users that have or don't have a certain permission. ```php $users = User::permission('edit articles')->get(); // Returns only users with the permission 'edit articles' (inherited or directly) +$usersWhoCannotEditArticles = User::withoutPermission('edit articles')->get(); // Returns all users without the permission 'edit articles' (inherited or directly) ``` The scope can accept a string, a `\Spatie\Permission\Models\Permission` object or an `\Illuminate\Support\Collection` object. @@ -97,7 +99,7 @@ Since Role and Permission models are extended from Eloquent models, basic Eloque ```php $all_users_with_all_their_roles = User::with('roles')->get(); -$all_users_with_all_direct_permissions = User::with('permissions')->get(); +$all_users_with_all_their_direct_permissions = User::with('permissions')->get(); $all_roles_in_database = Role::all()->pluck('name'); $users_without_any_roles = User::doesntHave('roles')->get(); $all_roles_except_a_and_b = Role::whereNotIn('name', ['role A', 'role B'])->get(); diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 6b6c8c91e..1404bb501 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -122,6 +122,37 @@ public function scopePermission(Builder $query, $permissions): Builder ); } + /** + * Scope the model query to only those without certain permissions, + * whether indirectly by role or by direct permission. + * + * @param string|int|array|Permission|Collection|\BackedEnum $permissions + */ + public function scopeWithoutPermission(Builder $query, $permissions, $debug = false): Builder + { + $permissions = $this->convertToPermissionModels($permissions); + + $permissionClass = $this->getPermissionClass(); + $permissionKey = (new $permissionClass())->getKeyName(); + $roleClass = is_a($this, Role::class) ? static::class : $this->getRoleClass(); + $roleKey = (new $roleClass())->getKeyName(); + + $rolesWithPermissions = is_a($this, Role::class) ? [] : array_unique( + array_reduce($permissions, fn ($result, $permission) => array_merge($result, $permission->roles->all()), []) + ); + + return $query->where(fn (Builder $query) => $query + ->whereDoesntHave('permissions', fn (Builder $subQuery) => $subQuery + ->whereIn(config('permission.table_names.permissions').".$permissionKey", \array_column($permissions, $permissionKey)) + ) + ->when(count($rolesWithPermissions), fn ($whenQuery) => $whenQuery + ->whereDoesntHave('roles', fn (Builder $subQuery) => $subQuery + ->whereIn(config('permission.table_names.roles').".$roleKey", \array_column($rolesWithPermissions, $roleKey)) + ) + ) + ); + } + /** * @param string|int|array|Permission|Collection|\BackedEnum $permissions * diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index ab637a795..8e9664cd7 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -95,6 +95,36 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder ); } + /** + * Scope the model query to only those without certain roles. + * + * @param string|int|array|Role|Collection $roles + * @param string $guard + */ + public function scopeWithoutRole(Builder $query, $roles, $guard = null): Builder + { + if ($roles instanceof Collection) { + $roles = $roles->all(); + } + + $roles = array_map(function ($role) use ($guard) { + if ($role instanceof Role) { + return $role; + } + + $method = is_int($role) || PermissionRegistrar::isUid($role) ? 'findById' : 'findByName'; + + return $this->getRoleClass()::{$method}($role, $guard ?: $this->getDefaultGuardName()); + }, Arr::wrap($roles)); + + $roleClass = $this->getRoleClass(); + $key = (new $roleClass())->getKeyName(); + + return $query->whereHas('roles', fn (Builder $subQuery) => $subQuery + ->whereNotIn(config('permission.table_names.roles').".$key", \array_column($roles, $key)) + ); + } + /** * Returns roles ids as array keys * diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index e5c9a4c85..0eeb961a9 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -87,123 +87,160 @@ public function it_can_scope_users_using_enums() $enum2 = TestModels\TestRolePermissionsEnum::EDITARTICLES; $permission1 = app(Permission::class)->findOrCreate($enum1->value, 'web'); $permission2 = app(Permission::class)->findOrCreate($enum2->value, 'web'); + + User::all()->each(fn ($item) => $item->delete()); $user1 = User::create(['email' => 'user1@test.com']); $user2 = User::create(['email' => 'user2@test.com']); + $user3 = User::create(['email' => 'user3@test.com']); $user1->givePermissionTo([$enum1, $enum2]); $this->testUserRole->givePermissionTo($enum2); $user2->assignRole('testRole'); $scopedUsers1 = User::permission($enum2)->get(); $scopedUsers2 = User::permission([$enum1])->get(); + $scopedUsers3 = User::withoutPermission([$enum1])->get(); + $scopedUsers4 = User::withoutPermission([$enum2])->get(); $this->assertEquals(2, $scopedUsers1->count()); $this->assertEquals(1, $scopedUsers2->count()); + $this->assertEquals(2, $scopedUsers3->count()); + $this->assertEquals(1, $scopedUsers4->count()); } /** @test */ public function it_can_scope_users_using_a_string() { + User::all()->each(fn ($item) => $item->delete()); $user1 = User::create(['email' => 'user1@test.com']); $user2 = User::create(['email' => 'user2@test.com']); + $user3 = User::create(['email' => 'user3@test.com']); $user1->givePermissionTo(['edit-articles', 'edit-news']); $this->testUserRole->givePermissionTo('edit-articles'); $user2->assignRole('testRole'); $scopedUsers1 = User::permission('edit-articles')->get(); $scopedUsers2 = User::permission(['edit-news'])->get(); + $scopedUsers3 = User::withoutPermission('edit-news')->get(); $this->assertEquals(2, $scopedUsers1->count()); $this->assertEquals(1, $scopedUsers2->count()); + $this->assertEquals(2, $scopedUsers3->count()); } /** @test */ public function it_can_scope_users_using_a_int() { + User::all()->each(fn ($item) => $item->delete()); $user1 = User::create(['email' => 'user1@test.com']); $user2 = User::create(['email' => 'user2@test.com']); + $user3 = User::create(['email' => 'user3@test.com']); $user1->givePermissionTo([1, 2]); $this->testUserRole->givePermissionTo(1); $user2->assignRole('testRole'); $scopedUsers1 = User::permission(1)->get(); $scopedUsers2 = User::permission([2])->get(); + $scopedUsers3 = User::withoutPermission([2])->get(); $this->assertEquals(2, $scopedUsers1->count()); $this->assertEquals(1, $scopedUsers2->count()); + $this->assertEquals(2, $scopedUsers3->count()); } /** @test */ public function it_can_scope_users_using_an_array() { + User::all()->each(fn ($item) => $item->delete()); $user1 = User::create(['email' => 'user1@test.com']); $user2 = User::create(['email' => 'user2@test.com']); + $user3 = User::create(['email' => 'user3@test.com']); $user1->givePermissionTo(['edit-articles', 'edit-news']); $this->testUserRole->givePermissionTo('edit-articles'); $user2->assignRole('testRole'); + $user3->assignRole('testRole2'); $scopedUsers1 = User::permission(['edit-articles', 'edit-news'])->get(); $scopedUsers2 = User::permission(['edit-news'])->get(); + $scopedUsers3 = User::withoutPermission(['edit-news'])->get(); $this->assertEquals(2, $scopedUsers1->count()); $this->assertEquals(1, $scopedUsers2->count()); + $this->assertEquals(2, $scopedUsers3->count()); } /** @test */ public function it_can_scope_users_using_a_collection() { + User::all()->each(fn ($item) => $item->delete()); $user1 = User::create(['email' => 'user1@test.com']); $user2 = User::create(['email' => 'user2@test.com']); + $user3 = User::create(['email' => 'user3@test.com']); $user1->givePermissionTo(['edit-articles', 'edit-news']); $this->testUserRole->givePermissionTo('edit-articles'); $user2->assignRole('testRole'); + $user3->assignRole('testRole2'); $scopedUsers1 = User::permission(collect(['edit-articles', 'edit-news']))->get(); $scopedUsers2 = User::permission(collect(['edit-news']))->get(); + $scopedUsers3 = User::withoutPermission(collect(['edit-news']))->get(); $this->assertEquals(2, $scopedUsers1->count()); $this->assertEquals(1, $scopedUsers2->count()); + $this->assertEquals(2, $scopedUsers3->count()); } /** @test */ public function it_can_scope_users_using_an_object() { + User::all()->each(fn ($item) => $item->delete()); $user1 = User::create(['email' => 'user1@test.com']); $user1->givePermissionTo($this->testUserPermission->name); $scopedUsers1 = User::permission($this->testUserPermission)->get(); $scopedUsers2 = User::permission([$this->testUserPermission])->get(); $scopedUsers3 = User::permission(collect([$this->testUserPermission]))->get(); + $scopedUsers4 = User::withoutPermission(collect([$this->testUserPermission]))->get(); $this->assertEquals(1, $scopedUsers1->count()); $this->assertEquals(1, $scopedUsers2->count()); $this->assertEquals(1, $scopedUsers3->count()); + $this->assertEquals(0, $scopedUsers4->count()); } /** @test */ - public function it_can_scope_users_without_permissions_only_role() + public function it_can_scope_users_without_direct_permissions_only_role() { + User::all()->each(fn ($item) => $item->delete()); $user1 = User::create(['email' => 'user1@test.com']); $user2 = User::create(['email' => 'user2@test.com']); + $user3 = User::create(['email' => 'user3@test.com']); $this->testUserRole->givePermissionTo('edit-articles'); $user1->assignRole('testRole'); $user2->assignRole('testRole'); + $user3->assignRole('testRole2'); - $scopedUsers = User::permission('edit-articles')->get(); + $scopedUsers1 = User::permission('edit-articles')->get(); + $scopedUsers2 = User::withoutPermission('edit-articles')->get(); - $this->assertEquals(2, $scopedUsers->count()); + $this->assertEquals(2, $scopedUsers1->count()); + $this->assertEquals(1, $scopedUsers2->count()); } /** @test */ - public function it_can_scope_users_without_permissions_only_permission() + public function it_can_scope_users_with_only_direct_permission() { + User::all()->each(fn ($item) => $item->delete()); $user1 = User::create(['email' => 'user1@test.com']); $user2 = User::create(['email' => 'user2@test.com']); + $user3 = User::create(['email' => 'user3@test.com']); $user1->givePermissionTo(['edit-news']); $user2->givePermissionTo(['edit-articles', 'edit-news']); - $scopedUsers = User::permission('edit-news')->get(); + $scopedUsers1 = User::permission('edit-news')->get(); + $scopedUsers2 = User::withoutPermission('edit-news')->get(); - $this->assertEquals(2, $scopedUsers->count()); + $this->assertEquals(2, $scopedUsers1->count()); + $this->assertEquals(1, $scopedUsers2->count()); } /** @test */ @@ -252,6 +289,10 @@ public function it_throws_an_exception_when_trying_to_scope_a_non_existing_permi $this->expectException(PermissionDoesNotExist::class); User::permission('not defined permission')->get(); + + $this->expectException(PermissionDoesNotExist::class); + + User::withoutPermission('not defined permission')->get(); } /** @test */ @@ -261,9 +302,17 @@ public function it_throws_an_exception_when_trying_to_scope_a_permission_from_an User::permission('testAdminPermission')->get(); + $this->expectException(PermissionDoesNotExist::class); + + User::withoutPermission('testAdminPermission')->get(); + $this->expectException(GuardDoesNotMatch::class); User::permission($this->testAdminPermission)->get(); + + $this->expectException(GuardDoesNotMatch::class); + + User::withoutPermission($this->testAdminPermission)->get(); } /** @test */ diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 8dafa0e7b..870177fd6 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -386,6 +386,21 @@ public function it_can_scope_users_using_a_string() $this->assertEquals(1, $scopedUsers->count()); } + /** @test */ + public function it_can_withoutscope_users_using_a_string() + { + $user1 = User::create(['email' => 'user1@test.com']); + $user2 = User::create(['email' => 'user2@test.com']); + $user3 = User::create(['email' => 'user3@test.com']); + $user1->assignRole('testRole'); + $user2->assignRole('testRole2'); + $user3->assignRole('testRole2'); + + $scopedUsers = User::withoutRole('testRole2')->get(); + + $this->assertEquals(1, $scopedUsers->count()); + } + /** @test */ public function it_can_scope_users_using_an_array() { @@ -401,6 +416,23 @@ public function it_can_scope_users_using_an_array() $this->assertEquals(2, $scopedUsers2->count()); } + /** @test */ + public function it_can_withoutscope_users_using_an_array() + { + $user1 = User::create(['email' => 'user1@test.com']); + $user2 = User::create(['email' => 'user2@test.com']); + $user3 = User::create(['email' => 'user3@test.com']); + $user1->assignRole($this->testUserRole); + $user2->assignRole('testRole2'); + $user3->assignRole('testRole2'); + + $scopedUsers1 = User::withoutRole([$this->testUserRole])->get(); + $scopedUsers2 = User::withoutRole([$this->testUserRole->name, 'testRole2'])->get(); + + $this->assertEquals(2, $scopedUsers1->count()); + $this->assertEquals(0, $scopedUsers2->count()); + } + /** @test */ public function it_can_scope_users_using_an_array_of_ids_and_names() { @@ -417,6 +449,26 @@ public function it_can_scope_users_using_an_array_of_ids_and_names() $this->assertEquals(2, $scopedUsers->count()); } + /** @test */ + public function it_can_withoutscope_users_using_an_array_of_ids_and_names() + { + app(Role::class)->create(['name' => 'testRole3']); + + $user1 = User::create(['email' => 'user1@test.com']); + $user2 = User::create(['email' => 'user2@test.com']); + $user3 = User::create(['email' => 'user3@test.com']); + $user1->assignRole($this->testUserRole); + $user2->assignRole('testRole2'); + $user3->assignRole('testRole2'); + + $firstAssignedRoleName = $this->testUserRole->name; + $unassignedRoleId = app(Role::class)->findByName('testRole3')->getKey(); + + $scopedUsers = User::withoutRole([$firstAssignedRoleName, $unassignedRoleId])->get(); + + $this->assertEquals(2, $scopedUsers->count()); + } + /** @test */ public function it_can_scope_users_using_a_collection() { @@ -432,6 +484,25 @@ public function it_can_scope_users_using_a_collection() $this->assertEquals(2, $scopedUsers2->count()); } + /** @test */ + public function it_can_withoutscope_users_using_a_collection() + { + app(Role::class)->create(['name' => 'testRole3']); + + $user1 = User::create(['email' => 'user1@test.com']); + $user2 = User::create(['email' => 'user2@test.com']); + $user3 = User::create(['email' => 'user3@test.com']); + $user1->assignRole($this->testUserRole); + $user2->assignRole('testRole'); + $user3->assignRole('testRole2'); + + $scopedUsers1 = User::withoutRole([$this->testUserRole])->get(); + $scopedUsers2 = User::withoutRole(collect(['testRole', 'testRole3']))->get(); + + $this->assertEquals(1, $scopedUsers1->count()); + $this->assertEquals(1, $scopedUsers2->count()); + } + /** @test */ public function it_can_scope_users_using_an_object() { @@ -449,6 +520,25 @@ public function it_can_scope_users_using_an_object() $this->assertEquals(1, $scopedUsers3->count()); } + /** @test */ + public function it_can_withoutscope_users_using_an_object() + { + $user1 = User::create(['email' => 'user1@test.com']); + $user2 = User::create(['email' => 'user2@test.com']); + $user3 = User::create(['email' => 'user3@test.com']); + $user1->assignRole($this->testUserRole); + $user2->assignRole('testRole2'); + $user3->assignRole('testRole2'); + + $scopedUsers1 = User::withoutRole($this->testUserRole)->get(); + $scopedUsers2 = User::withoutRole([$this->testUserRole])->get(); + $scopedUsers3 = User::withoutRole(collect([$this->testUserRole]))->get(); + + $this->assertEquals(2, $scopedUsers1->count()); + $this->assertEquals(2, $scopedUsers2->count()); + $this->assertEquals(2, $scopedUsers3->count()); + } + /** @test */ public function it_can_scope_against_a_specific_guard() { @@ -475,6 +565,34 @@ public function it_can_scope_against_a_specific_guard() $this->assertEquals(1, $scopedUsers3->count()); } + /** @test */ + public function it_can_withoutscope_against_a_specific_guard() + { + $user1 = User::create(['email' => 'user1@test.com']); + $user2 = User::create(['email' => 'user2@test.com']); + $user3 = User::create(['email' => 'user3@test.com']); + $user1->assignRole('testRole'); + $user2->assignRole('testRole2'); + $user3->assignRole('testRole2'); + + $scopedUsers1 = User::withoutRole('testRole', 'web')->get(); + + $this->assertEquals(2, $scopedUsers1->count()); + + $user4 = Admin::create(['email' => 'user4@test.com']); + $user5 = Admin::create(['email' => 'user5@test.com']); + $user6 = Admin::create(['email' => 'user6@test.com']); + $testAdminRole2 = app(Role::class)->create(['name' => 'testAdminRole2', 'guard_name' => 'admin']); + $user4->assignRole($this->testAdminRole); + $user5->assignRole($this->testAdminRole); + $user6->assignRole($testAdminRole2); + $scopedUsers2 = Admin::withoutRole('testAdminRole', 'admin')->get(); + $scopedUsers3 = Admin::withoutRole('testAdminRole2', 'admin')->get(); + + $this->assertEquals(1, $scopedUsers2->count()); + $this->assertEquals(2, $scopedUsers3->count()); + } + /** @test */ public function it_throws_an_exception_when_trying_to_scope_a_role_from_another_guard() { @@ -487,6 +605,18 @@ public function it_throws_an_exception_when_trying_to_scope_a_role_from_another_ User::role($this->testAdminRole)->get(); } + /** @test */ + public function it_throws_an_exception_when_trying_to_call_withoutscope_on_a_role_from_another_guard() + { + $this->expectException(RoleDoesNotExist::class); + + User::withoutRole('testAdminRole')->get(); + + $this->expectException(GuardDoesNotMatch::class); + + User::withoutRole($this->testAdminRole)->get(); + } + /** @test */ public function it_throws_an_exception_when_trying_to_scope_a_non_existing_role() { @@ -495,6 +625,14 @@ public function it_throws_an_exception_when_trying_to_scope_a_non_existing_role( User::role('role not defined')->get(); } + /** @test */ + public function it_throws_an_exception_when_trying_to_use_withoutscope_on_a_non_existing_role() + { + $this->expectException(RoleDoesNotExist::class); + + User::withoutRole('role not defined')->get(); + } + /** @test */ public function it_can_determine_that_a_user_has_one_of_the_given_roles() { diff --git a/tests/TeamHasRolesTest.php b/tests/TeamHasRolesTest.php index e67010009..31ba19f53 100644 --- a/tests/TeamHasRolesTest.php +++ b/tests/TeamHasRolesTest.php @@ -133,15 +133,19 @@ public function it_can_scope_users_on_different_teams() setPermissionsTeamId(2); $scopedUsers1Team1 = User::role($this->testUserRole)->get(); $scopedUsers2Team1 = User::role(['testRole', 'testRole2'])->get(); + $scopedUsers3Team1 = User::withoutRole('testRole')->get(); $this->assertEquals(1, $scopedUsers1Team1->count()); $this->assertEquals(2, $scopedUsers2Team1->count()); + $this->assertEquals(1, $scopedUsers3Team1->count()); setPermissionsTeamId(1); $scopedUsers1Team2 = User::role($this->testUserRole)->get(); $scopedUsers2Team2 = User::role('testRole2')->get(); + $scopedUsers3Team2 = User::withoutRole('testRole')->get(); $this->assertEquals(1, $scopedUsers1Team2->count()); $this->assertEquals(0, $scopedUsers2Team2->count()); + $this->assertEquals(0, $scopedUsers3Team2->count()); } } From bf4b198e6033234460028c982862c60787c40f72 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 16 Jul 2023 16:33:19 -0400 Subject: [PATCH 0723/1013] Fix withoutRole scope added in #2463 Ref #2463 Ref #1037 --- src/Traits/HasRoles.php | 4 ++-- tests/HasRolesTest.php | 7 +++++++ tests/TeamHasRolesTest.php | 3 ++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 8e9664cd7..273205a51 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -120,8 +120,8 @@ public function scopeWithoutRole(Builder $query, $roles, $guard = null): Builder $roleClass = $this->getRoleClass(); $key = (new $roleClass())->getKeyName(); - return $query->whereHas('roles', fn (Builder $subQuery) => $subQuery - ->whereNotIn(config('permission.table_names.roles').".$key", \array_column($roles, $key)) + return $query->whereDoesntHave('roles', fn (Builder $subQuery) => $subQuery + ->whereIn(config('permission.table_names.roles').".$key", \array_column($roles, $key)) ); } diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 870177fd6..1e0c38c77 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -389,6 +389,7 @@ public function it_can_scope_users_using_a_string() /** @test */ public function it_can_withoutscope_users_using_a_string() { + User::all()->each(fn ($item) => $item->delete()); $user1 = User::create(['email' => 'user1@test.com']); $user2 = User::create(['email' => 'user2@test.com']); $user3 = User::create(['email' => 'user3@test.com']); @@ -419,6 +420,7 @@ public function it_can_scope_users_using_an_array() /** @test */ public function it_can_withoutscope_users_using_an_array() { + User::all()->each(fn ($item) => $item->delete()); $user1 = User::create(['email' => 'user1@test.com']); $user2 = User::create(['email' => 'user2@test.com']); $user3 = User::create(['email' => 'user3@test.com']); @@ -454,6 +456,7 @@ public function it_can_withoutscope_users_using_an_array_of_ids_and_names() { app(Role::class)->create(['name' => 'testRole3']); + User::all()->each(fn ($item) => $item->delete()); $user1 = User::create(['email' => 'user1@test.com']); $user2 = User::create(['email' => 'user2@test.com']); $user3 = User::create(['email' => 'user3@test.com']); @@ -489,6 +492,7 @@ public function it_can_withoutscope_users_using_a_collection() { app(Role::class)->create(['name' => 'testRole3']); + User::all()->each(fn ($item) => $item->delete()); $user1 = User::create(['email' => 'user1@test.com']); $user2 = User::create(['email' => 'user2@test.com']); $user3 = User::create(['email' => 'user3@test.com']); @@ -523,6 +527,7 @@ public function it_can_scope_users_using_an_object() /** @test */ public function it_can_withoutscope_users_using_an_object() { + User::all()->each(fn ($item) => $item->delete()); $user1 = User::create(['email' => 'user1@test.com']); $user2 = User::create(['email' => 'user2@test.com']); $user3 = User::create(['email' => 'user3@test.com']); @@ -568,6 +573,7 @@ public function it_can_scope_against_a_specific_guard() /** @test */ public function it_can_withoutscope_against_a_specific_guard() { + User::all()->each(fn ($item) => $item->delete()); $user1 = User::create(['email' => 'user1@test.com']); $user2 = User::create(['email' => 'user2@test.com']); $user3 = User::create(['email' => 'user3@test.com']); @@ -579,6 +585,7 @@ public function it_can_withoutscope_against_a_specific_guard() $this->assertEquals(2, $scopedUsers1->count()); + Admin::all()->each(fn ($item) => $item->delete()); $user4 = Admin::create(['email' => 'user4@test.com']); $user5 = Admin::create(['email' => 'user5@test.com']); $user6 = Admin::create(['email' => 'user6@test.com']); diff --git a/tests/TeamHasRolesTest.php b/tests/TeamHasRolesTest.php index 31ba19f53..16d8cdd6d 100644 --- a/tests/TeamHasRolesTest.php +++ b/tests/TeamHasRolesTest.php @@ -120,6 +120,7 @@ public function it_can_sync_or_remove_roles_without_detach_on_different_teams() /** @test */ public function it_can_scope_users_on_different_teams() { + User::all()->each(fn ($item) => $item->delete()); $user1 = User::create(['email' => 'user1@test.com']); $user2 = User::create(['email' => 'user2@test.com']); @@ -146,6 +147,6 @@ public function it_can_scope_users_on_different_teams() $this->assertEquals(1, $scopedUsers1Team2->count()); $this->assertEquals(0, $scopedUsers2Team2->count()); - $this->assertEquals(0, $scopedUsers3Team2->count()); + $this->assertEquals(1, $scopedUsers3Team2->count()); } } From 299fe4beb7e6f77079c9642e92bbd887d8736b6f Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 16 Jul 2023 16:34:46 -0400 Subject: [PATCH 0724/1013] Add Enum support for Role and WithoutRole scopes Ref #2391 --- src/Traits/HasRoles.php | 14 +++++++++++--- tests/HasRolesTest.php | 31 +++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 273205a51..829afde22 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -68,7 +68,7 @@ public function roles(): BelongsToMany /** * Scope the model query to certain roles only. * - * @param string|int|array|Role|Collection $roles + * @param string|int|array|Role|Collection|\BackedEnum $roles * @param string $guard */ public function scopeRole(Builder $query, $roles, $guard = null): Builder @@ -82,6 +82,10 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder return $role; } + if ($role instanceof \BackedEnum) { + $role = $role->value; + } + $method = is_int($role) || PermissionRegistrar::isUid($role) ? 'findById' : 'findByName'; return $this->getRoleClass()::{$method}($role, $guard ?: $this->getDefaultGuardName()); @@ -98,7 +102,7 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder /** * Scope the model query to only those without certain roles. * - * @param string|int|array|Role|Collection $roles + * @param string|int|array|Role|Collection|\BackedEnum $roles * @param string $guard */ public function scopeWithoutRole(Builder $query, $roles, $guard = null): Builder @@ -112,6 +116,10 @@ public function scopeWithoutRole(Builder $query, $roles, $guard = null): Builder return $role; } + if ($role instanceof \BackedEnum) { + $role = $role->value; + } + $method = is_int($role) || PermissionRegistrar::isUid($role) ? 'findById' : 'findByName'; return $this->getRoleClass()::{$method}($role, $guard ?: $this->getDefaultGuardName()); @@ -128,7 +136,7 @@ public function scopeWithoutRole(Builder $query, $roles, $guard = null): Builder /** * Returns roles ids as array keys * - * @param array|string|int|Role|Collection $roles + * @param array|string|int|Role|Collection|\BackedEnum $roles */ private function collectRoles(...$roles): array { diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 1e0c38c77..ec7bccc72 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -70,6 +70,37 @@ public function it_can_assign_and_remove_a_role_using_enums() $this->assertFalse($this->testUser->hasRole($enum1)); } + /** + * @test + * + * @requires PHP >= 8.1 + */ + public function it_can_scope_a_role_using_enums() + { + $enum1 = TestModels\TestRolePermissionsEnum::USERMANAGER; + $enum2 = TestModels\TestRolePermissionsEnum::WRITER; + $role1 = app(Role::class)->findOrCreate($enum1->value, 'web'); + $role2 = app(Role::class)->findOrCreate($enum2->value, 'web'); + + User::all()->each(fn ($item) => $item->delete()); + $user1 = User::create(['email' => 'user1@test.com']); + $user2 = User::create(['email' => 'user2@test.com']); + $user3 = User::create(['email' => 'user3@test.com']); + + // assign only one user to a role + $user2->assignRole($enum1); + $this->assertTrue($user2->hasRole($enum1)); + $this->assertFalse($user2->hasRole($enum2)); + + $scopedUsers1 = User::role($enum1)->get(); + $scopedUsers2 = User::role($enum2)->get(); + $scopedUsers3 = User::withoutRole($enum2)->get(); + + $this->assertEquals(1, $scopedUsers1->count()); + $this->assertEquals(0, $scopedUsers2->count()); + $this->assertEquals(3, $scopedUsers3->count()); + } + /** @test */ public function it_can_assign_and_remove_a_role() { From 484a79d5da0adaf422e8fa0a62c277e06d935962 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 23 Jul 2023 14:48:20 -0400 Subject: [PATCH 0725/1013] Fix Show command Co-authored-by: erikn69 --- src/Commands/Show.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/Show.php b/src/Commands/Show.php index 418daaa65..e20a371ed 100644 --- a/src/Commands/Show.php +++ b/src/Commands/Show.php @@ -67,7 +67,7 @@ public function handle() return implode('_', $name); }) - ->prepend('')->toArray(), + ->prepend(new TableCell(''))->toArray(), ), $body->toArray(), $style From 2dc2787dd8b0b6ccba2e40a7620a0a1653f39ec3 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 23 Jul 2023 14:58:49 -0400 Subject: [PATCH 0726/1013] Document the config override for Wildcard class name --- config/permission.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/permission.php b/config/permission.php index c23acb9ce..b4407316b 100644 --- a/config/permission.php +++ b/config/permission.php @@ -138,6 +138,12 @@ 'enable_wildcard_permission' => false, + /* + * The class to use for interpreting wildcard permissions. + * If you need to modify delimiters, override the class and specify its name here. + */ + // 'permission.wildcard_permission' => Spatie\Permission\WildcardPermission::class, + 'cache' => [ /* From 04a9592eb5a954511b1a044a3775ee076728102d Mon Sep 17 00:00:00 2001 From: SuperDJ <6484766+SuperDJ@users.noreply.github.com> Date: Wed, 26 Jul 2023 22:29:25 +0200 Subject: [PATCH 0727/1013] Adding the ability to use package with service-to-service Passport client (#2467) * Adding the ability to use package with service-to-service Passport clients Co-authored-by: SuperDJ Co-authored-by: erikn69 --- .github/workflows/run-tests-L8.yml | 4 + composer.json | 1 + config/permission.php | 7 + docs/basic-usage/passport.md | 53 +++++++ phpunit.xml.dist | 2 + src/Guard.php | 32 ++++ src/Middlewares/PermissionMiddleware.php | 12 +- src/Middlewares/RoleMiddleware.php | 12 +- .../RoleOrPermissionMiddleware.php | 13 +- tests/PermissionMiddlewareTest.php | 140 ++++++++++++++++++ tests/RoleMiddlewareTest.php | 123 +++++++++++++++ tests/RoleOrPermissionMiddlewareTest.php | 79 ++++++++++ tests/TestCase.php | 51 ++++++- tests/TestModels/Client.php | 21 +++ 14 files changed, 538 insertions(+), 12 deletions(-) create mode 100644 docs/basic-usage/passport.md create mode 100644 tests/TestModels/Client.php diff --git a/.github/workflows/run-tests-L8.yml b/.github/workflows/run-tests-L8.yml index dd9d0c446..4d8f8e774 100644 --- a/.github/workflows/run-tests-L8.yml +++ b/.github/workflows/run-tests-L8.yml @@ -40,6 +40,10 @@ jobs: extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv coverage: none + - name: Install dependencies (remove passport) + run: composer remove --dev laravel/passport --no-interaction --no-update + if: matrix.laravel == '8.*' + - name: Install dependencies run: | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:>=4.3.4" "mockery/mockery:^1.3.2" "nesbot/carbon:>=2.62.1" --no-interaction --no-update diff --git a/composer.json b/composer.json index 9ea458849..4e31db50e 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ "illuminate/database": "^8.0|^9.0|^10.0" }, "require-dev": { + "laravel/passport": "^11.0", "orchestra/testbench": "^6.0|^7.0|^8.0", "phpunit/phpunit": "^9.4" }, diff --git a/config/permission.php b/config/permission.php index b4407316b..b98b24148 100644 --- a/config/permission.php +++ b/config/permission.php @@ -115,6 +115,13 @@ 'teams' => false, + /* + * Passport Client Credentials Grant + * When set to true the package will use Passports Client to check permissions + */ + + 'use_passport_client_credentials' => false, + /* * When set to true, the required permission names are added to exception messages. * This could be considered an information leak in some contexts, so the default diff --git a/docs/basic-usage/passport.md b/docs/basic-usage/passport.md new file mode 100644 index 000000000..a5a007009 --- /dev/null +++ b/docs/basic-usage/passport.md @@ -0,0 +1,53 @@ +--- +title: Passport Client Credentials Grant usage +weight: 12 +--- + +**NOTE** currently this only works for Laravel 9 and Passport 11 and newer. + +## Install Passport +First of all make sure to have Passport installed as described in the [Laravel documentation](https://laravel.com/docs/master/passport). + +## Extend the Client model +After installing the Passport package we need to extend Passports Client model. +The extended Client model should look like something as shown below. + +```php +use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; +use Illuminate\Foundation\Auth\Access\Authorizable; +use Laravel\Passport\Client as BaseClient; +use Spatie\Permission\Traits\HasRoles; + +class Client extends BaseClient implements AuthorizableContract +{ + use HasRoles; + use Authorizable; + + public $guard_name = 'api'; + + // or + + public function guardName() + { + return 'api' + } +} +``` + +You need to extend the Client model to make it possible to add the required traits and properties/ methods. +The extended Client should either provide a `$guard_name` property or a `guardName()` method. +They should return a string that matches the [configured](https://laravel.com/docs/master/passport#installation) guard name for the passport driver. + +## Middleware +All middlewares provided by this package work with the Client. + +Do make sure that you only wrap your routes in the [`client`](https://laravel.com/docs/master/passport#via-middleware) middleware and not the `auth:api` middleware as well. +Wrapping routes in the `auth:api` middleware currently does not work for the Client Credentials Grant. + +## Config +Finally, update the config file as well. Setting `use_passport_client_credentials` to `true` will make sure that the right checks are performed. + +```php +// config/permission.php +'use_passport_client_credentials' => true, +``` diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 6cd82acfb..1e08d1014 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -12,5 +12,7 @@ + + diff --git a/src/Guard.php b/src/Guard.php index b5904cc4d..dd5023148 100644 --- a/src/Guard.php +++ b/src/Guard.php @@ -2,8 +2,10 @@ namespace Spatie\Permission; +use Illuminate\Contracts\Auth\Access\Authorizable; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Auth; class Guard { @@ -72,4 +74,34 @@ public static function getDefaultName($class): string return $possible_guards->first() ?: $default; } + + /** + * Lookup a passport guard + */ + public static function getPassportClient($guard): ?Authorizable + { + $guards = collect(config('auth.guards'))->where('driver', 'passport'); + + if (! $guards->count()) { + return null; + } + + $authGuard = Auth::guard($guards->keys()[0]); + + if (! \method_exists($authGuard, 'client')) { + return null; + } + + $client = $authGuard->client(); + + if (! $guard || ! $client) { + return $client; + } + + if (self::getNames($client)->contains($guard)) { + return $client; + } + + return null; + } } diff --git a/src/Middlewares/PermissionMiddleware.php b/src/Middlewares/PermissionMiddleware.php index ca37cb672..7d303261c 100644 --- a/src/Middlewares/PermissionMiddleware.php +++ b/src/Middlewares/PermissionMiddleware.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Support\Facades\Auth; use Spatie\Permission\Exceptions\UnauthorizedException; +use Spatie\Permission\Guard; class PermissionMiddleware { @@ -12,11 +13,16 @@ public function handle($request, Closure $next, $permission, $guard = null) { $authGuard = Auth::guard($guard); - if ($authGuard->guest()) { - throw UnauthorizedException::notLoggedIn(); + $user = $authGuard->user(); + + // For machine-to-machine Passport clients + if (! $user && $request->bearerToken() && config('permission.use_passport_client_credentials')) { + $user = Guard::getPassportClient($guard); } - $user = $authGuard->user(); + if (! $user) { + throw UnauthorizedException::notLoggedIn(); + } if (! method_exists($user, 'hasAnyPermission')) { throw UnauthorizedException::missingTraitHasRoles($user); diff --git a/src/Middlewares/RoleMiddleware.php b/src/Middlewares/RoleMiddleware.php index ae9232cd5..c98ab9d30 100644 --- a/src/Middlewares/RoleMiddleware.php +++ b/src/Middlewares/RoleMiddleware.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Support\Facades\Auth; use Spatie\Permission\Exceptions\UnauthorizedException; +use Spatie\Permission\Guard; class RoleMiddleware { @@ -12,11 +13,16 @@ public function handle($request, Closure $next, $role, $guard = null) { $authGuard = Auth::guard($guard); - if ($authGuard->guest()) { - throw UnauthorizedException::notLoggedIn(); + $user = $authGuard->user(); + + // For machine-to-machine Passport clients + if (! $user && $request->bearerToken() && config('permission.use_passport_client_credentials')) { + $user = Guard::getPassportClient($guard); } - $user = $authGuard->user(); + if (! $user) { + throw UnauthorizedException::notLoggedIn(); + } if (! method_exists($user, 'hasAnyRole')) { throw UnauthorizedException::missingTraitHasRoles($user); diff --git a/src/Middlewares/RoleOrPermissionMiddleware.php b/src/Middlewares/RoleOrPermissionMiddleware.php index 52675a43f..4a4abb562 100644 --- a/src/Middlewares/RoleOrPermissionMiddleware.php +++ b/src/Middlewares/RoleOrPermissionMiddleware.php @@ -5,18 +5,25 @@ use Closure; use Illuminate\Support\Facades\Auth; use Spatie\Permission\Exceptions\UnauthorizedException; +use Spatie\Permission\Guard; class RoleOrPermissionMiddleware { public function handle($request, Closure $next, $roleOrPermission, $guard = null) { $authGuard = Auth::guard($guard); - if ($authGuard->guest()) { - throw UnauthorizedException::notLoggedIn(); - } $user = $authGuard->user(); + // For machine-to-machine Passport clients + if (! $user && $request->bearerToken() && config('permission.use_passport_client_credentials')) { + $user = Guard::getPassportClient($guard); + } + + if (! $user) { + throw UnauthorizedException::notLoggedIn(); + } + if (! method_exists($user, 'hasAnyRole') || ! method_exists($user, 'hasAnyPermission')) { throw UnauthorizedException::missingTraitHasRoles($user); } diff --git a/tests/PermissionMiddlewareTest.php b/tests/PermissionMiddlewareTest.php index d8831c309..7059bd3e9 100644 --- a/tests/PermissionMiddlewareTest.php +++ b/tests/PermissionMiddlewareTest.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Gate; use InvalidArgumentException; +use Laravel\Passport\Passport; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Exceptions\UnauthorizedException; use Spatie\Permission\Middlewares\PermissionMiddleware; @@ -17,6 +18,8 @@ class PermissionMiddlewareTest extends TestCase { protected $permissionMiddleware; + protected $usePassport = true; + protected function setUp(): void { parent::setUp(); @@ -71,6 +74,32 @@ public function a_user_cannot_access_a_route_protected_by_the_permission_middlew ); } + /** @test */ + public function a_client_cannot_access_a_route_protected_by_the_permission_middleware_of_a_different_guard(): void + { + if ($this->getLaravelVersion() < 9) { + $this->markTestSkipped('requires laravel >= 9'); + } + + // These permissions are created fresh here in reverse order of guard being applied, so they are not "found first" in the db lookup when matching + app(Permission::class)->create(['name' => 'admin-permission2', 'guard_name' => 'web']); + $p1 = app(Permission::class)->create(['name' => 'admin-permission2', 'guard_name' => 'api']); + + Passport::actingAsClient($this->testClient, ['*']); + + $this->testClient->givePermissionTo($p1); + + $this->assertEquals( + 200, + $this->runMiddleware($this->permissionMiddleware, 'admin-permission2', 'api', true) + ); + + $this->assertEquals( + 403, + $this->runMiddleware($this->permissionMiddleware, 'edit-articles2', 'web', true) + ); + } + /** @test */ public function a_super_admin_user_can_access_a_route_protected_by_permission_middleware() { @@ -99,6 +128,23 @@ public function a_user_can_access_a_route_protected_by_permission_middleware_if_ ); } + /** @test */ + public function a_client_can_access_a_route_protected_by_permission_middleware_if_have_this_permission(): void + { + if ($this->getLaravelVersion() < 9) { + $this->markTestSkipped('requires laravel >= 9'); + } + + Passport::actingAsClient($this->testClient, ['*'], 'api'); + + $this->testClient->givePermissionTo('edit-posts'); + + $this->assertEquals( + 200, + $this->runMiddleware($this->permissionMiddleware, 'edit-posts', null, true) + ); + } + /** @test */ public function a_user_can_access_a_route_protected_by_this_permission_middleware_if_have_one_of_the_permissions() { @@ -117,6 +163,28 @@ public function a_user_can_access_a_route_protected_by_this_permission_middlewar ); } + /** @test */ + public function a_client_can_access_a_route_protected_by_this_permission_middleware_if_have_one_of_the_permissions(): void + { + if ($this->getLaravelVersion() < 9) { + $this->markTestSkipped('requires laravel >= 9'); + } + + Passport::actingAsClient($this->testClient, ['*']); + + $this->testClient->givePermissionTo('edit-posts'); + + $this->assertEquals( + 200, + $this->runMiddleware($this->permissionMiddleware, 'edit-news|edit-posts', null, true) + ); + + $this->assertEquals( + 200, + $this->runMiddleware($this->permissionMiddleware, ['edit-news', 'edit-posts'], null, true) + ); + } + /** @test */ public function a_user_cannot_access_a_route_protected_by_the_permission_middleware_if_have_not_has_roles_trait() { @@ -143,6 +211,23 @@ public function a_user_cannot_access_a_route_protected_by_the_permission_middlew ); } + /** @test */ + public function a_client_cannot_access_a_route_protected_by_the_permission_middleware_if_have_a_different_permission(): void + { + if ($this->getLaravelVersion() < 9) { + $this->markTestSkipped('requires laravel >= 9'); + } + + Passport::actingAsClient($this->testClient, ['*']); + + $this->testClient->givePermissionTo('edit-posts'); + + $this->assertEquals( + 403, + $this->runMiddleware($this->permissionMiddleware, 'edit-news', null, true) + ); + } + /** @test */ public function a_user_cannot_access_a_route_protected_by_permission_middleware_if_have_not_permissions() { @@ -154,6 +239,21 @@ public function a_user_cannot_access_a_route_protected_by_permission_middleware_ ); } + /** @test */ + public function a_client_cannot_access_a_route_protected_by_permission_middleware_if_have_not_permissions(): void + { + if ($this->getLaravelVersion() < 9) { + $this->markTestSkipped('requires laravel >= 9'); + } + + Passport::actingAsClient($this->testClient, ['*']); + + $this->assertEquals( + 403, + $this->runMiddleware($this->permissionMiddleware, 'edit-articles|edit-posts', null, true) + ); + } + /** @test */ public function a_user_can_access_a_route_protected_by_permission_middleware_if_has_permission_via_role() { @@ -173,6 +273,29 @@ public function a_user_can_access_a_route_protected_by_permission_middleware_if_ ); } + /** @test */ + public function a_client_can_access_a_route_protected_by_permission_middleware_if_has_permission_via_role(): void + { + if ($this->getLaravelVersion() < 9) { + $this->markTestSkipped('requires laravel >= 9'); + } + + Passport::actingAsClient($this->testClient, ['*']); + + $this->assertEquals( + 403, + $this->runMiddleware($this->permissionMiddleware, 'edit-articles', null, true) + ); + + $this->testClientRole->givePermissionTo('edit-posts'); + $this->testClient->assignRole('clientRole'); + + $this->assertEquals( + 200, + $this->runMiddleware($this->permissionMiddleware, 'edit-posts', null, true) + ); + } + /** @test */ public function the_required_permissions_can_be_fetched_from_the_exception() { @@ -242,6 +365,23 @@ public function user_can_not_access_permission_with_guard_admin_while_login_usin ); } + /** @test */ + public function client_can_not_access_permission_with_guard_admin_while_login_using_default_guard(): void + { + if ($this->getLaravelVersion() < 9) { + $this->markTestSkipped('requires laravel >= 9'); + } + + Passport::actingAsClient($this->testClient, ['*']); + + $this->testClient->givePermissionTo('edit-posts'); + + $this->assertEquals( + 403, + $this->runMiddleware($this->permissionMiddleware, 'edit-posts', 'admin', true) + ); + } + /** @test */ public function user_can_access_permission_with_guard_admin_while_login_using_admin_guard() { diff --git a/tests/RoleMiddlewareTest.php b/tests/RoleMiddlewareTest.php index a28c2de07..25f411ee1 100644 --- a/tests/RoleMiddlewareTest.php +++ b/tests/RoleMiddlewareTest.php @@ -7,6 +7,7 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Config; use InvalidArgumentException; +use Laravel\Passport\Passport; use Spatie\Permission\Exceptions\UnauthorizedException; use Spatie\Permission\Middlewares\RoleMiddleware; use Spatie\Permission\Tests\TestModels\UserWithoutHasRoles; @@ -15,6 +16,8 @@ class RoleMiddlewareTest extends TestCase { protected $roleMiddleware; + protected $usePassport = true; + protected function setUp(): void { parent::setUp(); @@ -44,6 +47,23 @@ public function a_user_cannot_access_a_route_protected_by_role_middleware_of_ano ); } + /** @test */ + public function a_client_cannot_access_a_route_protected_by_role_middleware_of_another_guard(): void + { + if ($this->getLaravelVersion() < 9) { + $this->markTestSkipped('requires laravel >= 9'); + } + + Passport::actingAsClient($this->testClient, ['*']); + + $this->testClient->assignRole('clientRole'); + + $this->assertEquals( + 403, + $this->runMiddleware($this->roleMiddleware, 'testAdminRole', null, true) + ); + } + /** @test */ public function a_user_can_access_a_route_protected_by_role_middleware_if_have_this_role() { @@ -57,6 +77,23 @@ public function a_user_can_access_a_route_protected_by_role_middleware_if_have_t ); } + /** @test */ + public function a_client_can_access_a_route_protected_by_role_middleware_if_have_this_role(): void + { + if ($this->getLaravelVersion() < 9) { + $this->markTestSkipped('requires laravel >= 9'); + } + + Passport::actingAsClient($this->testClient, ['*']); + + $this->testClient->assignRole('clientRole'); + + $this->assertEquals( + 200, + $this->runMiddleware($this->roleMiddleware, 'clientRole', null, true) + ); + } + /** @test */ public function a_user_can_access_a_route_protected_by_this_role_middleware_if_have_one_of_the_roles() { @@ -75,6 +112,28 @@ public function a_user_can_access_a_route_protected_by_this_role_middleware_if_h ); } + /** @test */ + public function a_client_can_access_a_route_protected_by_this_role_middleware_if_have_one_of_the_roles(): void + { + if ($this->getLaravelVersion() < 9) { + $this->markTestSkipped('requires laravel >= 9'); + } + + Passport::actingAsClient($this->testClient, ['*']); + + $this->testClient->assignRole('clientRole'); + + $this->assertEquals( + 200, + $this->runMiddleware($this->roleMiddleware, 'clientRole|testRole2', null, true) + ); + + $this->assertEquals( + 200, + $this->runMiddleware($this->roleMiddleware, ['testRole2', 'clientRole'], null, true) + ); + } + /** @test */ public function a_user_cannot_access_a_route_protected_by_the_role_middleware_if_have_not_has_roles_trait() { @@ -101,6 +160,23 @@ public function a_user_cannot_access_a_route_protected_by_the_role_middleware_if ); } + /** @test */ + public function a_client_cannot_access_a_route_protected_by_the_role_middleware_if_have_a_different_role(): void + { + if ($this->getLaravelVersion() < 9) { + $this->markTestSkipped('requires laravel >= 9'); + } + + Passport::actingAsClient($this->testClient, ['*']); + + $this->testClient->assignRole(['clientRole']); + + $this->assertEquals( + 403, + $this->runMiddleware($this->roleMiddleware, 'clientRole2', null, true) + ); + } + /** @test */ public function a_user_cannot_access_a_route_protected_by_role_middleware_if_have_not_roles() { @@ -112,6 +188,21 @@ public function a_user_cannot_access_a_route_protected_by_role_middleware_if_hav ); } + /** @test */ + public function a_client_cannot_access_a_route_protected_by_role_middleware_if_have_not_roles(): void + { + if ($this->getLaravelVersion() < 9) { + $this->markTestSkipped('requires laravel >= 9'); + } + + Passport::actingAsClient($this->testClient, ['*']); + + $this->assertEquals( + 403, + $this->runMiddleware($this->roleMiddleware, 'testRole|testRole2', null, true) + ); + } + /** @test */ public function a_user_cannot_access_a_route_protected_by_role_middleware_if_role_is_undefined() { @@ -123,6 +214,21 @@ public function a_user_cannot_access_a_route_protected_by_role_middleware_if_rol ); } + /** @test */ + public function a_client_cannot_access_a_route_protected_by_role_middleware_if_role_is_undefined(): void + { + if ($this->getLaravelVersion() < 9) { + $this->markTestSkipped('requires laravel >= 9'); + } + + Passport::actingAsClient($this->testClient, ['*']); + + $this->assertEquals( + 403, + $this->runMiddleware($this->roleMiddleware, '', null, true) + ); + } + /** @test */ public function the_required_roles_can_be_fetched_from_the_exception() { @@ -192,6 +298,23 @@ public function user_can_not_access_role_with_guard_admin_while_login_using_defa ); } + /** @test */ + public function client_can_not_access_role_with_guard_admin_while_login_using_default_guard(): void + { + if ($this->getLaravelVersion() < 9) { + $this->markTestSkipped('requires laravel >= 9'); + } + + Passport::actingAsClient($this->testClient, ['*']); + + $this->testClient->assignRole('clientRole'); + + $this->assertEquals( + 403, + $this->runMiddleware($this->roleMiddleware, 'clientRole', 'admin', true) + ); + } + /** @test */ public function user_can_access_role_with_guard_admin_while_login_using_admin_guard() { diff --git a/tests/RoleOrPermissionMiddlewareTest.php b/tests/RoleOrPermissionMiddlewareTest.php index 24367e51f..a3de1054d 100644 --- a/tests/RoleOrPermissionMiddlewareTest.php +++ b/tests/RoleOrPermissionMiddlewareTest.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Gate; use InvalidArgumentException; +use Laravel\Passport\Passport; use Spatie\Permission\Exceptions\UnauthorizedException; use Spatie\Permission\Middlewares\RoleOrPermissionMiddleware; use Spatie\Permission\Tests\TestModels\UserWithoutHasRoles; @@ -16,6 +17,8 @@ class RoleOrPermissionMiddlewareTest extends TestCase { protected $roleOrPermissionMiddleware; + protected $usePassport = true; + protected function setUp(): void { parent::setUp(); @@ -66,6 +69,44 @@ public function a_user_can_access_a_route_protected_by_permission_or_role_middle ); } + /** @test */ + public function a_client_can_access_a_route_protected_by_permission_or_role_middleware_if_has_this_permission_or_role(): void + { + if ($this->getLaravelVersion() < 9) { + $this->markTestSkipped('requires laravel >= 9'); + } + + Passport::actingAsClient($this->testClient, ['*']); + + $this->testClient->assignRole('clientRole'); + $this->testClient->givePermissionTo('edit-posts'); + + $this->assertEquals( + 200, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'clientRole|edit-news|edit-posts', null, true) + ); + + $this->testClient->removeRole('clientRole'); + + $this->assertEquals( + 200, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'clientRole|edit-posts', null, true) + ); + + $this->testClient->revokePermissionTo('edit-posts'); + $this->testClient->assignRole('clientRole'); + + $this->assertEquals( + 200, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'clientRole|edit-posts', null, true) + ); + + $this->assertEquals( + 200, + $this->runMiddleware($this->roleOrPermissionMiddleware, ['clientRole', 'edit-posts'], null, true) + ); + } + /** @test */ public function a_super_admin_user_can_access_a_route_protected_by_permission_or_role_middleware() { @@ -110,6 +151,26 @@ public function a_user_can_not_access_a_route_protected_by_permission_or_role_mi ); } + /** @test */ + public function a_client_can_not_access_a_route_protected_by_permission_or_role_middleware_if_have_not_this_permission_and_role(): void + { + if ($this->getLaravelVersion() < 9) { + $this->markTestSkipped('requires laravel >= 9'); + } + + Passport::actingAsClient($this->testClient, ['*']); + + $this->assertEquals( + 403, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'clientRole|edit-posts', null, true) + ); + + $this->assertEquals( + 403, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'missingRole|missingPermission', null, true) + ); + } + /** @test */ public function use_not_existing_custom_guard_in_role_or_permission() { @@ -140,6 +201,24 @@ public function user_can_not_access_permission_or_role_with_guard_admin_while_lo ); } + /** @test */ + public function client_can_not_access_permission_or_role_with_guard_admin_while_login_using_default_guard(): void + { + if ($this->getLaravelVersion() < 9) { + $this->markTestSkipped('requires laravel >= 9'); + } + + Passport::actingAsClient($this->testClient, ['*']); + + $this->testClient->assignRole('clientRole'); + $this->testClient->givePermissionTo('edit-posts'); + + $this->assertEquals( + 403, + $this->runMiddleware($this->roleOrPermissionMiddleware, 'edit-posts|clientRole', 'admin', true) + ); + } + /** @test */ public function user_can_access_permission_or_role_with_guard_admin_while_login_using_admin_guard() { diff --git a/tests/TestCase.php b/tests/TestCase.php index 3fe775b08..302169f65 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Schema; +use Laravel\Passport\PassportServiceProvider; use Orchestra\Testbench\TestCase as Orchestra; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Contracts\Role; @@ -15,6 +16,7 @@ use Spatie\Permission\PermissionRegistrar; use Spatie\Permission\PermissionServiceProvider; use Spatie\Permission\Tests\TestModels\Admin; +use Spatie\Permission\Tests\TestModels\Client; use Spatie\Permission\Tests\TestModels\User; abstract class TestCase extends Orchestra @@ -47,6 +49,15 @@ abstract class TestCase extends Orchestra protected static $customMigration; + /** @var bool */ + protected $usePassport = false; + + protected Client $testClient; + + protected \Spatie\Permission\Models\Permission $testClientPermission; + + protected \Spatie\Permission\Models\Role $testClientRole; + protected function setUp(): void { parent::setUp(); @@ -61,6 +72,10 @@ protected function setUp(): void setPermissionsTeamId(1); } + if ($this->usePassport) { + $this->setUpPassport($this->app); + } + $this->setUpRoutes(); } @@ -70,8 +85,11 @@ protected function setUp(): void */ protected function getPackageProviders($app) { - return [ + return $this->getLaravelVersion() < 9 ? [ + PermissionServiceProvider::class, + ] : [ PermissionServiceProvider::class, + PassportServiceProvider::class, ]; } @@ -169,6 +187,23 @@ protected function setUpDatabase($app) $app[Permission::class]->create(['name' => 'Edit News']); } + protected function setUpPassport($app): void + { + if ($this->getLaravelVersion() < 9) { + return; + } + + $app['config']->set('permission.use_passport_client_credentials', true); + $app['config']->set('auth.guards.api', ['driver' => 'passport', 'provider' => 'users']); + + $this->artisan('migrate'); + $this->artisan('passport:install'); + + $this->testClient = Client::create(['name' => 'Test', 'redirect' => 'https://example.com/', 'personal_access_client' => 0, 'password_client' => 0, 'revoked' => 0]); + $this->testClientRole = $app[Role::class]->create(['name' => 'clientRole', 'guard_name' => 'api']); + $this->testClientPermission = $app[Permission::class]->create(['name' => 'edit-posts', 'guard_name' => 'api']); + } + private function prepareMigration() { $migration = str_replace( @@ -227,10 +262,15 @@ public function setUpRoutes(): void } ////// TEST HELPERS - public function runMiddleware($middleware, $permission, $guard = null) + public function runMiddleware($middleware, $permission, $guard = null, bool $client = false) { + $request = new Request; + if ($client) { + $request->headers->set('Authorization', 'Bearer '.str()->random(30)); + } + try { - return $middleware->handle(new Request(), function () { + return $middleware->handle($request, function () { return (new Response())->setContent(''); }, $permission, $guard)->status(); } catch (UnauthorizedException $e) { @@ -254,4 +294,9 @@ public function getRouteResponse() return (new Response())->setContent(''); }; } + + protected function getLaravelVersion() + { + return (float) app()->version(); + } } diff --git a/tests/TestModels/Client.php b/tests/TestModels/Client.php new file mode 100644 index 000000000..a920bddd0 --- /dev/null +++ b/tests/TestModels/Client.php @@ -0,0 +1,21 @@ + Date: Wed, 26 Jul 2023 19:46:31 -0400 Subject: [PATCH 0728/1013] Document unsetRelations() requirements when switching teams @erikn69 does this need additional clarification? --- docs/basic-usage/teams-permissions.md | 31 +++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/basic-usage/teams-permissions.md b/docs/basic-usage/teams-permissions.md index a97136e8c..d60657377 100644 --- a/docs/basic-usage/teams-permissions.md +++ b/docs/basic-usage/teams-permissions.md @@ -73,6 +73,37 @@ Role::create(['name' => 'reviewer']); The role/permission assignment and removal for teams are the same as without teams, but they take the global `team_id` which is set on login. +## Changing The Active Team ID + +While your middleware will set a user's `team_id` upon login, you may later need to set it to another team for various reasons. The two most common reasons are these: + +### Switching Teams After Login +If your application allows the user to switch between various teams which they belong to, you can activate the roles/permissions for that team by calling `setPermissionsTeamId($new_team_id)` and unsetting relations as described below. + +### Administrating Team Details +You may have created a User-Manager page where you can view the roles/permissions of users on certain teams. For managing that user in each team they belong to, you must also use `setPermissionsTeamId($new_team_id)` to cause lookups to relate to that new team, and unset prior relations as described below. + +### Querying Roles/Permissions for Other Teams +Whenever you switch the active `team_id` using `setPermissionsTeamId()`, you need to `unset` the user's/model's `roles` and `permissions` relations before querying what roles/permissions that user has (`$user->roles`, etc) and before calling any authorization functions (`can()`, `hasPermissionTo()`, `hasRole()`, etc). + +Example: +```php +// set active global team_id +setPermissionsTeamId($new_team_id); + +// $user = Auth::user(); + +// unset cached model relations so new team relations will get reloaded +$user->unsetRelation('roles','permissions'); + +// Now you can check: +$roles = $user->roles; +$hasRole = $user->hasRole('my_role'); +$user->hasPermissionTo('foo'); +$user->can('bar'); +// etc +``` + ## Defining a Super-Admin on Teams Global roles can be assigned to different teams, and `team_id` (which is the primary key of the relationships) is always required. From 2c0f7677451e8ab709d1eaeed6449f43c8babde0 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 31 Jul 2023 14:37:13 -0400 Subject: [PATCH 0729/1013] [Docs] For WithoutModelEvents add note to flush cache after seeding --- docs/advanced-usage/seeding.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/advanced-usage/seeding.md b/docs/advanced-usage/seeding.md index d27bc2a1e..c9abac299 100644 --- a/docs/advanced-usage/seeding.md +++ b/docs/advanced-usage/seeding.md @@ -3,9 +3,11 @@ title: Database Seeding weight: 2 --- -## Flush cache before seeding +## Flush cache before/after seeding -You may discover that it is best to flush this package's cache before seeding, to avoid cache conflict errors. +You may discover that it is best to flush this package's cache **BEFORE seeding, to avoid cache conflict errors**. + +And if you use the `WithoutModelEvents` trait in your seeders, flush it **AFTER seeding as well**. ```php // reset cached roles and permissions From 2cc68fdce917d7f7f6c37ec91157225afe9db902 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 31 Jul 2023 14:40:00 -0400 Subject: [PATCH 0730/1013] [docs] another note about flushing cache in seeders --- docs/advanced-usage/seeding.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/advanced-usage/seeding.md b/docs/advanced-usage/seeding.md index c9abac299..0f40e7f27 100644 --- a/docs/advanced-usage/seeding.md +++ b/docs/advanced-usage/seeding.md @@ -14,7 +14,7 @@ And if you use the `WithoutModelEvents` trait in your seeders, flush it **AFTER app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions(); ``` -You can do this in the `SetUp()` method of your test suite (see the Testing page in the docs). +You can optionally flush the cache before seeding by using the `SetUp()` method of your test suite (see the Testing page in the docs). Or it can be done directly in a seeder class, as shown below. @@ -97,6 +97,8 @@ foreach ($permissionIdsByRole as $role => $permissionIds) { ])->toArray() ); } + +// and also add the command to flush the cache again now after doing all these inserts ``` **CAUTION**: ANY TIME YOU DIRECTLY RUN DB QUERIES you are bypassing cache-control features. So you will need to manually flush the package cache AFTER running direct DB queries, even in a seeder. From 0c2422b4b295a0f936a12d118317091ca9267159 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 15 Aug 2023 17:11:51 -0400 Subject: [PATCH 0731/1013] [Docs] Show a way to use a single guard for all roles/permissions --- docs/basic-usage/multiple-guards.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/basic-usage/multiple-guards.md b/docs/basic-usage/multiple-guards.md index 08e44d1d2..9a1911906 100644 --- a/docs/basic-usage/multiple-guards.md +++ b/docs/basic-usage/multiple-guards.md @@ -12,6 +12,16 @@ However, when using multiple guards they will act like namespaces for your permi Note that this package requires you to register a permission name for each guard you want to authenticate with. So, "edit-article" would have to be created multiple times for each guard your app uses. An exception will be thrown if you try to authenticate against a non-existing permission+guard combination. Same for roles. > **Tip**: If your app uses only a single guard, but is not `web` (Laravel's default, which shows "first" in the auth config file) then change the order of your listed guards in your `config/auth.php` to list your primary guard as the default and as the first in the list of defined guards. While you're editing that file, best to remove any guards you don't use, too. +> +> OR you could use the suggestion below to force the use of a single guard: + +## Forcing Use Of A Single Guard + +If your app structure does NOT differentiate between guards when it comes to roles/permissions, (ie: if ALL your roles/permissions are the SAME for ALL guards), you can override the `getDefaultGuardName` function by adding it to your User model, and specifying your desired guard_name. Then you only need to create roles/permissions for that single guard_name, not duplicating them. The example here sets it to `web`, but use whatever your application's default is: + +```php + protected function getDefaultGuardName(): string { return 'web'; } +```` ## Using permissions and roles with multiple guards From feb301470b7ca09e77cca8d2dec662a89d2770a8 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Wed, 16 Aug 2023 19:42:19 -0500 Subject: [PATCH 0732/1013] [V6] Register OctaneReloadPermissions listener for Laravel Octane (#2403) * Register OctaneReloadPermissions listener for Laravel Octane --------- Co-authored-by: Chris Brown --- config/permission.php | 8 ++++++++ src/Listeners/OctaneReloadPermissions.php | 13 +++++++++++++ src/PermissionServiceProvider.php | 19 +++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 src/Listeners/OctaneReloadPermissions.php diff --git a/config/permission.php b/config/permission.php index b98b24148..0507baec5 100644 --- a/config/permission.php +++ b/config/permission.php @@ -103,6 +103,14 @@ 'register_permission_check_method' => true, + /* + * When set to true, the Spatie\Permission\Listeners\OctaneReloadPermissions listener will be registered + * on the Laravel\Octane\Events\OperationTerminated event, this will refresh permissions on every + * TickTerminated, TaskTerminated and RequestTerminated + * NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it. + */ + 'register_octane_reset_listener' => false, + /* * Teams Feature. * When set to true the package implements teams using the 'team_foreign_key'. diff --git a/src/Listeners/OctaneReloadPermissions.php b/src/Listeners/OctaneReloadPermissions.php new file mode 100644 index 000000000..6f4544e5e --- /dev/null +++ b/src/Listeners/OctaneReloadPermissions.php @@ -0,0 +1,13 @@ +sandbox->make(PermissionRegistrar::class)->clearPermissionsCollection(); + } +} diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 639e7faa7..f2643cef0 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -3,6 +3,7 @@ namespace Spatie\Permission; use Illuminate\Contracts\Auth\Access\Gate; +use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Foundation\Application; use Illuminate\Filesystem\Filesystem; use Illuminate\Routing\Route; @@ -12,6 +13,7 @@ use Illuminate\View\Compilers\BladeCompiler; use Spatie\Permission\Contracts\Permission as PermissionContract; use Spatie\Permission\Contracts\Role as RoleContract; +use Spatie\Permission\Listeners\OctaneReloadPermissions; class PermissionServiceProvider extends ServiceProvider { @@ -25,6 +27,8 @@ public function boot() $this->registerModelBindings(); + $this->registerOctaneListener(); + $this->callAfterResolving(Gate::class, function (Gate $gate, Application $app) { if ($this->app['config']->get('permission.register_permission_check_method')) { /** @var PermissionRegistrar $permissionLoader */ @@ -85,6 +89,21 @@ protected function registerCommands(): void ]); } + protected function registerOctaneListener(): void + { + if ($this->app->runningInConsole() || ! $this->app['config']->get('permission.register_octane_reset_listener')) { + return; + } + + if (! $this->app['config']->get('octane.listeners')) { + return; + } + + $dispatcher = $this->app[Dispatcher::class]; + // @phpstan-ignore-next-line + $dispatcher->listen(\Laravel\Octane\Events\OperationTerminated::class, OctaneReloadPermissions::class); + } + protected function registerModelBindings(): void { $this->app->bind(PermissionContract::class, fn ($app) => $app->make($app->config['permission.models.permission'])); From 7920916de2d2c13745e190181ce5570830991ceb Mon Sep 17 00:00:00 2001 From: Denis Kovtunenko Date: Sat, 19 Aug 2023 08:04:31 +0700 Subject: [PATCH 0733/1013] Update uuid.md (#2438) * Update uuid.md Resolve problems with Laravel 10. Ex version of uuid.md doesn`t correct working. In this manual -- all good --------- Co-authored-by: Chris Brown --- docs/advanced-usage/uuid.md | 167 +++++++++++++++++++++++++++--------- 1 file changed, 125 insertions(+), 42 deletions(-) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index 517cf7eb5..1f54e2e1e 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -3,61 +3,67 @@ title: UUID weight: 7 --- -If you're using UUIDs or GUIDs for your User models there are a few considerations to note. +If you're using UUIDs for your User models there are a few considerations to note. > THIS IS NOT A FULL LESSON ON HOW TO IMPLEMENT UUIDs IN YOUR APP. Since each UUID implementation approach is different, some of these may or may not benefit you. As always, your implementation may vary. - ## Migrations -You will probably want to update the `create_permission_tables.php` migration: +You will need to update the `create_permission_tables.php` migration: -If your User models are using `uuid` instead of `unsignedBigInteger` then you'll need to reflect the change in the migration provided by this package. Something like this would be typical, for both `model_has_permissions` and `model_has_roles` tables: +**User Models using UUIDs** +If your User models are using `uuid` instead of `unsignedBigInteger` then you'll need to reflect the change in the migration provided by this package. Something like the following would be typical, for **both** `model_has_permissions` and `model_has_roles` tables: ```diff +// note: this is done in two places in the default migration file, so edit both places: - $table->unsignedBigInteger($columnNames['model_morph_key']) + $table->uuid($columnNames['model_morph_key']) ``` -OPTIONAL: If you also want the roles and permissions to use a UUID for their `id` value, then you'll need to also change the id fields accordingly, and manually set the primary key. LEAVE THE FIELD NAME AS `id` unless you also change it in dozens of other places. +**Roles and Permissions using UUIDS** +If you also want the roles and permissions to use a UUID for their `id` value, then you'll need to change the id fields accordingly, and manually set the primary key. ```diff Schema::create($tableNames['permissions'], function (Blueprint $table) { -- $table->bigIncrements('id'); -+ $table->uuid('id'); - $table->string('name'); - $table->string('guard_name'); - $table->timestamps(); - -+ $table->primary('id'); +- $table->bigIncrements('id'); // permission id ++ $table->uuid('id')->primary()->unique(); // permission id +//... }); Schema::create($tableNames['roles'], function (Blueprint $table) { -- $table->bigIncrements('id'); -+ $table->uuid('id'); - $table->string('name'); - $table->string('guard_name'); - $table->timestamps(); - -+ $table->primary('id'); +- $table->bigIncrements('id'); // role id ++ $table->uuid('id')->primary()->unique(); // role id +//... }); Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) { -- $table->bigIncrements('permission_id'); -+ $table->uuid('permission_id'); - ... +- $table->unsignedBigInteger(PermissionRegistrar::$pivotPermission); ++ $table->uuid(PermissionRegistrar::$pivotPermission); + $table->string('model_type'); +//... + $table->foreign(PermissionRegistrar::$pivotPermission) +- ->references('id') // permission id ++ ->references('uuid') // permission id + ->on($tableNames['permissions']) + ->onDelete('cascade'); +//... Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames) { -- $table->bigIncrements('role_id'); -+ $table->uuid('role_id'); - ... +- $table->unsignedBigInteger(PermissionRegistrar::$pivotRole); ++ $table->uuid(PermissionRegistrar::$pivotRole); +//... + $table->foreign(PermissionRegistrar::$pivotRole) +- ->references('id') // role id ++ ->references('uuid') // role id + ->on($tableNames['roles']) + ->onDelete('cascade');//... Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames) { -- $table->bigIncrements('permission_id'); -- $table->bigIncrements('role_id'); -+ $table->uuid('permission_id'); -+ $table->uuid('role_id'); +- $table->unsignedBigInteger(PermissionRegistrar::$pivotPermission); +- $table->unsignedBigInteger(PermissionRegistrar::$pivotRole); ++ $table->uuid(PermissionRegistrar::$pivotPermission); ++ $table->uuid(PermissionRegistrar::$pivotRole); ``` @@ -65,26 +71,103 @@ OPTIONAL: If you also want the roles and permissions to use a UUID for their `id You might want to change the pivot table field name from `model_id` to `model_uuid`, just for semantic purposes. For this, in the configuration file edit `column_names.model_morph_key`: -- OPTIONAL: Change to `model_uuid` instead of the default `model_id`. (The default of `model_id` is shown in this snippet below. Change it to match your needs.) - +- OPTIONAL: Change to `model_uuid` instead of the default `model_id`. +```diff 'column_names' => [ - /* - * Change this if you want to name the related model primary key other than - * `model_id`. - * - * For example, this would be nice if your primary keys are all UUIDs. In - * that case, name this `model_uuid`. - */ - 'model_morph_key' => 'model_id', + /* + * Change this if you want to name the related pivots other than defaults + */ + 'role_pivot_key' => null, //default 'role_id', + 'permission_pivot_key' => null, //default 'permission_id', + + /* + * Change this if you want to name the related model primary key other than + * `model_id`. + * + * For example, this would be nice if your primary keys are all UUIDs. In + * that case, name this `model_uuid`. + */ +- 'model_morph_key' => 'model_id', ++ 'model_morph_key' => 'model_uuid', ], +``` - If you extend the models into your app, be sure to list those models in your configuration file. See the Extending section of the documentation and the Models section below. ## Models If you want all the role/permission objects to have a UUID instead of an integer, you will need to Extend the default Role and Permission models into your own namespace in order to set some specific properties. (See the Extending section of the docs, where it explains requirements of Extending, as well as the configuration settings you need to update.) -- You likely want to set `protected $keyType = 'string';` so Laravel handles joins as strings and doesn't cast to integer. -- OPTIONAL: If you changed the field name in your migrations, you must set `protected $primaryKey = 'uuid';` to match. -- Usually for UUID you will also set `public $incrementing = false;`. Remove it if it causes problems for you. +Examples: + +Create new models, which extend the Role and Permission models of this package, and add the HasUuids trait (available since Laravel 9): +```bash +php artisan make:model Role +php artisan make:model Permission +``` + +`App\Model\Role.php` +```php + [ + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * Eloquent model should be used to retrieve your permissions. Of course, it + * is often just the "Permission" model but you may use whatever you like. + * + * The model you want to use as a Permission model needs to implement the + * `Spatie\Permission\Contracts\Permission` contract. + */ + +- 'permission' => Spatie\Permission\Models\Permission::class ++ 'permission' => App\Models\Permission::class, + + /* + * When using the "HasRoles" trait from this package, we need to know which + * Eloquent model should be used to retrieve your roles. Of course, it + * is often just the "Role" model but you may use whatever you like. + * + * The model you want to use as a Role model needs to implement the + * `Spatie\Permission\Contracts\Role` contract. + */ + +- 'role' => Spatie\Permission\Models\Role::class, ++ 'role' => App\Models\Role::class, + + ], +``` + It is common to use a trait to handle the $keyType and $incrementing settings, as well as add a boot event trigger to ensure new records are assigned a uuid. You would `use` this trait in your User and extended Role/Permission models. An example `UuidTrait` is shown here for inspiration. Adjust to suit your needs. From 188bcda43c2aad39b9287770e5418d0523fcfa80 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 18 Aug 2023 21:12:05 -0400 Subject: [PATCH 0734/1013] [docs] encourage HasUuids trait instead of bespoke code --- docs/advanced-usage/uuid.md | 74 ++++--------------------------------- 1 file changed, 7 insertions(+), 67 deletions(-) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index 1f54e2e1e..81fe7d2fe 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -27,13 +27,13 @@ If you also want the roles and permissions to use a UUID for their `id` value, t ```diff Schema::create($tableNames['permissions'], function (Blueprint $table) { - $table->bigIncrements('id'); // permission id -+ $table->uuid('id')->primary()->unique(); // permission id ++ $table->uuid('uuid')->primary()->unique(); // permission id //... }); Schema::create($tableNames['roles'], function (Blueprint $table) { - $table->bigIncrements('id'); // role id -+ $table->uuid('id')->primary()->unique(); // role id ++ $table->uuid('uuid')->primary()->unique(); // role id //... }); @@ -98,7 +98,7 @@ If you want all the role/permission objects to have a UUID instead of an integer Examples: -Create new models, which extend the Role and Permission models of this package, and add the HasUuids trait (available since Laravel 9): +Create new models, which extend the Role and Permission models of this package, and add Laravel's `HasUuids` trait (available since Laravel 9): ```bash php artisan make:model Role php artisan make:model Permission @@ -107,7 +107,6 @@ php artisan make:model Permission `App\Model\Role.php` ```php keyType = 'string'; - $model->incrementing = false; - - $model->{$model->getKeyName()} = $model->{$model->getKeyName()} ?: (string) Str::orderedUuid(); - }); - } - - public function getIncrementing() - { - return false; - } - - public function getKeyType() - { - return 'string'; - } - } -``` - - -## User Models -> Troubleshooting tip: In the ***Prerequisites*** section of the docs we remind you that your User model must implement the `Illuminate\Contracts\Auth\Access\Authorizable` contract so that the Gate features are made available to the User object. -In the default User model provided with Laravel, this is done by extending another model (aliased to `Authenticatable`), which extends the base Eloquent model. -However, your app's UUID implementation may need to override that in order to set some of the properties mentioned in the Models section above. - -If you are running into difficulties, you may want to double-check whether your User model is doing UUIDs consistent with other parts of your app. - - -# REMINDER: - -> THIS IS NOT A FULL LESSON ON HOW TO IMPLEMENT UUIDs IN YOUR APP. - -Again, since each UUID implementation approach is different, some of these may or may not benefit you. As always, your implementation may vary. - - - -## Packages -There are many packages offering UUID features for Eloquent models. You may want to explore whether these are of value to you in your study of implementing UUID in your applications: - -https://github.com/JamesHemery/laravel-uuid -https://github.com/jamesmills/eloquent-uuid -https://github.com/goldspecdigital/laravel-eloquent-uuid -https://github.com/michaeldyrynda/laravel-model-uuid - -Remember: always make sure you understand what a package is doing before you use it! If it's doing "more than what you need" then you're adding more complexity to your application, as well as more things to test and support! From 04b3230c157f296ac3580612682d70b4c8969fdc Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 18 Aug 2023 21:14:37 -0400 Subject: [PATCH 0735/1013] [docs] minor edits to uuid --- docs/advanced-usage/uuid.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index 81fe7d2fe..2209159d9 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -10,7 +10,7 @@ If you're using UUIDs for your User models there are a few considerations to not Since each UUID implementation approach is different, some of these may or may not benefit you. As always, your implementation may vary. ## Migrations -You will need to update the `create_permission_tables.php` migration: +You will need to update the `create_permission_tables.php` migration after creating it with `php artisan vendor:publish`. After making your edits, be sure to run the migration! **User Models using UUIDs** If your User models are using `uuid` instead of `unsignedBigInteger` then you'll need to reflect the change in the migration provided by this package. Something like the following would be typical, for **both** `model_has_permissions` and `model_has_roles` tables: From 8eb76af0115208052bff3233402e2ccd46d62775 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 21 Aug 2023 11:21:08 -0400 Subject: [PATCH 0736/1013] [Docs] Simplify instructions --- docs/installation-laravel.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index e1e3d5792..54894042d 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -43,28 +43,30 @@ Package Version | Laravel Version php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" ``` -6. NOTE: **If you are using UUIDs**, see the Advanced section of the docs on UUID steps, before you continue. It explains some changes you may want to make to the migrations and config file before continuing. It also mentions important considerations after extending this package's models for UUID capability. +6. BEFORE RUNNING MIGRATIONS - **If you are going to use the TEAMS features**, you must update your [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/main/config/permission.php) and set `'teams' => true,`; and in your database if you want to use a custom foreign key for teams you must change `team_foreign_key`. + - **If you are using UUIDs**, see the Advanced section of the docs on UUID steps, before you continue. It explains some changes you may want to make to the migrations and config file before continuing. It also mentions important considerations after extending this package's models for UUID capability. -7. NOTE: If you are using MySQL 8, look at the migration files for notes about MySQL 8 to set/limit the index key length, and edit accordingly. + - **If you are going to use the TEAMS features**, you must update your [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/main/config/permission.php) and set `'teams' => true,`; and in your database if you want to use a custom foreign key for teams you must change `team_foreign_key`. -8. **Clear your config cache**. This package requires access to the `permission` config. Generally it's bad practice to do config-caching in a development environment. If you've been caching configurations locally, clear your config cache with either of these commands: + - **If you are using MySQL 8**, look at the migration files for notes about MySQL 8 to set/limit the index key length, and edit accordingly. + +7. **Clear your config cache**. This package requires access to the `permission` config settings in order to run migrations. If you've been caching configurations locally, clear your config cache with either of these commands: php artisan optimize:clear # or php artisan config:clear -9. **Run the migrations**: After the config and migration have been published and configured, you can create the tables for this package by running: +8. **Run the migrations**: After the config and migration have been published and configured, you can create the tables for this package by running: php artisan migrate -10. **Add the necessary trait to your User model**: +9. **Add the necessary trait to your User model**: // The User model requires this trait use HasRoles; - Consult the **Basic Usage** section of the docs to get started using the features of this package. +10. Consult the **Basic Usage** section of the docs to get started using the features of this package. . From e915a89b25bdbf99d8bbad656e975486e177a843 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 21 Aug 2023 11:24:52 -0400 Subject: [PATCH 0737/1013] [Docs] Simplify instructions --- docs/installation-laravel.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 54894042d..8ace927b3 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -47,7 +47,9 @@ Package Version | Laravel Version - **If you are using UUIDs**, see the Advanced section of the docs on UUID steps, before you continue. It explains some changes you may want to make to the migrations and config file before continuing. It also mentions important considerations after extending this package's models for UUID capability. - - **If you are going to use the TEAMS features**, you must update your [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/main/config/permission.php) and set `'teams' => true,`; and in your database if you want to use a custom foreign key for teams you must change `team_foreign_key`. + - **If you are going to use the TEAMS features** you must update your [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/main/config/permission.php): + - must set `'teams' => true,` + - and (optional) you may set `team_foreign_key` name in the config file if you want to use a custom foreign key in your database for teams - **If you are using MySQL 8**, look at the migration files for notes about MySQL 8 to set/limit the index key length, and edit accordingly. From 9e7e4c23d0641e81f389ec35fd468a9ea0957e58 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 21 Aug 2023 11:31:31 -0400 Subject: [PATCH 0738/1013] Add guard name to exceptions (#2481) * Add guard_name to DoesNotExist exceptions It's much easier to troubleshoot guard-related issues when the guard is included in the exception. --- src/Exceptions/PermissionDoesNotExist.php | 11 ++++++++--- src/Exceptions/RoleDoesNotExist.php | 12 ++++++++---- src/Models/Role.php | 4 ++-- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/Exceptions/PermissionDoesNotExist.php b/src/Exceptions/PermissionDoesNotExist.php index e6d7c101e..b451ace6b 100644 --- a/src/Exceptions/PermissionDoesNotExist.php +++ b/src/Exceptions/PermissionDoesNotExist.php @@ -6,13 +6,18 @@ class PermissionDoesNotExist extends InvalidArgumentException { - public static function create(string $permissionName, string $guardName = '') + public static function create(string $permissionName, ?string $guardName) { return new static("There is no permission named `{$permissionName}` for guard `{$guardName}`."); } - public static function withId(int $permissionId, string $guardName = '') + /** + * @param int|string $permissionId + * @param string $guardName + * @return static + */ + public static function withId($permissionId, ?string $guardName) { - return new static("There is no [permission] with id `{$permissionId}` for guard `{$guardName}`."); + return new static("There is no [permission] with ID `{$permissionId}` for guard `{$guardName}`."); } } diff --git a/src/Exceptions/RoleDoesNotExist.php b/src/Exceptions/RoleDoesNotExist.php index cee34e146..a4871c665 100644 --- a/src/Exceptions/RoleDoesNotExist.php +++ b/src/Exceptions/RoleDoesNotExist.php @@ -6,13 +6,17 @@ class RoleDoesNotExist extends InvalidArgumentException { - public static function named(string $roleName) + public static function named(string $roleName, ?string $guardName) { - return new static("There is no role named `{$roleName}`."); + return new static("There is no role named `{$roleName}` for guard `{$guardName}`."); } - public static function withId(int $roleId) + /** + * @param int|string $roleId + * @return static + */ + public static function withId($roleId, ?string $guardName) { - return new static("There is no role with id `{$roleId}`."); + return new static("There is no role with ID `{$roleId}` for guard `{$guardName}`."); } } diff --git a/src/Models/Role.php b/src/Models/Role.php index 025147864..d96102a2f 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -103,7 +103,7 @@ public static function findByName(string $name, $guardName = null): RoleContract $role = static::findByParam(['name' => $name, 'guard_name' => $guardName]); if (! $role) { - throw RoleDoesNotExist::named($name); + throw RoleDoesNotExist::named($name, $guardName); } return $role; @@ -123,7 +123,7 @@ public static function findById($id, $guardName = null): RoleContract $role = static::findByParam([(new static())->getKeyName() => $id, 'guard_name' => $guardName]); if (! $role) { - throw RoleDoesNotExist::withId($id); + throw RoleDoesNotExist::withId($id, $guardName); } return $role; From 6cbe27eba4a84d741abbfe4fec0eb080bf076e04 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 21 Aug 2023 11:47:44 -0400 Subject: [PATCH 0739/1013] Update middleware.md --- docs/basic-usage/middleware.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index 48a5ceee1..37f1dd850 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -1,5 +1,5 @@ --- -title: Using a middleware +title: Using a Middleware weight: 11 --- @@ -45,7 +45,7 @@ protected $middlewareAliases = [ Then you can protect your routes using middleware rules: ```php -Route::group(['middleware' => ['role:super-admin']], function () { +Route::group(['middleware' => ['role:manager']], function () { // }); @@ -53,7 +53,7 @@ Route::group(['middleware' => ['permission:publish articles']], function () { // }); -Route::group(['middleware' => ['role:super-admin','permission:publish articles']], function () { +Route::group(['middleware' => ['role:manager','permission:publish articles']], function () { // }); @@ -65,7 +65,7 @@ Route::group(['middleware' => ['role_or_permission:publish articles']], function You can specify multiple roles or permissions with a `|` (pipe) character, which is treated as `OR`: ```php -Route::group(['middleware' => ['role:super-admin|writer']], function () { +Route::group(['middleware' => ['role:manager|writer']], function () { // }); @@ -73,7 +73,7 @@ Route::group(['middleware' => ['permission:publish articles|edit articles']], fu // }); -Route::group(['middleware' => ['role_or_permission:super-admin|edit articles']], function () { +Route::group(['middleware' => ['role_or_permission:manager|edit articles']], function () { // }); ``` @@ -85,14 +85,14 @@ You can protect your controllers similarly, by setting desired middleware in the ```php public function __construct() { - $this->middleware(['role:super-admin','permission:publish articles|edit articles']); + $this->middleware(['role:manager','permission:publish articles|edit articles']); } ``` ```php public function __construct() { - $this->middleware(['role_or_permission:super-admin|edit articles']); + $this->middleware(['role_or_permission:manager|edit articles']); } ``` @@ -104,7 +104,7 @@ All of the middlewares can also be applied by calling the static `using` method, which accepts either a `|`-separated string or an array as input. ```php -Route::group(['middleware' => [\Spatie\Permission\Middlewares\RoleMiddleware::using('super-admin')]], function () { +Route::group(['middleware' => [\Spatie\Permission\Middlewares\RoleMiddleware::using('manager')]], function () { // }); @@ -112,7 +112,7 @@ Route::group(['middleware' => [\Spatie\Permission\Middlewares\PermissionMiddlewa // }); -Route::group(['middleware' => [\Spatie\Permission\Middlewares\RoleOrPermissionMiddleware::using(['super-admin', 'edit articles'])]], function () { +Route::group(['middleware' => [\Spatie\Permission\Middlewares\RoleOrPermissionMiddleware::using(['manager', 'edit articles'])]], function () { // }); ``` From 06714c7da6eeb1ae0947734692a684fb259fb864 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 21 Aug 2023 11:50:18 -0400 Subject: [PATCH 0740/1013] [Docs] Specifying a guard in middleware calls --- docs/basic-usage/middleware.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index 37f1dd850..e7f2ea773 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -49,6 +49,11 @@ Route::group(['middleware' => ['role:manager']], function () { // }); +// for a specific guard: +Route::group(['middleware' => ['role:manager,api']], function () { + // +}); + Route::group(['middleware' => ['permission:publish articles']], function () { // }); @@ -73,6 +78,11 @@ Route::group(['middleware' => ['permission:publish articles|edit articles']], fu // }); +// for a specific guard +Route::group(['middleware' => ['permission:publish articles|edit articles,api']], function () { + // +}); + Route::group(['middleware' => ['role_or_permission:manager|edit articles']], function () { // }); From 025d56b68e2eec3aa76734c771a18b5fe09f6ea6 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 21 Aug 2023 12:56:17 -0400 Subject: [PATCH 0741/1013] Drop PHP 7.4 support (#2485) (Note: Laravel 8.12 minimum is required for using PHP 8.0) --- .../{run-tests-L8.yml => run-tests.yml} | 20 ++++++++----------- composer.json | 12 +++++------ docs/installation-laravel.md | 2 +- 3 files changed, 15 insertions(+), 19 deletions(-) rename .github/workflows/{run-tests-L8.yml => run-tests.yml} (78%) diff --git a/.github/workflows/run-tests-L8.yml b/.github/workflows/run-tests.yml similarity index 78% rename from .github/workflows/run-tests-L8.yml rename to .github/workflows/run-tests.yml index 4d8f8e774..f4d0b4e92 100644 --- a/.github/workflows/run-tests-L8.yml +++ b/.github/workflows/run-tests.yml @@ -9,23 +9,19 @@ jobs: strategy: fail-fast: false matrix: - php: [8.2, 8.1, 8.0, 7.4] - laravel: [10.*, 9.*, 8.*] + php: [8.2, 8.1, 8.0] + laravel: ["^10.0", "^9.0", "^8.12"] dependency-version: [prefer-lowest, prefer-stable] include: - - laravel: 10.* + - laravel: "^10.0" testbench: 8.* - - laravel: 9.* + - laravel: "^9.0" testbench: 7.* - - laravel: 8.* - testbench: 6.23 + - laravel: "^8.12" + testbench: "^6.23" exclude: - - laravel: 10.* + - laravel: "^10.0" php: 8.0 - - laravel: 10.* - php: 7.4 - - laravel: 9.* - php: 7.4 name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} @@ -42,7 +38,7 @@ jobs: - name: Install dependencies (remove passport) run: composer remove --dev laravel/passport --no-interaction --no-update - if: matrix.laravel == '8.*' + if: matrix.laravel == '^8.12' - name: Install dependencies run: | diff --git a/composer.json b/composer.json index 4e31db50e..e7af6faa2 100644 --- a/composer.json +++ b/composer.json @@ -22,15 +22,15 @@ ], "homepage": "https://github.com/spatie/laravel-permission", "require": { - "php": "^7.4|^8.0", - "illuminate/auth": "^8.0|^9.0|^10.0", - "illuminate/container": "^8.0|^9.0|^10.0", - "illuminate/contracts": "^8.0|^9.0|^10.0", - "illuminate/database": "^8.0|^9.0|^10.0" + "php": "^8.0", + "illuminate/auth": "^8.12|^9.0|^10.0", + "illuminate/container": "^8.12|^9.0|^10.0", + "illuminate/contracts": "^8.12|^9.0|^10.0", + "illuminate/database": "^8.12|^9.0|^10.0" }, "require-dev": { "laravel/passport": "^11.0", - "orchestra/testbench": "^6.0|^7.0|^8.0", + "orchestra/testbench": "^6.23|^7.0|^8.0", "phpunit/phpunit": "^9.4" }, "minimum-stability": "dev", diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 8ace927b3..21e31edba 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -9,7 +9,7 @@ This package can be used with Laravel 6.0 or higher. Package Version | Laravel Version ----------------|----------- - ^6.0 | 8,9,10 + ^6.0 | 8,9,10 (PHP 8.0+) ^5.8 | 7,8,9,10 ^5.7 | 7,8,9 ^5.4-^5.6 | 7,8 From 4cb45949fabb6133ecb83a6299e47e7ffc16cc27 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 21 Aug 2023 13:25:47 -0400 Subject: [PATCH 0742/1013] Update contracts to allow for UUID (#2480) * Update contracts to allow for UUID --- docs/upgrading.md | 2 +- src/Contracts/Permission.php | 16 ++++++++-------- src/Contracts/Role.php | 15 ++++++--------- src/Models/Permission.php | 10 +++------- src/Models/Role.php | 13 ++++--------- 5 files changed, 22 insertions(+), 34 deletions(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index f44ec1172..3709250ff 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -32,7 +32,7 @@ There are a few breaking-changes when upgrading to v6, but most of them won't af eg: if you have a custom model you will need to make changes, including accessing the model using `$this->permissionClass::` syntax (eg: using `::` instead of `->`) in all the overridden methods that make use of the models. Be sure to compare your custom models with originals to see what else may have changed. -2. If you have a custom Role model and (in the rare case that you might) have overridden the `hasPermissionTo()` method in it, you will need to update its method signature to `hasPermissionTo($permission, $guardName = null):bool`. See PR #2380. +2. Model and Contract/Interface updates. Both the Permission and Role contracts have been updated with syntax changes to method signatures, so if you have implemented those contracts in any models, you will need to update the function signatures. Further, if you have extended the Role or Permission models you will need to check any methods you have overridden and update method signatures. See PR #2380 and #2480 especially. 3. Migrations will need to be upgraded. (They have been updated to anonymous-class syntax that was introduced in Laravel 8, AND some structural coding changes in the registrar class changed the way we extracted configuration settings in the migration files.) If you had not customized it from the original then replacing the contents of the file should be straightforward. Usually the only customization is if you've switched to UUIDs or customized MySQL index name lengths. **If you get the following error, it means your migration file needs upgrading: `Error: Access to undeclared static property Spatie\Permission\PermissionRegistrar::$pivotPermission`** diff --git a/src/Contracts/Permission.php b/src/Contracts/Permission.php index 8b3d14e18..a2444adeb 100644 --- a/src/Contracts/Permission.php +++ b/src/Contracts/Permission.php @@ -5,9 +5,9 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; /** - * @property int $id + * @property int|string $id * @property string $name - * @property string $guard_name + * @property string|null $guard_name * * @mixin \Spatie\Permission\Models\Permission */ @@ -21,25 +21,25 @@ public function roles(): BelongsToMany; /** * Find a permission by its name. * - * @param string|null $guardName + * @return \Spatie\Permission\Contracts\Permission * * @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist */ - public static function findByName(string $name, $guardName): self; + public static function findByName(string $name, ?string $guardName): self; /** * Find a permission by its id. * - * @param string|null $guardName + * @return \Spatie\Permission\Contracts\Permission * * @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist */ - public static function findById(int $id, $guardName): self; + public static function findById(int|string $id, ?string $guardName): self; /** * Find or Create a permission by its name and guard name. * - * @param string|null $guardName + * @return \Spatie\Permission\Contracts\Permission */ - public static function findOrCreate(string $name, $guardName): self; + public static function findOrCreate(string $name, ?string $guardName): self; } diff --git a/src/Contracts/Role.php b/src/Contracts/Role.php index 04009de37..5745dd6b3 100644 --- a/src/Contracts/Role.php +++ b/src/Contracts/Role.php @@ -5,9 +5,9 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; /** - * @property int $id + * @property int|string $id * @property string $name - * @property string $guard_name + * @property string|null $guard_name * * @mixin \Spatie\Permission\Models\Role */ @@ -21,35 +21,32 @@ public function permissions(): BelongsToMany; /** * Find a role by its name and guard name. * - * @param string|null $guardName * @return \Spatie\Permission\Contracts\Role * * @throws \Spatie\Permission\Exceptions\RoleDoesNotExist */ - public static function findByName(string $name, $guardName): self; + public static function findByName(string $name, ?string $guardName): self; /** * Find a role by its id and guard name. * - * @param string|null $guardName * @return \Spatie\Permission\Contracts\Role * * @throws \Spatie\Permission\Exceptions\RoleDoesNotExist */ - public static function findById(int $id, $guardName): self; + public static function findById(int|string $id, ?string $guardName): self; /** * Find or create a role by its name and guard name. * - * @param string|null $guardName * @return \Spatie\Permission\Contracts\Role */ - public static function findOrCreate(string $name, $guardName): self; + public static function findOrCreate(string $name, ?string $guardName): self; /** * Determine if the user may perform the given permission. * * @param string|\Spatie\Permission\Contracts\Permission $permission */ - public function hasPermissionTo($permission): bool; + public function hasPermissionTo($permission, ?string $guardName): bool; } diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 21ce630ea..95df94fb6 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -82,12 +82,11 @@ public function users(): BelongsToMany /** * Find a permission by its name (and optionally guardName). * - * @param string|null $guardName * @return PermissionContract|Permission * * @throws PermissionDoesNotExist */ - public static function findByName(string $name, $guardName = null): PermissionContract + public static function findByName(string $name, string $guardName = null): PermissionContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); $permission = static::getPermission(['name' => $name, 'guard_name' => $guardName]); @@ -101,13 +100,11 @@ public static function findByName(string $name, $guardName = null): PermissionCo /** * Find a permission by its id (and optionally guardName). * - * @param int|string $id - * @param string|null $guardName * @return PermissionContract|Permission * * @throws PermissionDoesNotExist */ - public static function findById($id, $guardName = null): PermissionContract + public static function findById(int|string $id, string $guardName = null): PermissionContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); $permission = static::getPermission([(new static())->getKeyName() => $id, 'guard_name' => $guardName]); @@ -122,10 +119,9 @@ public static function findById($id, $guardName = null): PermissionContract /** * Find or create permission by its name (and optionally guardName). * - * @param string|null $guardName * @return PermissionContract|Permission */ - public static function findOrCreate(string $name, $guardName = null): PermissionContract + public static function findOrCreate(string $name, string $guardName = null): PermissionContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); $permission = static::getPermission(['name' => $name, 'guard_name' => $guardName]); diff --git a/src/Models/Role.php b/src/Models/Role.php index d96102a2f..4fd6e782e 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -91,12 +91,11 @@ public function users(): BelongsToMany /** * Find a role by its name and guard name. * - * @param string|null $guardName * @return RoleContract|Role * * @throws RoleDoesNotExist */ - public static function findByName(string $name, $guardName = null): RoleContract + public static function findByName(string $name, string $guardName = null): RoleContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); @@ -112,11 +111,9 @@ public static function findByName(string $name, $guardName = null): RoleContract /** * Find a role by its id (and optionally guardName). * - * @param int|string $id - * @param string|null $guardName * @return RoleContract|Role */ - public static function findById($id, $guardName = null): RoleContract + public static function findById(int|string $id, string $guardName = null): RoleContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); @@ -132,10 +129,9 @@ public static function findById($id, $guardName = null): RoleContract /** * Find or create role by its name (and optionally guardName). * - * @param string|null $guardName * @return RoleContract|Role */ - public static function findOrCreate(string $name, $guardName = null): RoleContract + public static function findOrCreate(string $name, string $guardName = null): RoleContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); @@ -177,11 +173,10 @@ protected static function findByParam(array $params = []): ?RoleContract * Determine if the role may perform the given permission. * * @param string|int|Permission|\BackedEnum $permission - * @param string|null $guardName * * @throws PermissionDoesNotExist|GuardDoesNotMatch */ - public function hasPermissionTo($permission, $guardName = null): bool + public function hasPermissionTo($permission, string $guardName = null): bool { if ($this->getWildcardClass()) { return $this->hasWildcardPermission($permission, $guardName); From 46e9df3bc21380e23b6c314109b100ef49e2d89c Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 21 Aug 2023 13:37:08 -0400 Subject: [PATCH 0743/1013] [Docs] Update v6 upgrade instructions --- docs/upgrading.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index 3709250ff..ade6e7fd4 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -15,13 +15,15 @@ ALL upgrades of this package should follow these steps: 3. Config file. Incorporate any changes to the permission.php config file, updating your existing file. (It may be easiest to make a backup copy of your existing file, re-publish it from this package, and then re-make your customizations to it.) -4. Models. If you have made any custom Models from this package into your own app, compare the old and new models and apply any relevant updates to your custom models. +4. Models. If you have made any custom Models by extending them into your own app, compare the package's old and new models and apply any relevant updates to your custom models. 5. Custom Methods/Traits. If you have overridden any methods from this package's Traits, compare the old and new traits, and apply any relevant updates to your overridden methods. -6. Apply any version-specific special updates as outlined below... +6. Contract/Interface updates. If you have implemented this package's contracts in any models, check to see if there were any changes to method signatures. Mismatches will trigger PHP errors. -7. Review the changelog, which details all the changes: [CHANGELOG](https://github.com/spatie/laravel-permission/blob/main/CHANGELOG.md) +7. Apply any version-specific special updates as outlined below... + +8. Review the changelog, which details all the changes: [CHANGELOG](https://github.com/spatie/laravel-permission/blob/main/CHANGELOG.md) and/or consult the [Release Notes](https://github.com/spatie/laravel-permission/releases) @@ -30,9 +32,10 @@ There are a few breaking-changes when upgrading to v6, but most of them won't af 1. If you have overridden the `getPermissionClass()` or `getRoleClass()` methods or have custom Models, you will need to revisit those customizations. See PR #2368 for details. eg: if you have a custom model you will need to make changes, including accessing the model using `$this->permissionClass::` syntax (eg: using `::` instead of `->`) in all the overridden methods that make use of the models. -Be sure to compare your custom models with originals to see what else may have changed. -2. Model and Contract/Interface updates. Both the Permission and Role contracts have been updated with syntax changes to method signatures, so if you have implemented those contracts in any models, you will need to update the function signatures. Further, if you have extended the Role or Permission models you will need to check any methods you have overridden and update method signatures. See PR #2380 and #2480 especially. + Be sure to compare your custom models with originals to see what else may have changed. + +2. Model and Contract/Interface updates. The Role and Permission Models and Contracts/Interfaces have been updated with syntax changes to method signatures. Update any models you have extended, or contracts implemented, accordingly. See PR #2380 and #2480 for some of the specifics. 3. Migrations will need to be upgraded. (They have been updated to anonymous-class syntax that was introduced in Laravel 8, AND some structural coding changes in the registrar class changed the way we extracted configuration settings in the migration files.) If you had not customized it from the original then replacing the contents of the file should be straightforward. Usually the only customization is if you've switched to UUIDs or customized MySQL index name lengths. **If you get the following error, it means your migration file needs upgrading: `Error: Access to undeclared static property Spatie\Permission\PermissionRegistrar::$pivotPermission`** @@ -41,7 +44,9 @@ Be sure to compare your custom models with originals to see what else may have c 5. In the unlikely event that you have customized the Wildcard Permissions feature by extending the `WildcardPermission` model, please note that the public interface has changed and you will need to update your extended model with the new method signatures. -6. Test suites. If you have tests which manually clear the permission cache and re-register permissions, you no longer need to call `\Spatie\Permission\PermissionRegistrar::class)->registerPermissions();`. Such calls MUST be deleted from your tests. +6. Test suites. If you have tests which manually clear the permission cache and re-register permissions, you no longer need to call `\Spatie\Permission\PermissionRegistrar::class)->registerPermissions();`. In fact, **calls to `->registerPermissions()` MUST be deleted from your tests**. + + (Calling `app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();` after creating roles and permissions is still okay and encouraged.) ## Upgrading from v4 to v5 From bd2c1e60dafb317083b491fdbe832448fa39f375 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 21 Aug 2023 13:57:33 -0400 Subject: [PATCH 0744/1013] Add cache heading in comments --- config/permission.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/permission.php b/config/permission.php index 0507baec5..9c0532d89 100644 --- a/config/permission.php +++ b/config/permission.php @@ -159,6 +159,8 @@ */ // 'permission.wildcard_permission' => Spatie\Permission\WildcardPermission::class, + /* Cache-specific settings */ + 'cache' => [ /* From ba5a7be2f6033768cd76748f484b41c8aa2bb226 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 25 Aug 2023 17:31:46 -0400 Subject: [PATCH 0745/1013] [docs] remove registerPolicies call that was removed with L10.0.2 The line was previously there just to be a goalpost, to give an idea where to put the suggested new code. --- docs/basic-usage/super-admin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/super-admin.md b/docs/basic-usage/super-admin.md index 344a9a41e..70cb0b5a1 100644 --- a/docs/basic-usage/super-admin.md +++ b/docs/basic-usage/super-admin.md @@ -18,7 +18,7 @@ class AuthServiceProvider extends ServiceProvider { public function boot() { - $this->registerPolicies(); +//... // Implicitly grant "Super Admin" role all permissions // This works in the app by using gate-related functions like auth()->user->can() and @can() From 7fdf95db4993f4dee3447e016771d9e0074fb7e9 Mon Sep 17 00:00:00 2001 From: drbyte Date: Wed, 30 Aug 2023 23:42:18 +0000 Subject: [PATCH 0746/1013] Update CHANGELOG --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a9a38279..f1e5f9f0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.11.0 - 2023-08-30 + +### What's Changed + +- [V5] Avoid triggering `eloquent.retrieved` event by @erikn69 in https://github.com/spatie/laravel-permission/pull/2490 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.10.2...5.11.0 + ## 5.10.2 - 2023-07-04 ### What's Changed @@ -618,6 +626,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -674,6 +683,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From 4df31adf9a3564fef3f4a43ce30be421ac9f04de Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 1 Sep 2023 14:13:06 -0400 Subject: [PATCH 0747/1013] Add note about "1071 Specified key was too long" --- docs/installation-laravel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 21e31edba..8fb36b812 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -51,7 +51,7 @@ Package Version | Laravel Version - must set `'teams' => true,` - and (optional) you may set `team_foreign_key` name in the config file if you want to use a custom foreign key in your database for teams - - **If you are using MySQL 8**, look at the migration files for notes about MySQL 8 to set/limit the index key length, and edit accordingly. + - **If you are using MySQL 8**, look at the migration files for notes about MySQL 8 to set/limit the index key length, and edit accordingly. If you get `ERROR: 1071 Specified key was too long` then you need to do this. 7. **Clear your config cache**. This package requires access to the `permission` config settings in order to run migrations. If you've been caching configurations locally, clear your config cache with either of these commands: From c98a8b3d571771d6fc52cf4a25a031682efb0004 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 1 Sep 2023 14:14:32 -0400 Subject: [PATCH 0748/1013] Note about Laravel versions --- docs/installation-laravel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 8fb36b812..39e8087ee 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -5,7 +5,7 @@ weight: 4 ## Laravel Version Compatibility -This package can be used with Laravel 6.0 or higher. +Choose the version of this package that suits your Laravel version. Package Version | Laravel Version ----------------|----------- From 465b9d7760a577630f846bc5bfcb0cebe1d347c3 Mon Sep 17 00:00:00 2001 From: Arthur LORENT Date: Fri, 1 Sep 2023 20:21:51 +0200 Subject: [PATCH 0749/1013] [Docs] Listen to DatabaseRefreshed rather than MigrationsEnded in TestCase (#2492) Listening to `DatabaseRefreshed` event rather than `MigrationsEnded` can avoid issues during testing after `artisan schema:dump` command has been executed. In fact, `MigrationsEnded` is not called when the migration is based on a `mysql-schema.sql` file and has no migration executed anymore. `DatabaseRefreshed` is always executed in both cases. --- docs/advanced-usage/testing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/advanced-usage/testing.md b/docs/advanced-usage/testing.md index f67ecd4a2..a98955951 100644 --- a/docs/advanced-usage/testing.md +++ b/docs/advanced-usage/testing.md @@ -22,10 +22,10 @@ In your tests simply add a `setUp()` instruction to re-register the permissions, ## Clear Cache When Using Seeders -If you are using Laravel's `LazilyRefreshDatabase` trait, you most likely want to avoid seeding permissions before every test, because that would negate the use of the `LazilyRefreshDatabase` trait. To overcome this, you should wrap your seeder in an event listener for the `MigrationsEnded` event: +If you are using Laravel's `LazilyRefreshDatabase` trait, you most likely want to avoid seeding permissions before every test, because that would negate the use of the `LazilyRefreshDatabase` trait. To overcome this, you should wrap your seeder in an event listener for the `DatabaseRefreshed` event: ```php -Event::listen(MigrationsEnded::class, function () { +Event::listen(DatabaseRefreshed::class, function () { $this->artisan('db:seed', ['--class' => RoleAndPermissionSeeder::class]); $this->app->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions(); }); From f50e7cc4fd09c459fa83884c1f770f714bd808d0 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Mon, 4 Sep 2023 11:51:57 -0500 Subject: [PATCH 0750/1013] Bump actions/checkout from 3 to 4 (#2493) --- .github/workflows/fix-php-code-style-issues.yml | 2 +- .github/workflows/phpstan.yml | 2 +- .github/workflows/run-tests.yml | 2 +- .github/workflows/test-cache-drivers.yml | 2 +- .github/workflows/update-changelog.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/fix-php-code-style-issues.yml b/.github/workflows/fix-php-code-style-issues.yml index 49d14b77e..3687af011 100644 --- a/.github/workflows/fix-php-code-style-issues.yml +++ b/.github/workflows/fix-php-code-style-issues.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.head_ref }} diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index acd8b906e..587e6c2d7 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -11,7 +11,7 @@ jobs: name: phpstan runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index f4d0b4e92..d119985db 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/test-cache-drivers.yml b/.github/workflows/test-cache-drivers.yml index ab49f2cf2..403f23db6 100644 --- a/.github/workflows/test-cache-drivers.yml +++ b/.github/workflows/test-cache-drivers.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index 74f888987..ab69c62c1 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -10,7 +10,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: main From 3cafa8dabf4a13cc9d6b0c8a9e636fc7c2642ea4 Mon Sep 17 00:00:00 2001 From: Erin Dalzell Date: Mon, 4 Sep 2023 21:00:30 -0400 Subject: [PATCH 0751/1013] Typehint BackedEnum (#2494) --- src/Traits/HasRoles.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 829afde22..3737b557d 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -163,7 +163,7 @@ private function collectRoles(...$roles): array /** * Assign the given role to the model. * - * @param array|string|int|Role|Collection ...$roles + * @param array|string|int|Role|Collection|\BackedEnum ...$roles * @return $this */ public function assignRole(...$roles) From 21829869f13a89b1e5dd4049443c97fab0bd5baa Mon Sep 17 00:00:00 2001 From: drbyte Date: Tue, 5 Sep 2023 01:01:13 +0000 Subject: [PATCH 0752/1013] Fix styling --- src/Contracts/Permission.php | 4 ---- src/Contracts/Role.php | 4 ---- tests/TestModels/Client.php | 2 +- tests/TestModels/UserWithoutHasRoles.php | 4 ++-- 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/Contracts/Permission.php b/src/Contracts/Permission.php index a2444adeb..111d2ed2e 100644 --- a/src/Contracts/Permission.php +++ b/src/Contracts/Permission.php @@ -21,7 +21,6 @@ public function roles(): BelongsToMany; /** * Find a permission by its name. * - * @return \Spatie\Permission\Contracts\Permission * * @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist */ @@ -30,7 +29,6 @@ public static function findByName(string $name, ?string $guardName): self; /** * Find a permission by its id. * - * @return \Spatie\Permission\Contracts\Permission * * @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist */ @@ -38,8 +36,6 @@ public static function findById(int|string $id, ?string $guardName): self; /** * Find or Create a permission by its name and guard name. - * - * @return \Spatie\Permission\Contracts\Permission */ public static function findOrCreate(string $name, ?string $guardName): self; } diff --git a/src/Contracts/Role.php b/src/Contracts/Role.php index 5745dd6b3..d00201a2a 100644 --- a/src/Contracts/Role.php +++ b/src/Contracts/Role.php @@ -21,7 +21,6 @@ public function permissions(): BelongsToMany; /** * Find a role by its name and guard name. * - * @return \Spatie\Permission\Contracts\Role * * @throws \Spatie\Permission\Exceptions\RoleDoesNotExist */ @@ -30,7 +29,6 @@ public static function findByName(string $name, ?string $guardName): self; /** * Find a role by its id and guard name. * - * @return \Spatie\Permission\Contracts\Role * * @throws \Spatie\Permission\Exceptions\RoleDoesNotExist */ @@ -38,8 +36,6 @@ public static function findById(int|string $id, ?string $guardName): self; /** * Find or create a role by its name and guard name. - * - * @return \Spatie\Permission\Contracts\Role */ public static function findOrCreate(string $name, ?string $guardName): self; diff --git a/tests/TestModels/Client.php b/tests/TestModels/Client.php index a920bddd0..267c36122 100644 --- a/tests/TestModels/Client.php +++ b/tests/TestModels/Client.php @@ -9,8 +9,8 @@ class Client extends BaseClient implements AuthorizableContract { - use HasRoles; use Authorizable; + use HasRoles; /** * Required to make clear that the client requires the api guard diff --git a/tests/TestModels/UserWithoutHasRoles.php b/tests/TestModels/UserWithoutHasRoles.php index dd9a29445..6cc1d840e 100644 --- a/tests/TestModels/UserWithoutHasRoles.php +++ b/tests/TestModels/UserWithoutHasRoles.php @@ -8,10 +8,10 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Auth\Access\Authorizable; -class UserWithoutHasRoles extends Model implements AuthorizableContract, AuthenticatableContract +class UserWithoutHasRoles extends Model implements AuthenticatableContract, AuthorizableContract { - use Authorizable; use Authenticatable; + use Authorizable; protected $fillable = ['email']; From 905be9d457a1a8d90da047361935ce9ccdc82a5d Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 4 Sep 2023 21:04:59 -0400 Subject: [PATCH 0753/1013] Typehint BackedEnum --- src/Traits/HasRoles.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 3737b557d..2fc31b012 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -136,7 +136,7 @@ public function scopeWithoutRole(Builder $query, $roles, $guard = null): Builder /** * Returns roles ids as array keys * - * @param array|string|int|Role|Collection|\BackedEnum $roles + * @param string|int|array|Role|Collection|\BackedEnum $roles */ private function collectRoles(...$roles): array { @@ -163,7 +163,7 @@ private function collectRoles(...$roles): array /** * Assign the given role to the model. * - * @param array|string|int|Role|Collection|\BackedEnum ...$roles + * @param string|int|array|Role|Collection|\BackedEnum ...$roles * @return $this */ public function assignRole(...$roles) @@ -221,7 +221,7 @@ public function removeRole($role) /** * Remove all current roles and set the given ones. * - * @param array|Role|Collection|string|int ...$roles + * @param string|int|array|Role|Collection|\BackedEnum ...$roles * @return $this */ public function syncRoles(...$roles) @@ -293,7 +293,7 @@ public function hasRole($roles, string $guard = null): bool * * Alias to hasRole() but without Guard controls * - * @param string|int|array|Role|Collection $roles + * @param string|int|array|Role|Collection|\BackedEnum $roles */ public function hasAnyRole(...$roles): bool { From 7c454fa1e01f4ee81fe5fa98cbc78e4e8a7b0688 Mon Sep 17 00:00:00 2001 From: Erin Dalzell Date: Tue, 12 Sep 2023 10:55:16 -0700 Subject: [PATCH 0754/1013] Missing typehint (#2495) --- src/Traits/HasRoles.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 2fc31b012..1be3abb9d 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -203,7 +203,7 @@ function ($object) use ($roles, $model, $teamPivot) { /** * Revoke the given role from the model. * - * @param string|int|Role $role + * @param string|int|Role|\BackedEnum $role */ public function removeRole($role) { From 982d4d407643182a335774e53b92afc344828a2d Mon Sep 17 00:00:00 2001 From: drbyte Date: Tue, 12 Sep 2023 17:55:39 +0000 Subject: [PATCH 0755/1013] Fix styling --- src/PermissionRegistrar.php | 2 +- src/Traits/HasRoles.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index bc434e571..90f0d199a 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -398,7 +398,7 @@ public static function isUid($value): bool } // check if is ULID - $ulid = 26 == strlen($value) && 26 == strspn($value, '0123456789ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz') && $value[0] <= '7'; + $ulid = strlen($value) == 26 && strspn($value, '0123456789ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz') == 26 && $value[0] <= '7'; if ($ulid) { return true; } diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 1be3abb9d..e0ac80301 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -244,7 +244,7 @@ public function hasRole($roles, string $guard = null): bool { $this->loadMissing('roles'); - if (is_string($roles) && false !== strpos($roles, '|')) { + if (is_string($roles) && strpos($roles, '|') !== false) { $roles = $this->convertPipeToArray($roles); } @@ -313,7 +313,7 @@ public function hasAllRoles($roles, string $guard = null): bool $roles = $roles->value; } - if (is_string($roles) && false !== strpos($roles, '|')) { + if (is_string($roles) && strpos($roles, '|') !== false) { $roles = $this->convertPipeToArray($roles); } @@ -351,7 +351,7 @@ public function hasExactRoles($roles, string $guard = null): bool { $this->loadMissing('roles'); - if (is_string($roles) && false !== strpos($roles, '|')) { + if (is_string($roles) && strpos($roles, '|') !== false) { $roles = $this->convertPipeToArray($roles); } From 8f15d1846aaa5039292978e7775bbf0f2ca73cc8 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Wed, 13 Sep 2023 13:27:46 -0500 Subject: [PATCH 0756/1013] [v6] Sync v5 fix: Avoid triggering eloquent.retrieved event #2498 --- src/PermissionRegistrar.php | 13 ++++++------- src/Traits/HasPermissions.php | 12 ++++-------- src/Traits/HasRoles.php | 3 +-- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 90f0d199a..30c6d5676 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -354,12 +354,11 @@ private function getSerializedRoleRelation($permission): array private function getHydratedPermissionCollection(): Collection { - $permissionClass = $this->getPermissionClass(); - $permissionInstance = new $permissionClass(); + $permissionInstance = new ($this->getPermissionClass())(); return Collection::make(array_map( - fn ($item) => $permissionInstance - ->newFromBuilder($this->aliasedArray(array_diff_key($item, ['r' => 0]))) + fn ($item) => $permissionInstance->newInstance([], true) + ->setRawAttributes($this->aliasedArray(array_diff_key($item, ['r' => 0])), true) ->setRelation('roles', $this->getHydratedRoleCollection($item['r'] ?? [])), $this->permissions['permissions'] )); @@ -374,11 +373,11 @@ private function getHydratedRoleCollection(array $roles): Collection private function hydrateRolesCache(): void { - $roleClass = $this->getRoleClass(); - $roleInstance = new $roleClass(); + $roleInstance = new ($this->getRoleClass())(); array_map(function ($item) use ($roleInstance) { - $role = $roleInstance->newFromBuilder($this->aliasedArray($item)); + $role = $roleInstance->newInstance([], true) + ->setRawAttributes($this->aliasedArray($item), true); $this->cachedRoles[$role->getKey()] = $role; }, $this->permissions['roles']); diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 1404bb501..21b2d2420 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -101,10 +101,8 @@ public function scopePermission(Builder $query, $permissions): Builder { $permissions = $this->convertToPermissionModels($permissions); - $permissionClass = $this->getPermissionClass(); - $permissionKey = (new $permissionClass())->getKeyName(); - $roleClass = is_a($this, Role::class) ? static::class : $this->getRoleClass(); - $roleKey = (new $roleClass())->getKeyName(); + $permissionKey = (new ($this->getPermissionClass())())->getKeyName(); + $roleKey = (new (is_a($this, Role::class) ? static::class : $this->getRoleClass())())->getKeyName(); $rolesWithPermissions = is_a($this, Role::class) ? [] : array_unique( array_reduce($permissions, fn ($result, $permission) => array_merge($result, $permission->roles->all()), []) @@ -132,10 +130,8 @@ public function scopeWithoutPermission(Builder $query, $permissions, $debug = fa { $permissions = $this->convertToPermissionModels($permissions); - $permissionClass = $this->getPermissionClass(); - $permissionKey = (new $permissionClass())->getKeyName(); - $roleClass = is_a($this, Role::class) ? static::class : $this->getRoleClass(); - $roleKey = (new $roleClass())->getKeyName(); + $permissionKey = (new ($this->getPermissionClass())())->getKeyName(); + $roleKey = (new (is_a($this, Role::class) ? static::class : $this->getRoleClass())())->getKeyName(); $rolesWithPermissions = is_a($this, Role::class) ? [] : array_unique( array_reduce($permissions, fn ($result, $permission) => array_merge($result, $permission->roles->all()), []) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index e0ac80301..78ec59812 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -253,8 +253,7 @@ public function hasRole($roles, string $guard = null): bool } if (is_int($roles) || PermissionRegistrar::isUid($roles)) { - $roleClass = $this->getRoleClass(); - $key = (new $roleClass())->getKeyName(); + $key = (new ($this->getRoleClass())())->getKeyName(); return $guard ? $this->roles->where('guard_name', $guard)->contains($key, $roles) From 5659d6abf6f66f662e9562b5409a52270178da48 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Wed, 13 Sep 2023 16:21:25 -0500 Subject: [PATCH 0757/1013] Reuse code on withoutRole and withoutPermission (#2500) --- src/Traits/HasPermissions.php | 29 ++++++----------------------- src/Traits/HasRoles.php | 33 +++++---------------------------- 2 files changed, 11 insertions(+), 51 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 21b2d2420..eee592af3 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -96,8 +96,9 @@ public function permissions(): BelongsToMany * Scope the model query to certain permissions only. * * @param string|int|array|Permission|Collection|\BackedEnum $permissions + * @param bool $without */ - public function scopePermission(Builder $query, $permissions): Builder + public function scopePermission(Builder $query, $permissions, $without = false): Builder { $permissions = $this->convertToPermissionModels($permissions); @@ -109,11 +110,11 @@ public function scopePermission(Builder $query, $permissions): Builder ); return $query->where(fn (Builder $query) => $query - ->whereHas('permissions', fn (Builder $subQuery) => $subQuery + ->{! $without ? 'whereHas' : 'whereDoesntHave'}('permissions', fn (Builder $subQuery) => $subQuery ->whereIn(config('permission.table_names.permissions').".$permissionKey", \array_column($permissions, $permissionKey)) ) ->when(count($rolesWithPermissions), fn ($whenQuery) => $whenQuery - ->orWhereHas('roles', fn (Builder $subQuery) => $subQuery + ->{! $without ? 'orWhereHas' : 'whereDoesntHave'}('roles', fn (Builder $subQuery) => $subQuery ->whereIn(config('permission.table_names.roles').".$roleKey", \array_column($rolesWithPermissions, $roleKey)) ) ) @@ -126,27 +127,9 @@ public function scopePermission(Builder $query, $permissions): Builder * * @param string|int|array|Permission|Collection|\BackedEnum $permissions */ - public function scopeWithoutPermission(Builder $query, $permissions, $debug = false): Builder + public function scopeWithoutPermission(Builder $query, $permissions): Builder { - $permissions = $this->convertToPermissionModels($permissions); - - $permissionKey = (new ($this->getPermissionClass())())->getKeyName(); - $roleKey = (new (is_a($this, Role::class) ? static::class : $this->getRoleClass())())->getKeyName(); - - $rolesWithPermissions = is_a($this, Role::class) ? [] : array_unique( - array_reduce($permissions, fn ($result, $permission) => array_merge($result, $permission->roles->all()), []) - ); - - return $query->where(fn (Builder $query) => $query - ->whereDoesntHave('permissions', fn (Builder $subQuery) => $subQuery - ->whereIn(config('permission.table_names.permissions').".$permissionKey", \array_column($permissions, $permissionKey)) - ) - ->when(count($rolesWithPermissions), fn ($whenQuery) => $whenQuery - ->whereDoesntHave('roles', fn (Builder $subQuery) => $subQuery - ->whereIn(config('permission.table_names.roles').".$roleKey", \array_column($rolesWithPermissions, $roleKey)) - ) - ) - ); + return $this->scopePermission($query, $permissions, true); } /** diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 78ec59812..f02f7c160 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -70,8 +70,9 @@ public function roles(): BelongsToMany * * @param string|int|array|Role|Collection|\BackedEnum $roles * @param string $guard + * @param bool $without */ - public function scopeRole(Builder $query, $roles, $guard = null): Builder + public function scopeRole(Builder $query, $roles, $guard = null, $without = false): Builder { if ($roles instanceof Collection) { $roles = $roles->all(); @@ -91,10 +92,9 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder return $this->getRoleClass()::{$method}($role, $guard ?: $this->getDefaultGuardName()); }, Arr::wrap($roles)); - $roleClass = $this->getRoleClass(); - $key = (new $roleClass())->getKeyName(); + $key = (new ($this->getRoleClass())())->getKeyName(); - return $query->whereHas('roles', fn (Builder $subQuery) => $subQuery + return $query->{! $without ? 'whereHas' : 'whereDoesntHave'}('roles', fn (Builder $subQuery) => $subQuery ->whereIn(config('permission.table_names.roles').".$key", \array_column($roles, $key)) ); } @@ -107,30 +107,7 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder */ public function scopeWithoutRole(Builder $query, $roles, $guard = null): Builder { - if ($roles instanceof Collection) { - $roles = $roles->all(); - } - - $roles = array_map(function ($role) use ($guard) { - if ($role instanceof Role) { - return $role; - } - - if ($role instanceof \BackedEnum) { - $role = $role->value; - } - - $method = is_int($role) || PermissionRegistrar::isUid($role) ? 'findById' : 'findByName'; - - return $this->getRoleClass()::{$method}($role, $guard ?: $this->getDefaultGuardName()); - }, Arr::wrap($roles)); - - $roleClass = $this->getRoleClass(); - $key = (new $roleClass())->getKeyName(); - - return $query->whereDoesntHave('roles', fn (Builder $subQuery) => $subQuery - ->whereIn(config('permission.table_names.roles').".$key", \array_column($roles, $key)) - ); + return $this->scopeRole($query, $roles, $guard, true); } /** From d5eb73977b34248287bee8f530a69ad44ecfc3ba Mon Sep 17 00:00:00 2001 From: drbyte Date: Wed, 13 Sep 2023 21:21:50 +0000 Subject: [PATCH 0758/1013] Fix styling --- src/Traits/HasPermissions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index eee592af3..2618b380b 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -111,11 +111,11 @@ public function scopePermission(Builder $query, $permissions, $without = false): return $query->where(fn (Builder $query) => $query ->{! $without ? 'whereHas' : 'whereDoesntHave'}('permissions', fn (Builder $subQuery) => $subQuery - ->whereIn(config('permission.table_names.permissions').".$permissionKey", \array_column($permissions, $permissionKey)) + ->whereIn(config('permission.table_names.permissions').".$permissionKey", \array_column($permissions, $permissionKey)) ) ->when(count($rolesWithPermissions), fn ($whenQuery) => $whenQuery ->{! $without ? 'orWhereHas' : 'whereDoesntHave'}('roles', fn (Builder $subQuery) => $subQuery - ->whereIn(config('permission.table_names.roles').".$roleKey", \array_column($rolesWithPermissions, $roleKey)) + ->whereIn(config('permission.table_names.roles').".$roleKey", \array_column($rolesWithPermissions, $roleKey)) ) ) ); From f9069aa5e0f9ca63d45cf406e5cb2d0f8907b0ed Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 18 Sep 2023 16:01:04 -0400 Subject: [PATCH 0759/1013] Rename "Middlewares" namespace to "Middleware" (#2499) --- docs/basic-usage/middleware.md | 14 +++++++------- docs/basic-usage/passport.md | 2 +- docs/installation-lumen.md | 4 ++-- docs/upgrading.md | 10 +++++++--- .../PermissionMiddleware.php | 2 +- src/{Middlewares => Middleware}/RoleMiddleware.php | 2 +- .../RoleOrPermissionMiddleware.php | 2 +- tests/PermissionMiddlewareTest.php | 8 ++++---- tests/RoleMiddlewareTest.php | 8 ++++---- tests/RoleOrPermissionMiddlewareTest.php | 8 ++++---- tests/WildcardMiddlewareTest.php | 6 +++--- 11 files changed, 35 insertions(+), 31 deletions(-) rename src/{Middlewares => Middleware}/PermissionMiddleware.php (97%) rename src/{Middlewares => Middleware}/RoleMiddleware.php (97%) rename src/{Middlewares => Middleware}/RoleOrPermissionMiddleware.php (97%) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index e7f2ea773..592fe8343 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -34,9 +34,9 @@ Note the property name difference between Laravel 10 and older versions of Larav // Laravel 10+ uses $middlewareAliases = [ protected $middlewareAliases = [ // ... - 'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class, - 'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class, - 'role_or_permission' => \Spatie\Permission\Middlewares\RoleOrPermissionMiddleware::class, + 'role' => \Spatie\Permission\Middleware\RoleMiddleware::class, + 'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class, + 'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class, ]; ``` @@ -110,19 +110,19 @@ public function __construct() ## Use middleware static methods -All of the middlewares can also be applied by calling the static `using` method, +All of the middleware can also be applied by calling the static `using` method, which accepts either a `|`-separated string or an array as input. ```php -Route::group(['middleware' => [\Spatie\Permission\Middlewares\RoleMiddleware::using('manager')]], function () { +Route::group(['middleware' => [\Spatie\Permission\Middleware\RoleMiddleware::using('manager')]], function () { // }); -Route::group(['middleware' => [\Spatie\Permission\Middlewares\PermissionMiddleware::using('publish articles|edit articles')]], function () { +Route::group(['middleware' => [\Spatie\Permission\Middleware\PermissionMiddleware::using('publish articles|edit articles')]], function () { // }); -Route::group(['middleware' => [\Spatie\Permission\Middlewares\RoleOrPermissionMiddleware::using(['manager', 'edit articles'])]], function () { +Route::group(['middleware' => [\Spatie\Permission\Middleware\RoleOrPermissionMiddleware::using(['manager', 'edit articles'])]], function () { // }); ``` diff --git a/docs/basic-usage/passport.md b/docs/basic-usage/passport.md index a5a007009..4faa50fc3 100644 --- a/docs/basic-usage/passport.md +++ b/docs/basic-usage/passport.md @@ -39,7 +39,7 @@ The extended Client should either provide a `$guard_name` property or a `guardNa They should return a string that matches the [configured](https://laravel.com/docs/master/passport#installation) guard name for the passport driver. ## Middleware -All middlewares provided by this package work with the Client. +All middleware provided by this package work with the Client. Do make sure that you only wrap your routes in the [`client`](https://laravel.com/docs/master/passport#via-middleware) middleware and not the `auth:api` middleware as well. Wrapping routes in the `auth:api` middleware currently does not work for the Client Credentials Grant. diff --git a/docs/installation-lumen.md b/docs/installation-lumen.md index 70268977d..197ff31fe 100644 --- a/docs/installation-lumen.md +++ b/docs/installation-lumen.md @@ -34,8 +34,8 @@ Then, in `bootstrap/app.php`, uncomment the `auth` middleware, and register this ```php $app->routeMiddleware([ 'auth' => App\Http\Middleware\Authenticate::class, - 'permission' => Spatie\Permission\Middlewares\PermissionMiddleware::class, - 'role' => Spatie\Permission\Middlewares\RoleMiddleware::class, + 'permission' => Spatie\Permission\Middleware\PermissionMiddleware::class, + 'role' => Spatie\Permission\Middleware\RoleMiddleware::class, ]); ``` diff --git a/docs/upgrading.md b/docs/upgrading.md index ade6e7fd4..ab83fd790 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -40,11 +40,15 @@ eg: if you have a custom model you will need to make changes, including accessin 3. Migrations will need to be upgraded. (They have been updated to anonymous-class syntax that was introduced in Laravel 8, AND some structural coding changes in the registrar class changed the way we extracted configuration settings in the migration files.) If you had not customized it from the original then replacing the contents of the file should be straightforward. Usually the only customization is if you've switched to UUIDs or customized MySQL index name lengths. **If you get the following error, it means your migration file needs upgrading: `Error: Access to undeclared static property Spatie\Permission\PermissionRegistrar::$pivotPermission`** -4. NOTE: For consistency with `PermissionMiddleware`, the `RoleOrPermissionMiddleware` has switched from only checking permissions provided by this package to using `canAny()` to check against any abilities registered by your application. This may have the effect of granting those other abilities (such as Super Admin) when using the `RoleOrPermissionMiddleware`, which previously would have failed silently. +4. MIDDLEWARE: -5. In the unlikely event that you have customized the Wildcard Permissions feature by extending the `WildcardPermission` model, please note that the public interface has changed and you will need to update your extended model with the new method signatures. + 1. The `\Spatie\Permission\Middlewares\` namespace has been renamed to `\Spatie\Permission\Middleware\` (singular). Update your references to them in your `/app/Http/Kernel.php` and any routes that have the fully qualified path. -6. Test suites. If you have tests which manually clear the permission cache and re-register permissions, you no longer need to call `\Spatie\Permission\PermissionRegistrar::class)->registerPermissions();`. In fact, **calls to `->registerPermissions()` MUST be deleted from your tests**. + 2. NOTE: For consistency with `PermissionMiddleware`, the `RoleOrPermissionMiddleware` has switched from only checking permissions provided by this package to using `canAny()` to check against any abilities registered by your application. This may have the effect of granting those other abilities (such as Super Admin) when using the `RoleOrPermissionMiddleware`, which previously would have failed silently. + + 3. In the unlikely event that you have customized the Wildcard Permissions feature by extending the `WildcardPermission` model, please note that the public interface has changed and you will need to update your extended model with the new method signatures. + +5. Test suites. If you have tests which manually clear the permission cache and re-register permissions, you no longer need to call `\Spatie\Permission\PermissionRegistrar::class)->registerPermissions();`. In fact, **calls to `->registerPermissions()` MUST be deleted from your tests**. (Calling `app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();` after creating roles and permissions is still okay and encouraged.) diff --git a/src/Middlewares/PermissionMiddleware.php b/src/Middleware/PermissionMiddleware.php similarity index 97% rename from src/Middlewares/PermissionMiddleware.php rename to src/Middleware/PermissionMiddleware.php index 7d303261c..3e78f1d60 100644 --- a/src/Middlewares/PermissionMiddleware.php +++ b/src/Middleware/PermissionMiddleware.php @@ -1,6 +1,6 @@ assertSame( - 'Spatie\Permission\Middlewares\PermissionMiddleware:edit-articles', + 'Spatie\Permission\Middleware\PermissionMiddleware:edit-articles', PermissionMiddleware::using('edit-articles') ); $this->assertEquals( - 'Spatie\Permission\Middlewares\PermissionMiddleware:edit-articles,my-guard', + 'Spatie\Permission\Middleware\PermissionMiddleware:edit-articles,my-guard', PermissionMiddleware::using('edit-articles', 'my-guard') ); $this->assertEquals( - 'Spatie\Permission\Middlewares\PermissionMiddleware:edit-articles|edit-news', + 'Spatie\Permission\Middleware\PermissionMiddleware:edit-articles|edit-news', PermissionMiddleware::using(['edit-articles', 'edit-news']) ); } diff --git a/tests/RoleMiddlewareTest.php b/tests/RoleMiddlewareTest.php index 25f411ee1..7ffc3a5c6 100644 --- a/tests/RoleMiddlewareTest.php +++ b/tests/RoleMiddlewareTest.php @@ -9,7 +9,7 @@ use InvalidArgumentException; use Laravel\Passport\Passport; use Spatie\Permission\Exceptions\UnauthorizedException; -use Spatie\Permission\Middlewares\RoleMiddleware; +use Spatie\Permission\Middleware\RoleMiddleware; use Spatie\Permission\Tests\TestModels\UserWithoutHasRoles; class RoleMiddlewareTest extends TestCase @@ -332,15 +332,15 @@ public function user_can_access_role_with_guard_admin_while_login_using_admin_gu public function the_middleware_can_be_created_with_static_using_method() { $this->assertSame( - 'Spatie\Permission\Middlewares\RoleMiddleware:testAdminRole', + 'Spatie\Permission\Middleware\RoleMiddleware:testAdminRole', RoleMiddleware::using('testAdminRole') ); $this->assertEquals( - 'Spatie\Permission\Middlewares\RoleMiddleware:testAdminRole,my-guard', + 'Spatie\Permission\Middleware\RoleMiddleware:testAdminRole,my-guard', RoleMiddleware::using('testAdminRole', 'my-guard') ); $this->assertEquals( - 'Spatie\Permission\Middlewares\RoleMiddleware:testAdminRole|anotherRole', + 'Spatie\Permission\Middleware\RoleMiddleware:testAdminRole|anotherRole', RoleMiddleware::using(['testAdminRole', 'anotherRole']) ); } diff --git a/tests/RoleOrPermissionMiddlewareTest.php b/tests/RoleOrPermissionMiddlewareTest.php index a3de1054d..4d473aa4f 100644 --- a/tests/RoleOrPermissionMiddlewareTest.php +++ b/tests/RoleOrPermissionMiddlewareTest.php @@ -10,7 +10,7 @@ use InvalidArgumentException; use Laravel\Passport\Passport; use Spatie\Permission\Exceptions\UnauthorizedException; -use Spatie\Permission\Middlewares\RoleOrPermissionMiddleware; +use Spatie\Permission\Middleware\RoleOrPermissionMiddleware; use Spatie\Permission\Tests\TestModels\UserWithoutHasRoles; class RoleOrPermissionMiddlewareTest extends TestCase @@ -278,15 +278,15 @@ public function the_required_permissions_or_roles_can_be_displayed_in_the_except public function the_middleware_can_be_created_with_static_using_method() { $this->assertSame( - 'Spatie\Permission\Middlewares\RoleOrPermissionMiddleware:edit-articles', + 'Spatie\Permission\Middleware\RoleOrPermissionMiddleware:edit-articles', RoleOrPermissionMiddleware::using('edit-articles') ); $this->assertEquals( - 'Spatie\Permission\Middlewares\RoleOrPermissionMiddleware:edit-articles,my-guard', + 'Spatie\Permission\Middleware\RoleOrPermissionMiddleware:edit-articles,my-guard', RoleOrPermissionMiddleware::using('edit-articles', 'my-guard') ); $this->assertEquals( - 'Spatie\Permission\Middlewares\RoleOrPermissionMiddleware:edit-articles|testAdminRole', + 'Spatie\Permission\Middleware\RoleOrPermissionMiddleware:edit-articles|testAdminRole', RoleOrPermissionMiddleware::using(['edit-articles', 'testAdminRole']) ); } diff --git a/tests/WildcardMiddlewareTest.php b/tests/WildcardMiddlewareTest.php index f2d099ae8..dc359fe49 100644 --- a/tests/WildcardMiddlewareTest.php +++ b/tests/WildcardMiddlewareTest.php @@ -6,9 +6,9 @@ use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Spatie\Permission\Exceptions\UnauthorizedException; -use Spatie\Permission\Middlewares\PermissionMiddleware; -use Spatie\Permission\Middlewares\RoleMiddleware; -use Spatie\Permission\Middlewares\RoleOrPermissionMiddleware; +use Spatie\Permission\Middleware\PermissionMiddleware; +use Spatie\Permission\Middleware\RoleMiddleware; +use Spatie\Permission\Middleware\RoleOrPermissionMiddleware; use Spatie\Permission\Models\Permission; class WildcardMiddlewareTest extends TestCase From 2cc536247cad8a2bebbad114a389d9af02612f97 Mon Sep 17 00:00:00 2001 From: Siros Fakhri <56381478+sirosfakhri@users.noreply.github.com> Date: Tue, 19 Sep 2023 23:52:32 +0330 Subject: [PATCH 0760/1013] fix Lumen Documentation Address (#2501) --- docs/installation-lumen.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation-lumen.md b/docs/installation-lumen.md index 197ff31fe..ff3e4bce2 100644 --- a/docs/installation-lumen.md +++ b/docs/installation-lumen.md @@ -5,7 +5,7 @@ weight: 5 NOTE: Lumen is **not** officially supported by this package. However, the following are some steps which may help get you started. -Lumen installation instructions can be found in the [Lumen documentation](https://lumen.laravel.com/docs/main). +Lumen installation instructions can be found in the [Lumen documentation](https://lumen.laravel.com/docs/master). ## Installing From 08d8e8c66faeab204b92add6f003c762681ee5de Mon Sep 17 00:00:00 2001 From: erikn69 Date: Thu, 5 Oct 2023 16:09:28 -0500 Subject: [PATCH 0761/1013] Test against php 8.3 (#2512) --- .github/workflows/run-tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index d119985db..84e1a0f95 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - php: [8.2, 8.1, 8.0] + php: [8.3, 8.2, 8.1, 8.0] laravel: ["^10.0", "^9.0", "^8.12"] dependency-version: [prefer-lowest, prefer-stable] include: @@ -22,6 +22,8 @@ jobs: exclude: - laravel: "^10.0" php: 8.0 + - laravel: "^8.12" + php: 8.3 name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} From 3a7a17c03d3da28ea3a16949ab348e686e53dfe2 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 8 Oct 2023 15:51:00 -0400 Subject: [PATCH 0762/1013] [Docs] Add more UI options --- docs/advanced-usage/ui-options.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/advanced-usage/ui-options.md b/docs/advanced-usage/ui-options.md index 86151cfed..d9628e766 100644 --- a/docs/advanced-usage/ui-options.md +++ b/docs/advanced-usage/ui-options.md @@ -7,6 +7,10 @@ weight: 11 The package doesn't come with any screens out of the box, you should build that yourself. Here are some options to get you started: +- [Code With Tony - video series](https://www.youtube.com/watch?v=lGfV1ddMhHA) to create an admin panel for managing roles and permissions in Laravel 9. + +- [FilamentPHP plugin](https://filamentphp.com/plugins/tharinda-rodrigo-spatie-roles-permissions) to manage roles and permissions using this package. + - If you'd like to build your own UI, and understand the underlying logic for Gates and Roles and Users, the [Laravel 6 User Login and Management With Roles](https://www.youtube.com/watch?v=7PpJsho5aak&list=PLxFwlLOncxFLazmEPiB4N0iYc3Dwst6m4) video series by Mark Twigg of Penguin Digital gives thorough coverage to the topic, the theory, and implementation of a basic Roles system, independent of this Permissions Package. - [Laravel Nova package by @vyuldashev for managing Roles and Permissions](https://github.com/vyuldashev/nova-permission) From dcad17382210404bbe8c55ae78c5271e734d17b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 14:18:34 -0400 Subject: [PATCH 0763/1013] Bump stefanzweifel/git-auto-commit-action from 4 to 5 (#2514) Bumps [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action) from 4 to 5. - [Release notes](https://github.com/stefanzweifel/git-auto-commit-action/releases) - [Changelog](https://github.com/stefanzweifel/git-auto-commit-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/stefanzweifel/git-auto-commit-action/compare/v4...v5) --- updated-dependencies: - dependency-name: stefanzweifel/git-auto-commit-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/fix-php-code-style-issues.yml | 2 +- .github/workflows/update-changelog.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/fix-php-code-style-issues.yml b/.github/workflows/fix-php-code-style-issues.yml index 3687af011..53c026f89 100644 --- a/.github/workflows/fix-php-code-style-issues.yml +++ b/.github/workflows/fix-php-code-style-issues.yml @@ -19,6 +19,6 @@ jobs: uses: aglipanci/laravel-pint-action@2.3.0 - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v4 + uses: stefanzweifel/git-auto-commit-action@v5 with: commit_message: Fix styling diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index ab69c62c1..0cdea2336 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -21,7 +21,7 @@ jobs: release-notes: ${{ github.event.release.body }} - name: Commit updated CHANGELOG - uses: stefanzweifel/git-auto-commit-action@v4.16.0 + uses: stefanzweifel/git-auto-commit-action@v5 with: branch: main commit_message: Update CHANGELOG From a10d72d40ec41e3e65fe390864667f4c2dec2fb7 Mon Sep 17 00:00:00 2001 From: Julian Gums Date: Thu, 12 Oct 2023 12:51:16 +0300 Subject: [PATCH 0764/1013] remove redundant semicolon (#2516) --- database/migrations/add_teams_fields.php.stub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/add_teams_fields.php.stub b/database/migrations/add_teams_fields.php.stub index b8935dd09..01fcca697 100644 --- a/database/migrations/add_teams_fields.php.stub +++ b/database/migrations/add_teams_fields.php.stub @@ -59,7 +59,7 @@ return new class extends Migration if (! Schema::hasColumn($tableNames['model_has_roles'], $columnNames['team_foreign_key'])) { Schema::table($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole) { - $table->unsignedBigInteger($columnNames['team_foreign_key'])->default('1');; + $table->unsignedBigInteger($columnNames['team_foreign_key'])->default('1'); $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index'); if (DB::getDriverName() !== 'sqlite') { From 6732077cfc85a13afb66ebf5cd81f8222ef5bd23 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 13 Oct 2023 16:59:26 -0400 Subject: [PATCH 0765/1013] mention Policy::before() --- docs/basic-usage/super-admin.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/basic-usage/super-admin.md b/docs/basic-usage/super-admin.md index 70cb0b5a1..5fcfbccc1 100644 --- a/docs/basic-usage/super-admin.md +++ b/docs/basic-usage/super-admin.md @@ -5,7 +5,7 @@ weight: 8 We strongly recommend that a Super-Admin be handled by setting a global `Gate::before` or `Gate::after` rule which checks for the desired role. -Then you can implement the best-practice of primarily using permission-based controls (@can and $user->can, etc) throughout your app, without always having to check for "is this a super-admin" everywhere. Best not to use role-checking (ie: `hasRole`) when you have Super Admin features like this. +Then you can implement the best-practice of primarily using permission-based controls (@can and $user->can, etc) throughout your app, without always having to check for "is this a super-admin" everywhere. **Best not to use role-checking (ie: `hasRole`) (except here in Gate/Policy rules) when you have Super Admin features like this.** ## `Gate::before` @@ -33,6 +33,27 @@ NOTE: `Gate::before` rules need to return `null` rather than `false`, else it wi Jeffrey Way explains the concept of a super-admin (and a model owner, and model policies) in the [Laravel 6 Authorization Filters](https://laracasts.com/series/laravel-6-from-scratch/episodes/51) video and some related lessons in that chapter. +## Policy `before()` + +If you aren't using `Gate::before()` as described above, you could alternatively grant super-admin control by checking the role in individual Policy classes, using the `before()` method. + +Here is an example from the [Laravel Documentation on Policy Filters]([url](https://laravel.com/docs/master/authorization#policy-filters)) + +```php +use App\Models\User; // could be any model + +/** + * Perform pre-authorization checks on the model. + */ +public function before(User $user, string $ability): bool|null +{ + if ($user->hasRole('Super Admin') { + return true; + } + + return null; // see the note above in Gate::before about why null must be returned here. +} +``` ## `Gate::after` From 8e2d6632c1541fd4aabf9dad88ae5aa6783a5a40 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 13 Oct 2023 17:06:09 -0400 Subject: [PATCH 0766/1013] Links back to Laravel Docs --- docs/basic-usage/super-admin.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/basic-usage/super-admin.md b/docs/basic-usage/super-admin.md index 5fcfbccc1..fac0374aa 100644 --- a/docs/basic-usage/super-admin.md +++ b/docs/basic-usage/super-admin.md @@ -9,7 +9,7 @@ Then you can implement the best-practice of primarily using permission-based con ## `Gate::before` -If you want a "Super Admin" role to respond `true` to all permissions, without needing to assign all those permissions to a role, you can use Laravel's `Gate::before()` method. For example: +If you want a "Super Admin" role to respond `true` to all permissions, without needing to assign all those permissions to a role, you can use [Laravel's `Gate::before()` method](https://laravel.com/docs/master/authorization#intercepting-gate-checks). For example: ```php use Illuminate\Support\Facades\Gate; @@ -37,7 +37,7 @@ Jeffrey Way explains the concept of a super-admin (and a model owner, and model If you aren't using `Gate::before()` as described above, you could alternatively grant super-admin control by checking the role in individual Policy classes, using the `before()` method. -Here is an example from the [Laravel Documentation on Policy Filters]([url](https://laravel.com/docs/master/authorization#policy-filters)) +Here is an example from the [Laravel Documentation on Policy Filters](https://laravel.com/docs/master/authorization#policy-filters) ```php use App\Models\User; // could be any model @@ -59,7 +59,7 @@ public function before(User $user, string $ability): bool|null Alternatively you might want to move the Super Admin check to the `Gate::after` phase instead, particularly if your Super Admin shouldn't be allowed to do things your app doesn't want "anyone" to do, such as writing more than 1 review, or bypassing unsubscribe rules, etc. -The following code snippet is inspired from [Freek's blog article](https://freek.dev/1325-when-to-use-gateafter-in-laravel) where this topic is discussed further. +The following code snippet is inspired from [Freek's blog article](https://freek.dev/1325-when-to-use-gateafter-in-laravel) where this topic is discussed further. You can also consult the [Laravel Docs on gate interceptions](https://laravel.com/docs/master/authorization#intercepting-gate-checks) ```php // somewhere in a service provider From c4be1d931281cedfb2337fc63e921498687d4822 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 13 Oct 2023 17:16:25 -0400 Subject: [PATCH 0767/1013] [Docs] Lumen not officially supported --- docs/installation-lumen.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/installation-lumen.md b/docs/installation-lumen.md index ff3e4bce2..7709777e6 100644 --- a/docs/installation-lumen.md +++ b/docs/installation-lumen.md @@ -3,7 +3,9 @@ title: Installation in Lumen weight: 5 --- -NOTE: Lumen is **not** officially supported by this package. However, the following are some steps which may help get you started. +NOTE: Lumen is **not** officially supported by this package. And Lumen is no longer under active development. + +However, the following are some steps which may help get you started. Lumen installation instructions can be found in the [Lumen documentation](https://lumen.laravel.com/docs/master). From 8a5dd87f5157416d5605628fcb31a09d14eed1f1 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 16 Oct 2023 16:44:37 -0400 Subject: [PATCH 0768/1013] Add note about Middleware Priority in docs Fixes #2507 --- docs/basic-usage/middleware.md | 3 +++ docs/basic-usage/teams-permissions.md | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index 592fe8343..fee16f8ed 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -40,6 +40,9 @@ protected $middlewareAliases = [ ]; ``` +**YOU SHOULD ALSO** set [the `$middlewarePriority` array](https://laravel.com/docs/master/middleware#sorting-middleware) to include this package's middleware before the `SubstituteBindings` middleware, else you may get *404 Not Found* responses when a *403 Not Authorized* response might be expected. + + ## Middleware via Routes Then you can protect your routes using middleware rules: diff --git a/docs/basic-usage/teams-permissions.md b/docs/basic-usage/teams-permissions.md index d60657377..d6381daa6 100644 --- a/docs/basic-usage/teams-permissions.md +++ b/docs/basic-usage/teams-permissions.md @@ -35,7 +35,8 @@ Example Team Middleware: ```php namespace App\Http\Middleware; -class TeamsPermission{ +class TeamsPermission +{ public function handle($request, \Closure $next){ if(!empty(auth()->user())){ @@ -52,8 +53,9 @@ class TeamsPermission{ } } ``` -NOTE: You must add your custom `Middleware` to `$middlewarePriority` in `app/Http/Kernel.php`. - + +**YOU MUST ALSO** set [the `$middlewarePriority` array](https://laravel.com/docs/master/middleware#sorting-middleware) in `app/Http/Kernel.php` to include your custom middleware before the `SubstituteBindings` middleware, else you may get *404 Not Found* responses when a *403 Not Authorized* response might be expected. + ## Roles Creating When creating a role you can pass the `team_id` as an optional parameter From ddd396898a7abca829741230afd66bc5db6d4ee8 Mon Sep 17 00:00:00 2001 From: SuperDJ <6484766+SuperDJ@users.noreply.github.com> Date: Wed, 18 Oct 2023 22:14:26 +0200 Subject: [PATCH 0769/1013] Update passport.md (#2521) Add missing information --- docs/basic-usage/passport.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/basic-usage/passport.md b/docs/basic-usage/passport.md index 4faa50fc3..1926f1273 100644 --- a/docs/basic-usage/passport.md +++ b/docs/basic-usage/passport.md @@ -38,6 +38,11 @@ You need to extend the Client model to make it possible to add the required trai The extended Client should either provide a `$guard_name` property or a `guardName()` method. They should return a string that matches the [configured](https://laravel.com/docs/master/passport#installation) guard name for the passport driver. +To tell Passport to use this extended Client, add the rule below to the `boot` method of your `App\Providers\AuthServiceProvider` class. +```php +Passport::useClientModel(\App\Models\Client::class); // Use the namespace of your extended Client. +``` + ## Middleware All middleware provided by this package work with the Client. From 7545d85a61cf0cecbf20a471caea262bbdfbc62b Mon Sep 17 00:00:00 2001 From: Vitali K Date: Mon, 23 Oct 2023 21:21:47 +0400 Subject: [PATCH 0770/1013] [Docs] Add missing closing bracket (#2524) --- docs/basic-usage/super-admin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/super-admin.md b/docs/basic-usage/super-admin.md index fac0374aa..35392772a 100644 --- a/docs/basic-usage/super-admin.md +++ b/docs/basic-usage/super-admin.md @@ -47,7 +47,7 @@ use App\Models\User; // could be any model */ public function before(User $user, string $ability): bool|null { - if ($user->hasRole('Super Admin') { + if ($user->hasRole('Super Admin')) { return true; } From 4e6e86d1a6a09f6b9ec6e92dd9cfcd6a614347b3 Mon Sep 17 00:00:00 2001 From: Axel Cervantes Date: Mon, 23 Oct 2023 23:16:41 -0600 Subject: [PATCH 0771/1013] Has permission directive - Non Default guard (#2515) * Has permission directive to use instead can directive when using non-default guard --- src/PermissionServiceProvider.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index f2643cef0..b68f31984 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -123,6 +123,10 @@ protected function registerBladeExtensions($bladeCompiler): void $bladeCompiler->directive('elserole', fn ($args) => ""); $bladeCompiler->directive('endrole', fn () => ''); + $bladeCompiler->directive('haspermission', fn ($args) => ""); + $bladeCompiler->directive('elsehaspermission', fn ($args) => ""); + $bladeCompiler->directive('endhaspermission', fn () => ''); + $bladeCompiler->directive('hasrole', fn ($args) => ""); $bladeCompiler->directive('endhasrole', fn () => ''); From 914b9a4db99b93c65dcf95070f87977dab8f662f Mon Sep 17 00:00:00 2001 From: drbyte Date: Tue, 24 Oct 2023 05:17:06 +0000 Subject: [PATCH 0772/1013] Fix styling --- src/Exceptions/PermissionDoesNotExist.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Exceptions/PermissionDoesNotExist.php b/src/Exceptions/PermissionDoesNotExist.php index b451ace6b..c04f5eaf0 100644 --- a/src/Exceptions/PermissionDoesNotExist.php +++ b/src/Exceptions/PermissionDoesNotExist.php @@ -13,7 +13,6 @@ public static function create(string $permissionName, ?string $guardName) /** * @param int|string $permissionId - * @param string $guardName * @return static */ public static function withId($permissionId, ?string $guardName) From f8af645d2d98db9a8ed65105256981b128a386b1 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 24 Oct 2023 01:17:29 -0400 Subject: [PATCH 0773/1013] Add Policy Test (#2525) * Add Policy Test * Add Gate::after test --- tests/GateTest.php | 13 ++++++++++ tests/PolicyTest.php | 49 ++++++++++++++++++++++++++++++++++++ tests/TestCase.php | 7 ++++++ tests/TestModels/Content.php | 12 +++++++++ 4 files changed, 81 insertions(+) create mode 100644 tests/PolicyTest.php create mode 100644 tests/TestModels/Content.php diff --git a/tests/GateTest.php b/tests/GateTest.php index 93359834a..436900b84 100644 --- a/tests/GateTest.php +++ b/tests/GateTest.php @@ -19,6 +19,19 @@ public function it_allows_other_gate_before_callbacks_to_run_if_a_user_does_not_ $this->assertFalse($this->testUser->can('edit-articles')); app(Gate::class)->before(function () { + // this Gate-before intercept overrides everything to true ... like a typical Super-Admin might use + return true; + }); + + $this->assertTrue($this->testUser->can('edit-articles')); + } + + /** @test */ + public function it_allows_gate_after_callback_to_grant_denied_privileges() + { + $this->assertFalse($this->testUser->can('edit-articles')); + + app(Gate::class)->after(function ($user, $ability, $result) { return true; }); diff --git a/tests/PolicyTest.php b/tests/PolicyTest.php new file mode 100644 index 000000000..7708225b0 --- /dev/null +++ b/tests/PolicyTest.php @@ -0,0 +1,49 @@ + 'special admin content']); + $record2 = Content::create(['content' => 'viewable', 'user_id' => $this->testUser->id]); + + app(Gate::class)->policy(Content::class, ContentPolicy::class); + + $this->assertFalse($this->testUser->can('view', $record1)); // policy rule for 'view' + $this->assertFalse($this->testUser->can('update', $record1)); // policy rule for 'update' + + $this->assertTrue($this->testUser->can('update', $record2)); // policy rule for 'update' when matching user_id + + // test that the Admin cannot yet view 'special admin content', because doesn't have Role yet + $this->assertFalse($this->testAdmin->can('update', $record1)); + + $this->testAdmin->assignRole($this->testAdminRole); + // test that the Admin can view 'special admin content' + $this->assertTrue($this->testAdmin->can('update', $record1)); // policy override via 'before' + $this->assertTrue($this->testAdmin->can('update', $record2)); // policy override via 'before' + } +} +class ContentPolicy +{ + public function before(Authorizable $user, string $ability): ?bool + { + return $user->hasRole('testAdminRole', 'admin') ?: null; + } + + public function view($user, $content) + { + return $user->id === $content->user_id; + } + + public function update($user, $modelRecord): bool + { + return $user->id === $modelRecord->user_id || $user->can('edit-articles'); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 302169f65..48de05ed5 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -157,6 +157,13 @@ protected function setUpDatabase($app) $table->string('email'); }); + $schema->create('content', function (Blueprint $table) { + $table->increments('id'); + $table->string('content'); + $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); + $table->timestamps(); + }); + if (Cache::getStore() instanceof \Illuminate\Cache\DatabaseStore || $app[PermissionRegistrar::class]->getCacheStore() instanceof \Illuminate\Cache\DatabaseStore) { $this->createCacheTable(); diff --git a/tests/TestModels/Content.php b/tests/TestModels/Content.php new file mode 100644 index 000000000..6b959ea06 --- /dev/null +++ b/tests/TestModels/Content.php @@ -0,0 +1,12 @@ + Date: Tue, 24 Oct 2023 01:21:20 -0400 Subject: [PATCH 0774/1013] Add tests for @haspermission() directives Ref #2515 --- tests/BladeTest.php | 32 ++++++++++++++++++- tests/resources/views/haspermission.blade.php | 7 ++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 tests/resources/views/haspermission.blade.php diff --git a/tests/BladeTest.php b/tests/BladeTest.php index 02a899083..18d17a113 100644 --- a/tests/BladeTest.php +++ b/tests/BladeTest.php @@ -27,8 +27,10 @@ public function all_blade_directives_will_evaluate_false_when_there_is_nobody_lo $role = 'writer'; $roles = [$role]; $elserole = 'na'; + $elsepermission = 'na'; $this->assertEquals('does not have permission', $this->renderView('can', ['permission' => $permission])); + $this->assertEquals('does not have permission', $this->renderView('haspermission', compact('permission', 'elsepermission'))); $this->assertEquals('does not have role', $this->renderView('role', compact('role', 'elserole'))); $this->assertEquals('does not have role', $this->renderView('hasRole', compact('role', 'elserole'))); $this->assertEquals('does not have all of the given roles', $this->renderView('hasAllRoles', compact('roles'))); @@ -44,10 +46,12 @@ public function all_blade_directives_will_evaluate_false_when_somebody_without_r $role = 'writer'; $roles = 'writer'; $elserole = 'na'; + $elsepermission = 'na'; auth()->setUser($this->testUser); - $this->assertEquals('does not have permission', $this->renderView('can', ['permission' => $permission])); + $this->assertEquals('does not have permission', $this->renderView('can', compact('permission'))); + $this->assertEquals('does not have permission', $this->renderView('haspermission', compact('permission', 'elsepermission'))); $this->assertEquals('does not have role', $this->renderView('role', compact('role', 'elserole'))); $this->assertEquals('does not have role', $this->renderView('hasRole', compact('role', 'elserole'))); $this->assertEquals('does not have all of the given roles', $this->renderView('hasAllRoles', compact('roles'))); @@ -61,10 +65,12 @@ public function all_blade_directives_will_evaluate_false_when_somebody_with_anot $role = 'writer'; $roles = 'writer'; $elserole = 'na'; + $elsepermission = 'na'; auth('admin')->setUser($this->testAdmin); $this->assertEquals('does not have permission', $this->renderView('can', compact('permission'))); + $this->assertEquals('does not have permission', $this->renderView('haspermission', compact('permission', 'elsepermission'))); $this->assertEquals('does not have role', $this->renderView('role', compact('role', 'elserole'))); $this->assertEquals('does not have role', $this->renderView('hasRole', compact('role', 'elserole'))); $this->assertEquals('does not have all of the given roles', $this->renderView('hasAllRoles', compact('roles'))); @@ -84,6 +90,30 @@ public function the_can_directive_will_evaluate_true_when_the_logged_in_user_has $this->assertEquals('has permission', $this->renderView('can', ['permission' => 'edit-articles'])); } + /** @test */ + public function the_haspermission_directive_will_evaluate_true_when_the_logged_in_user_has_the_permission() + { + $user = $this->getWriter(); + + $permission = 'edit-articles'; + $user->givePermissionTo('edit-articles'); + + auth()->setUser($user); + + $this->assertEquals('has permission', $this->renderView('haspermission', compact('permission'))); + + $guard = 'admin'; + $elsepermission = 'na'; + $this->assertEquals('does not have permission', $this->renderView('haspermission', compact('permission', 'elsepermission' ,'guard'))); + + $this->testAdminRole->givePermissionTo($this->testAdminPermission); + $this->testAdmin->assignRole($this->testAdminRole); + auth('admin')->setUser($this->testAdmin); + $guard = 'admin'; + $permission = 'admin-permission'; + $this->assertEquals('has permission', $this->renderView('haspermission', compact('permission', 'guard', 'elsepermission'))); + } + /** @test */ public function the_role_directive_will_evaluate_true_when_the_logged_in_user_has_the_role() { diff --git a/tests/resources/views/haspermission.blade.php b/tests/resources/views/haspermission.blade.php new file mode 100644 index 000000000..7dd4e7903 --- /dev/null +++ b/tests/resources/views/haspermission.blade.php @@ -0,0 +1,7 @@ +@haspermission($permission, $guard ?? null) +has permission +@elsehaspermission($elsepermission, $guard ?? null) +has else permission +@else +does not have permission +@endhaspermission From 669d828de3de05f866c234d4e315215efb525b01 Mon Sep 17 00:00:00 2001 From: drbyte Date: Tue, 24 Oct 2023 05:21:44 +0000 Subject: [PATCH 0775/1013] Fix styling --- tests/BladeTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/BladeTest.php b/tests/BladeTest.php index 18d17a113..c65973b43 100644 --- a/tests/BladeTest.php +++ b/tests/BladeTest.php @@ -104,7 +104,7 @@ public function the_haspermission_directive_will_evaluate_true_when_the_logged_i $guard = 'admin'; $elsepermission = 'na'; - $this->assertEquals('does not have permission', $this->renderView('haspermission', compact('permission', 'elsepermission' ,'guard'))); + $this->assertEquals('does not have permission', $this->renderView('haspermission', compact('permission', 'elsepermission', 'guard'))); $this->testAdminRole->givePermissionTo($this->testAdminPermission); $this->testAdmin->assignRole($this->testAdminRole); From 239abc50a20b7b5b8dc80793ecc466433542852a Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 25 Oct 2023 00:55:50 -0400 Subject: [PATCH 0776/1013] Add guard parameter to can() (#2526) * Add guard parameter to can() As discussed in comments on 2515: https://github.com/spatie/laravel-permission/pull/2515#issuecomment-1762134150 Co-authored-by: parallels999 --- src/PermissionRegistrar.php | 7 +++-- tests/BladeTest.php | 30 +++++++++++++++++++++ tests/MultipleGuardsTest.php | 42 +++++++++++++++++++++++++++++ tests/resources/views/can.blade.php | 2 +- 4 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 30c6d5676..8a6e28cad 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -122,9 +122,12 @@ public function getPermissionsTeamId() */ public function registerPermissions(Gate $gate): bool { - $gate->before(function (Authorizable $user, string $ability) { + $gate->before(function (Authorizable $user, string $ability, array &$args = []) { + if (is_string($args[0] ?? null) && ! class_exists($args[0])) { + $guard = array_shift($args); + } if (method_exists($user, 'checkPermissionTo')) { - return $user->checkPermissionTo($ability) ?: null; + return $user->checkPermissionTo($ability, $guard ?? null) ?: null; } }); diff --git a/tests/BladeTest.php b/tests/BladeTest.php index c65973b43..7e19ec81f 100644 --- a/tests/BladeTest.php +++ b/tests/BladeTest.php @@ -78,6 +78,36 @@ public function all_blade_directives_will_evaluate_false_when_somebody_with_anot $this->assertEquals('does not have any of the given roles', $this->renderView('hasAnyRole', compact('roles'))); } + /** @test */ + public function the_can_directive_can_accept_a_guard_name() + { + $user = $this->getWriter(); + $user->givePermissionTo('edit-articles'); + auth()->setUser($user); + + $permission = 'edit-articles'; + $guard = 'web'; + $this->assertEquals('has permission', $this->renderView('can', compact('permission', 'guard'))); + $guard = 'admin'; + $this->assertEquals('does not have permission', $this->renderView('can', compact('permission', 'guard'))); + + auth()->logout(); + + // log in as the Admin with the permission-via-role + $this->testAdmin->givePermissionTo($this->testAdminPermission); + $user = $this->testAdmin; + auth()->setUser($user); + + $permission = 'edit-articles'; + $guard = 'web'; + $this->assertEquals('does not have permission', $this->renderView('can', compact('permission', 'guard'))); + + $permission = 'admin-permission'; + $guard = 'admin'; + $this->assertTrue($this->testAdmin->checkPermissionTo($permission, $guard)); + $this->assertEquals('has permission', $this->renderView('can', compact('permission', 'guard'))); + } + /** @test */ public function the_can_directive_will_evaluate_true_when_the_logged_in_user_has_the_permission() { diff --git a/tests/MultipleGuardsTest.php b/tests/MultipleGuardsTest.php index c93069f58..64c229a56 100644 --- a/tests/MultipleGuardsTest.php +++ b/tests/MultipleGuardsTest.php @@ -18,6 +18,7 @@ protected function getEnvironmentSetUp($app) 'api' => ['driver' => 'token', 'provider' => 'users'], 'jwt' => ['driver' => 'token', 'provider' => 'users'], 'abc' => ['driver' => 'abc'], + 'admin' => ['driver' => 'session', 'provider' => 'admins'], ]); $this->setUpRoutes(); @@ -53,6 +54,47 @@ public function it_can_give_a_permission_to_a_model_that_is_used_by_multiple_gua $this->assertFalse($this->testUser->checkPermissionTo('do_that', 'web')); } + /** @test */ + public function the_gate_can_grant_permission_to_a_user_by_passing_a_guard_name() + { + $this->testUser->givePermissionTo(app(Permission::class)::create([ + 'name' => 'do_this', + 'guard_name' => 'web', + ])); + + $this->testUser->givePermissionTo(app(Permission::class)::create([ + 'name' => 'do_that', + 'guard_name' => 'api', + ])); + + $this->assertTrue($this->testUser->can('do_this', 'web')); + $this->assertTrue($this->testUser->can('do_that', 'api')); + $this->assertFalse($this->testUser->can('do_that', 'web')); + + $this->assertTrue($this->testUser->cannot('do_that', 'web')); + $this->assertTrue($this->testUser->canAny(['do_this', 'do_that'], 'web')); + + $this->testAdminRole->givePermissionTo($this->testAdminPermission); + $this->testAdmin->assignRole($this->testAdminRole); + + $this->assertTrue($this->testAdmin->hasPermissionTo($this->testAdminPermission)); + $this->assertTrue($this->testAdmin->can('admin-permission')); + $this->assertTrue($this->testAdmin->can('admin-permission', 'admin')); + $this->assertTrue($this->testAdmin->cannot('admin-permission', 'web')); + + $this->assertTrue($this->testAdmin->cannot('non-existing-permission')); + $this->assertTrue($this->testAdmin->cannot('non-existing-permission', 'web')); + $this->assertTrue($this->testAdmin->cannot('non-existing-permission', 'admin')); + $this->assertTrue($this->testAdmin->cannot(['admin-permission', 'non-existing-permission'], 'web')); + + $this->assertFalse($this->testAdmin->can('edit-articles', 'web')); + $this->assertFalse($this->testAdmin->can('edit-articles', 'admin')); + + $this->assertTrue($this->testUser->cannot('edit-articles', 'admin')); + $this->assertTrue($this->testUser->cannot('admin-permission', 'admin')); + $this->assertTrue($this->testUser->cannot('admin-permission', 'web')); + } + /** @test */ public function it_can_honour_guardName_function_on_model_for_overriding_guard_name_property() { diff --git a/tests/resources/views/can.blade.php b/tests/resources/views/can.blade.php index 81d526906..f0c6c8214 100644 --- a/tests/resources/views/can.blade.php +++ b/tests/resources/views/can.blade.php @@ -1,4 +1,4 @@ -@can($permission) +@can($permission, $guard ?? null) has permission @else does not have permission From cdc329c367bde4d35716c8d04f95e8adafc6afdb Mon Sep 17 00:00:00 2001 From: Hamid Dehnavi Date: Wed, 25 Oct 2023 08:27:56 +0330 Subject: [PATCH 0777/1013] Update uuid doc (#2527) --- docs/advanced-usage/uuid.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index 2209159d9..79c1331bb 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -64,6 +64,18 @@ If you also want the roles and permissions to use a UUID for their `id` value, t - $table->unsignedBigInteger(PermissionRegistrar::$pivotRole); + $table->uuid(PermissionRegistrar::$pivotPermission); + $table->uuid(PermissionRegistrar::$pivotRole); + + $table->foreign(PermissionRegistrar::$pivotPermission) +- ->references('id') // permission id ++ ->references('uuid') // permission id + ->on($tableNames['permissions']) + ->onDelete('cascade'); + + $table->foreign(PermissionRegistrar::$pivotRole) +- ->references('id') // role id ++ ->references('uuid') // role id + ->on($tableNames['roles']) + ->onDelete('cascade'); ``` From 9d1def88fd2b8b4a44937d995997c9ab2aff0a23 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 25 Oct 2023 01:06:49 -0400 Subject: [PATCH 0778/1013] [Docs] Mention @haspermission() directive --- docs/basic-usage/blade-directives.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/basic-usage/blade-directives.md b/docs/basic-usage/blade-directives.md index 51924866b..3d95f8ec9 100644 --- a/docs/basic-usage/blade-directives.md +++ b/docs/basic-usage/blade-directives.md @@ -4,8 +4,7 @@ weight: 7 --- ## Permissions -This package doesn't add any **permission**-specific Blade directives. -Instead, use Laravel's native `@can` directive to check if a user has a certain permission. +This package lets you use Laravel's native `@can` directive to check if a user has a certain permission (whether you gave them that permission directly or if you granted it indirectly via a role): ```php @can('edit articles') @@ -21,13 +20,17 @@ or You can use `@can`, `@cannot`, `@canany`, and `@guest` to test for permission-related access. +When using a permission-name associated with permissions created in this package, you can use `@can('permission-name', 'guard_name')` if you need to check against a specific guard. + +You can also use `@haspermission('permission-name')` or `@haspermission('permission-name', 'guard_name')` in similar fashion. With corresponding `@endhaspermission`. + ## Roles As discussed in the Best Practices section of the docs, **it is strongly recommended to always use permission directives**, instead of role directives. Additionally, if your reason for testing against Roles is for a Super-Admin, see the *Defining A Super-Admin* section of the docs. -If you actually need to test for Roles, this package offers some Blade directives to verify whether the currently logged in user has all or any of a given list of roles. +If you actually need to directly test for Roles, this package offers some Blade directives to verify whether the currently logged in user has all or any of a given list of roles. Optionally you can pass in the `guard` that the check will be performed on as a second argument. @@ -48,6 +51,12 @@ is the same as I am not a writer... @endhasrole ``` +which is also the same as +```php +@if(auth()->user()->hasRole('writer')) + // +@endif +``` Check for any role in a list: ```php From e0d770c4a8c474dd7051e7e8b55fcdb349cb74b0 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 25 Oct 2023 01:22:02 -0400 Subject: [PATCH 0779/1013] [Docs] Update upgrade docs --- docs/upgrading.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index ab83fd790..5d3f00f2a 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -8,7 +8,7 @@ weight: 6 ALL upgrades of this package should follow these steps: 1. Composer. Upgrading between major versions of this package always require the usual Composer steps: - - Update your `composer.json` to specify the new major version, such as `^6.0` + - Update your `composer.json` to specify the new major version, for example: `^6.0` - Then run `composer update`. 2. Migrations. Compare the `migration` file stubs in the NEW version of this package against the migrations you've already run inside your app. If necessary, create a new migration (by hand) to apply any new database changes. @@ -30,6 +30,8 @@ and/or consult the [Release Notes](https://github.com/spatie/laravel-permission/ ## Upgrading from v5 to v6 There are a few breaking-changes when upgrading to v6, but most of them won't affect you unless you have been customizing things. +For guidance with upgrading your extended models, your migrations, your routes, etc, see the **Upgrade Essentials** section at the top of this file. + 1. If you have overridden the `getPermissionClass()` or `getRoleClass()` methods or have custom Models, you will need to revisit those customizations. See PR #2368 for details. eg: if you have a custom model you will need to make changes, including accessing the model using `$this->permissionClass::` syntax (eg: using `::` instead of `->`) in all the overridden methods that make use of the models. @@ -42,15 +44,15 @@ eg: if you have a custom model you will need to make changes, including accessin 4. MIDDLEWARE: - 1. The `\Spatie\Permission\Middlewares\` namespace has been renamed to `\Spatie\Permission\Middleware\` (singular). Update your references to them in your `/app/Http/Kernel.php` and any routes that have the fully qualified path. + 1. The `\Spatie\Permission\Middlewares\` namespace has been renamed to `\Spatie\Permission\Middleware\` (singular). Update any references to them in your `/app/Http/Kernel.php` and any routes (or imported classes in your routes files) that have the fully qualified namespace. 2. NOTE: For consistency with `PermissionMiddleware`, the `RoleOrPermissionMiddleware` has switched from only checking permissions provided by this package to using `canAny()` to check against any abilities registered by your application. This may have the effect of granting those other abilities (such as Super Admin) when using the `RoleOrPermissionMiddleware`, which previously would have failed silently. - 3. In the unlikely event that you have customized the Wildcard Permissions feature by extending the `WildcardPermission` model, please note that the public interface has changed and you will need to update your extended model with the new method signatures. + 3. In the unlikely event that you have customized the Wildcard Permissions feature by extending the `WildcardPermission` model, please note that the public interface has changed significantly and you will need to update your extended model with the new method signatures. 5. Test suites. If you have tests which manually clear the permission cache and re-register permissions, you no longer need to call `\Spatie\Permission\PermissionRegistrar::class)->registerPermissions();`. In fact, **calls to `->registerPermissions()` MUST be deleted from your tests**. - (Calling `app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();` after creating roles and permissions is still okay and encouraged.) + (Calling `app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();` after creating roles and permissions in migrations and factories and seeders is still okay and encouraged.) ## Upgrading from v4 to v5 From 095821962f8f287f0ce1d6265fc7aa7070dacf12 Mon Sep 17 00:00:00 2001 From: drbyte Date: Wed, 25 Oct 2023 05:25:50 +0000 Subject: [PATCH 0780/1013] Update CHANGELOG --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1e5f9f0b..e64ce47bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to `laravel-permission` will be documented in this file +## 5.11.1 - 2023-10-25 + +No functional changes. Just several small updates to the Documentation. + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.11.0...5.11.1 + ## 5.11.0 - 2023-08-30 ### What's Changed @@ -627,6 +633,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -684,6 +691,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From ddfe3870d850c1059fc8452ecfb9003f2232142e Mon Sep 17 00:00:00 2001 From: drbyte Date: Wed, 25 Oct 2023 05:38:11 +0000 Subject: [PATCH 0781/1013] Update CHANGELOG --- CHANGELOG.md | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e64ce47bc..cc5ade7aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,69 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.0.0 - 2023-10-25 + +### What's Changed + +- Full uuid/guid/ulid support by @erikn69 in https://github.com/spatie/laravel-permission/pull/2089 +- Refactor: Change static properties to non-static by @olivernybroe in https://github.com/spatie/laravel-permission/pull/2324 +- Fix Role::withCount if belongsToMany declared by @xenaio-daniil in https://github.com/spatie/laravel-permission/pull/2280 +- Fix: Lazily bind dependencies by @olivernybroe in https://github.com/spatie/laravel-permission/pull/2321 +- Avoid loss of all permissions/roles pivots on sync error by @erikn69 in https://github.com/spatie/laravel-permission/pull/2341 +- Fix delete permissions on Permissions Model by @erikn69 in https://github.com/spatie/laravel-permission/pull/2366 +- Detach users on role/permission physical deletion by @erikn69 in https://github.com/spatie/laravel-permission/pull/2370 +- Rename clearClassPermissions method to clearPermissionsCollection by @erikn69 in https://github.com/spatie/laravel-permission/pull/2369 +- Use anonymous migrations (for L8+) by @erikn69 in https://github.com/spatie/laravel-permission/pull/2374 +- [BC] Return string on getPermissionClass(), getRoleClass() by @erikn69 in https://github.com/spatie/laravel-permission/pull/2368 +- Only offer publishing when running in console by @erikn69 in https://github.com/spatie/laravel-permission/pull/2377 +- Don't add commands in web interface context by @angeljqv in https://github.com/spatie/laravel-permission/pull/2405 +- [BC] Fix Role->hasPermissionTo() signature to match HasPermissions trait by @erikn69 in https://github.com/spatie/laravel-permission/pull/2380 +- Force that getPermissionsViaRoles, hasPermissionViaRole must be used only by authenticable by @erikn69 in https://github.com/spatie/laravel-permission/pull/2382 +- fix BadMethodCallException: undefined methods hasAnyRole, hasAnyPermissions by @erikn69 in https://github.com/spatie/laravel-permission/pull/2381 +- Add PHPStan workflow with fixes by @erikn69 in https://github.com/spatie/laravel-permission/pull/2376 +- Add BackedEnum support by @drbyte in https://github.com/spatie/laravel-permission/pull/2391 +- Drop PHP 7.3 support by @angeljqv in https://github.com/spatie/laravel-permission/pull/2388 +- Drop PHP 7.4 support by @drbyte in https://github.com/spatie/laravel-permission/pull/2485 +- Test against PHP 8.3 by @erikn69 in https://github.com/spatie/laravel-permission/pull/2512 +- Fix call to an undefined method Role::getRoleClass by @erikn69 in https://github.com/spatie/laravel-permission/pull/2411 +- Remove force loading model relationships by @erikn69 in https://github.com/spatie/laravel-permission/pull/2412 +- Test alternate cache drivers by @erikn69 in https://github.com/spatie/laravel-permission/pull/2416 +- Use attach instead of sync on traits by @erikn69 in https://github.com/spatie/laravel-permission/pull/2420 +- Fewer sqls in syncRoles, syncPermissions by @erikn69 in https://github.com/spatie/laravel-permission/pull/2423 +- Add middleware using static method by @jnoordsij in https://github.com/spatie/laravel-permission/pull/2424 +- Update PHPDocs for IDE autocompletion by @erikn69 in https://github.com/spatie/laravel-permission/pull/2437 +- [BC] Wildcard permissions algorithm performance improvements (ALERT: Breaking Changes) by @danharrin in https://github.com/spatie/laravel-permission/pull/2445 +- Add withoutRole and withoutPermission scopes by @drbyte in https://github.com/spatie/laravel-permission/pull/2463 +- Add support for service-to-service Passport client by @SuperDJ in https://github.com/spatie/laravel-permission/pull/2467 +- Register OctaneReloadPermissions listener for Laravel Octane by @erikn69 in https://github.com/spatie/laravel-permission/pull/2403 +- Add guard name to exceptions by @drbyte in https://github.com/spatie/laravel-permission/pull/2481 +- Update contracts to allow for UUID by @drbyte in https://github.com/spatie/laravel-permission/pull/2480 +- Avoid triggering eloquent.retrieved event by @erikn69 in https://github.com/spatie/laravel-permission/pull/2498 +- [BC] Rename "Middlewares" namespace to "Middleware" by @drbyte in https://github.com/spatie/laravel-permission/pull/2499 +- `@haspermission` directive by @axlwild in https://github.com/spatie/laravel-permission/pull/2515 +- Add guard parameter to can() by @drbyte in https://github.com/spatie/laravel-permission/pull/2526 + +### New Contributors + +- @xenaio-daniil made their first contribution in https://github.com/spatie/laravel-permission/pull/2280 +- @JensvandeWiel made their first contribution in https://github.com/spatie/laravel-permission/pull/2336 +- @fsamapoor made their first contribution in https://github.com/spatie/laravel-permission/pull/2361 +- @yungifez made their first contribution in https://github.com/spatie/laravel-permission/pull/2394 +- @HasanEksi made their first contribution in https://github.com/spatie/laravel-permission/pull/2418 +- @jnoordsij made their first contribution in https://github.com/spatie/laravel-permission/pull/2424 +- @danharrin made their first contribution in https://github.com/spatie/laravel-permission/pull/2445 +- @SuperDJ made their first contribution in https://github.com/spatie/laravel-permission/pull/2467 +- @ChillMouse made their first contribution in https://github.com/spatie/laravel-permission/pull/2438 +- @Okipa made their first contribution in https://github.com/spatie/laravel-permission/pull/2492 +- @edalzell made their first contribution in https://github.com/spatie/laravel-permission/pull/2494 +- @sirosfakhri made their first contribution in https://github.com/spatie/laravel-permission/pull/2501 +- @juliangums made their first contribution in https://github.com/spatie/laravel-permission/pull/2516 +- @nnnnnnnngu made their first contribution in https://github.com/spatie/laravel-permission/pull/2524 +- @axlwild made their first contribution in https://github.com/spatie/laravel-permission/pull/2515 +- @shdehnavi made their first contribution in https://github.com/spatie/laravel-permission/pull/2527 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.11.1...6.0.0 + ## 5.11.1 - 2023-10-25 No functional changes. Just several small updates to the Documentation. @@ -634,6 +697,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -692,6 +756,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From 30051d6486e20f8de95d415132611512c263d826 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Wed, 25 Oct 2023 08:53:43 +0200 Subject: [PATCH 0782/1013] Update _index.md --- docs/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_index.md b/docs/_index.md index 591491f89..cf88be1b4 100644 --- a/docs/_index.md +++ b/docs/_index.md @@ -1,5 +1,5 @@ --- -title: v5 +title: v6 slogan: Associate users with roles and permissions githubUrl: https://github.com/spatie/laravel-permission branch: main From 56f91e842dba272ccc0825603588f7655e3add81 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 27 Oct 2023 17:35:08 -0400 Subject: [PATCH 0783/1013] Update v5-to-v6 migration-file note. --- docs/upgrading.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index 5d3f00f2a..0c7bc2b07 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -39,7 +39,7 @@ eg: if you have a custom model you will need to make changes, including accessin 2. Model and Contract/Interface updates. The Role and Permission Models and Contracts/Interfaces have been updated with syntax changes to method signatures. Update any models you have extended, or contracts implemented, accordingly. See PR #2380 and #2480 for some of the specifics. -3. Migrations will need to be upgraded. (They have been updated to anonymous-class syntax that was introduced in Laravel 8, AND some structural coding changes in the registrar class changed the way we extracted configuration settings in the migration files.) If you had not customized it from the original then replacing the contents of the file should be straightforward. Usually the only customization is if you've switched to UUIDs or customized MySQL index name lengths. +3. Migrations WILL need to be upgraded. (They have been updated to anonymous-class syntax that was introduced in Laravel 8, AND some structural coding changes in the registrar class changed the way we extracted configuration settings in the migration files.) There are no changes to the package's structure since v5, so if you had not customized it from the original then replacing the contents of the file should be enough. (Usually the only customization is if you've switched to UUIDs or customized MySQL index name lengths.) **If you get the following error, it means your migration file needs upgrading: `Error: Access to undeclared static property Spatie\Permission\PermissionRegistrar::$pivotPermission`** 4. MIDDLEWARE: From a5142d98ad60d676f47c0fc916f83906a37cf0d9 Mon Sep 17 00:00:00 2001 From: Hamid Dehnavi Date: Tue, 31 Oct 2023 02:24:52 +0330 Subject: [PATCH 0784/1013] Update teams-permissions doc (#2534) Update unsetRelation() example --- docs/basic-usage/teams-permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/teams-permissions.md b/docs/basic-usage/teams-permissions.md index d6381daa6..f4a0d729e 100644 --- a/docs/basic-usage/teams-permissions.md +++ b/docs/basic-usage/teams-permissions.md @@ -96,7 +96,7 @@ setPermissionsTeamId($new_team_id); // $user = Auth::user(); // unset cached model relations so new team relations will get reloaded -$user->unsetRelation('roles','permissions'); +$user->unsetRelation('roles')->unsetRelation('permissions'); // Now you can check: $roles = $user->roles; From 0e34712c309d248e8cee533cd5fcc22e37ed886d Mon Sep 17 00:00:00 2001 From: Sevan Nerse Date: Mon, 6 Nov 2023 22:21:43 +0300 Subject: [PATCH 0785/1013] Update direct-permissions.md (#2539) --- docs/basic-usage/direct-permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/direct-permissions.md b/docs/basic-usage/direct-permissions.md index c508ff9d5..7172f8448 100644 --- a/docs/basic-usage/direct-permissions.md +++ b/docs/basic-usage/direct-permissions.md @@ -7,7 +7,7 @@ weight: 2 It's better to assign permissions to Roles, and then assign Roles to Users. -See https://spatie.be/docs/laravel-permission/best-practices/roles-vs-permissions for a deeper explanation. +See the [Roles vs Permissions](./../best-practices/roles-vs-permissions.md) section of the docs for a deeper explanation. HOWEVER, If you have reason to directly assign individual permissions to specific users (instead of to roles assigned to those users), you can do that as described below: From 9e83ebe5084689a31263581e1fa821270957b8e6 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 6 Nov 2023 14:27:13 -0500 Subject: [PATCH 0786/1013] Set default team_foreign_key in case config file is old Fixes #2535 --- src/PermissionRegistrar.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 8a6e28cad..0443eb026 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -66,7 +66,7 @@ public function initializeCache(): void $this->cacheExpirationTime = config('permission.cache.expiration_time') ?: \DateInterval::createFromDateString('24 hours'); $this->teams = config('permission.teams', false); - $this->teamsKey = config('permission.column_names.team_foreign_key'); + $this->teamsKey = config('permission.column_names.team_foreign_key', 'team_id'); $this->cacheKey = config('permission.cache.key'); From 988aa32b030d4ad5796062e55a1bb899d41515eb Mon Sep 17 00:00:00 2001 From: drbyte Date: Mon, 6 Nov 2023 19:31:22 +0000 Subject: [PATCH 0787/1013] Update CHANGELOG --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc5ade7aa..32b534be2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.0.1 - 2023-11-06 + +### What's Changed + +- Provide a default team_foreign_key value in case config file isn't upgraded yet or teams feature is unused. Fixes #2535 +- [Docs] Update unsetRelation() example in teams-permissions.md by @shdehnavi in https://github.com/spatie/laravel-permission/pull/2534 +- [Docs] Update link in direct-permissions.md by @sevannerse in https://github.com/spatie/laravel-permission/pull/2539 + +### New Contributors + +- @sevannerse made their first contribution in https://github.com/spatie/laravel-permission/pull/2539 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.0.0...6.0.1 + ## 6.0.0 - 2023-10-25 ### What's Changed @@ -698,6 +712,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -757,6 +772,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From 9665eb96025fc1b35a0bd3bfb9d4e82acc871f36 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 6 Nov 2023 14:42:36 -0500 Subject: [PATCH 0788/1013] [Docs] Update direct-permissions.md --- docs/basic-usage/direct-permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/direct-permissions.md b/docs/basic-usage/direct-permissions.md index 7172f8448..5ebf52c92 100644 --- a/docs/basic-usage/direct-permissions.md +++ b/docs/basic-usage/direct-permissions.md @@ -7,7 +7,7 @@ weight: 2 It's better to assign permissions to Roles, and then assign Roles to Users. -See the [Roles vs Permissions](./../best-practices/roles-vs-permissions.md) section of the docs for a deeper explanation. +See the [Roles vs Permissions](../best-practices/roles-vs-permissions.md) section of the docs for a deeper explanation. HOWEVER, If you have reason to directly assign individual permissions to specific users (instead of to roles assigned to those users), you can do that as described below: From 24dc4f6c7eee17097cb971a3f519304faf77da84 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 6 Nov 2023 14:45:13 -0500 Subject: [PATCH 0789/1013] Update direct-permissions.md --- docs/basic-usage/direct-permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/direct-permissions.md b/docs/basic-usage/direct-permissions.md index 5ebf52c92..d8533b9b9 100644 --- a/docs/basic-usage/direct-permissions.md +++ b/docs/basic-usage/direct-permissions.md @@ -7,7 +7,7 @@ weight: 2 It's better to assign permissions to Roles, and then assign Roles to Users. -See the [Roles vs Permissions](../best-practices/roles-vs-permissions.md) section of the docs for a deeper explanation. +See the [Roles vs Permissions](../best-practices/roles-vs-permissions) section of the docs for a deeper explanation. HOWEVER, If you have reason to directly assign individual permissions to specific users (instead of to roles assigned to those users), you can do that as described below: From bd077c853670c290b69870ee089220e2657480ed Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 7 Nov 2023 01:53:08 -0500 Subject: [PATCH 0790/1013] [Docs] add note about ensuring IDs are passed as integers --- docs/upgrading.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index 0c7bc2b07..6c2e4017a 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -32,17 +32,19 @@ There are a few breaking-changes when upgrading to v6, but most of them won't af For guidance with upgrading your extended models, your migrations, your routes, etc, see the **Upgrade Essentials** section at the top of this file. -1. If you have overridden the `getPermissionClass()` or `getRoleClass()` methods or have custom Models, you will need to revisit those customizations. See PR #2368 for details. +1. Due to the improved ULID/UUID/GUID support, any package methods which accept a Permission or Role `id` must pass that `id` as an `integer`. If you pass it as a numeric string, the functions will attempt to lookup the role/permission as a string. In such cases you may see errors such as `There is no permission named '123' for guard 'web'.` (where `'123'` is being treated as a string because it was passed as a string instead of as an integer). This also applies to arrays of id's: if it's an array of strings we will do a lookup on the name instead of on the id. **This will mostly only affect UI pages** because an HTML Request is received as string data. **The solution is simple:** if you're passing integers to a form field, then convert them back to integers when using that field's data for calling functions to grant/assign/sync/remove/revoke permissions and roles. + +2. If you have overridden the `getPermissionClass()` or `getRoleClass()` methods or have custom Models, you will need to revisit those customizations. See PR #2368 for details. eg: if you have a custom model you will need to make changes, including accessing the model using `$this->permissionClass::` syntax (eg: using `::` instead of `->`) in all the overridden methods that make use of the models. Be sure to compare your custom models with originals to see what else may have changed. -2. Model and Contract/Interface updates. The Role and Permission Models and Contracts/Interfaces have been updated with syntax changes to method signatures. Update any models you have extended, or contracts implemented, accordingly. See PR #2380 and #2480 for some of the specifics. +3. Model and Contract/Interface updates. The Role and Permission Models and Contracts/Interfaces have been updated with syntax changes to method signatures. Update any models you have extended, or contracts implemented, accordingly. See PR #2380 and #2480 for some of the specifics. -3. Migrations WILL need to be upgraded. (They have been updated to anonymous-class syntax that was introduced in Laravel 8, AND some structural coding changes in the registrar class changed the way we extracted configuration settings in the migration files.) There are no changes to the package's structure since v5, so if you had not customized it from the original then replacing the contents of the file should be enough. (Usually the only customization is if you've switched to UUIDs or customized MySQL index name lengths.) +4. Migrations WILL need to be upgraded. (They have been updated to anonymous-class syntax that was introduced in Laravel 8, AND some structural coding changes in the registrar class changed the way we extracted configuration settings in the migration files.) There are no changes to the package's structure since v5, so if you had not customized it from the original then replacing the contents of the file should be enough. (Usually the only customization is if you've switched to UUIDs or customized MySQL index name lengths.) **If you get the following error, it means your migration file needs upgrading: `Error: Access to undeclared static property Spatie\Permission\PermissionRegistrar::$pivotPermission`** -4. MIDDLEWARE: +5. MIDDLEWARE: 1. The `\Spatie\Permission\Middlewares\` namespace has been renamed to `\Spatie\Permission\Middleware\` (singular). Update any references to them in your `/app/Http/Kernel.php` and any routes (or imported classes in your routes files) that have the fully qualified namespace. @@ -50,7 +52,7 @@ eg: if you have a custom model you will need to make changes, including accessin 3. In the unlikely event that you have customized the Wildcard Permissions feature by extending the `WildcardPermission` model, please note that the public interface has changed significantly and you will need to update your extended model with the new method signatures. -5. Test suites. If you have tests which manually clear the permission cache and re-register permissions, you no longer need to call `\Spatie\Permission\PermissionRegistrar::class)->registerPermissions();`. In fact, **calls to `->registerPermissions()` MUST be deleted from your tests**. +6. Test suites. If you have tests which manually clear the permission cache and re-register permissions, you no longer need to call `\Spatie\Permission\PermissionRegistrar::class)->registerPermissions();`. In fact, **calls to `->registerPermissions()` MUST be deleted from your tests**. (Calling `app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();` after creating roles and permissions in migrations and factories and seeders is still okay and encouraged.) From 62f22e192711fe56fb55b57b586001f8674b4396 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 7 Nov 2023 13:53:51 -0500 Subject: [PATCH 0791/1013] add example --- docs/upgrading.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index 6c2e4017a..da3d4423d 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -32,7 +32,7 @@ There are a few breaking-changes when upgrading to v6, but most of them won't af For guidance with upgrading your extended models, your migrations, your routes, etc, see the **Upgrade Essentials** section at the top of this file. -1. Due to the improved ULID/UUID/GUID support, any package methods which accept a Permission or Role `id` must pass that `id` as an `integer`. If you pass it as a numeric string, the functions will attempt to lookup the role/permission as a string. In such cases you may see errors such as `There is no permission named '123' for guard 'web'.` (where `'123'` is being treated as a string because it was passed as a string instead of as an integer). This also applies to arrays of id's: if it's an array of strings we will do a lookup on the name instead of on the id. **This will mostly only affect UI pages** because an HTML Request is received as string data. **The solution is simple:** if you're passing integers to a form field, then convert them back to integers when using that field's data for calling functions to grant/assign/sync/remove/revoke permissions and roles. +1. Due to the improved ULID/UUID/GUID support, any package methods which accept a Permission or Role `id` must pass that `id` as an `integer`. If you pass it as a numeric string, the functions will attempt to lookup the role/permission as a string. In such cases you may see errors such as `There is no permission named '123' for guard 'web'.` (where `'123'` is being treated as a string because it was passed as a string instead of as an integer). This also applies to arrays of id's: if it's an array of strings we will do a lookup on the name instead of on the id. **This will mostly only affect UI pages** because an HTML Request is received as string data. **The solution is simple:** if you're passing integers to a form field, then convert them back to integers when using that field's data for calling functions to grant/assign/sync/remove/revoke permissions and roles. One way to convert an array of permissions `id`'s from strings to integers is: `collect($validated['permission'])->map(fn($val)=>(int)$val)` 2. If you have overridden the `getPermissionClass()` or `getRoleClass()` methods or have custom Models, you will need to revisit those customizations. See PR #2368 for details. eg: if you have a custom model you will need to make changes, including accessing the model using `$this->permissionClass::` syntax (eg: using `::` instead of `->`) in all the overridden methods that make use of the models. From 8e584d3ac09856e106f989741a2d82b094278e58 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Thu, 9 Nov 2023 17:03:17 -0500 Subject: [PATCH 0792/1013] Reset teamId on octane (#2547) --- config/permission.php | 5 ++--- src/Listeners/OctaneReloadPermissions.php | 13 ------------- src/PermissionRegistrar.php | 2 +- src/PermissionServiceProvider.php | 19 +++++++++++++------ 4 files changed, 16 insertions(+), 23 deletions(-) delete mode 100644 src/Listeners/OctaneReloadPermissions.php diff --git a/config/permission.php b/config/permission.php index 9c0532d89..2a520f351 100644 --- a/config/permission.php +++ b/config/permission.php @@ -104,9 +104,8 @@ 'register_permission_check_method' => true, /* - * When set to true, the Spatie\Permission\Listeners\OctaneReloadPermissions listener will be registered - * on the Laravel\Octane\Events\OperationTerminated event, this will refresh permissions on every - * TickTerminated, TaskTerminated and RequestTerminated + * When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered + * this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated * NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it. */ 'register_octane_reset_listener' => false, diff --git a/src/Listeners/OctaneReloadPermissions.php b/src/Listeners/OctaneReloadPermissions.php deleted file mode 100644 index 6f4544e5e..000000000 --- a/src/Listeners/OctaneReloadPermissions.php +++ /dev/null @@ -1,13 +0,0 @@ -sandbox->make(PermissionRegistrar::class)->clearPermissionsCollection(); - } -} diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 0443eb026..051e48b0f 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -98,7 +98,7 @@ protected function getCacheStoreFromConfig(): Repository /** * Set the team id for teams/groups support, this id is used when querying permissions/roles * - * @param int|string|\Illuminate\Database\Eloquent\Model $id + * @param int|string|\Illuminate\Database\Eloquent\Model|null $id */ public function setPermissionsTeamId($id): void { diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index b68f31984..a9011cd2b 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -13,7 +13,6 @@ use Illuminate\View\Compilers\BladeCompiler; use Spatie\Permission\Contracts\Permission as PermissionContract; use Spatie\Permission\Contracts\Role as RoleContract; -use Spatie\Permission\Listeners\OctaneReloadPermissions; class PermissionServiceProvider extends ServiceProvider { @@ -91,17 +90,25 @@ protected function registerCommands(): void protected function registerOctaneListener(): void { - if ($this->app->runningInConsole() || ! $this->app['config']->get('permission.register_octane_reset_listener')) { + if ($this->app->runningInConsole() || ! $this->app['config']->get('octane.listeners')) { return; } - if (! $this->app['config']->get('octane.listeners')) { + $dispatcher = $this->app[Dispatcher::class]; + // @phpstan-ignore-next-line + $dispatcher->listen(function (\Laravel\Octane\Events\OperationTerminated $event) { + // @phpstan-ignore-next-line + $event->sandbox->make(PermissionRegistrar::class)->setPermissionsTeamId(null); + }); + + if (! $this->app['config']->get('permission.register_octane_reset_listener')) { return; } - - $dispatcher = $this->app[Dispatcher::class]; // @phpstan-ignore-next-line - $dispatcher->listen(\Laravel\Octane\Events\OperationTerminated::class, OctaneReloadPermissions::class); + $dispatcher->listen(function (\Laravel\Octane\Events\OperationTerminated $event) { + // @phpstan-ignore-next-line + $event->sandbox->make(PermissionRegistrar::class)->clearPermissionsCollection(); + }); } protected function registerModelBindings(): void From 6376f7be954f67b1a7a81ee9eaaecfbd1de0ab5a Mon Sep 17 00:00:00 2001 From: drbyte Date: Thu, 9 Nov 2023 22:06:43 +0000 Subject: [PATCH 0793/1013] Update CHANGELOG --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32b534be2..bf8211cb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.1.0 - 2023-11-09 + +### What's Changed + +- Reset teamId on octane by @erikn69 in https://github.com/spatie/laravel-permission/pull/2547 + NOTE: The `\Spatie\Permission\Listeners\OctaneReloadPermissions` listener introduced in 6.0.0 is removed in 6.1.0, because the logic is directly incorporated into the ServiceProvider now. + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.0.1...6.1.0 + ## 6.0.1 - 2023-11-06 ### What's Changed @@ -713,6 +722,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -773,6 +783,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From 641bc2f10d7d5db62f8463a5fc011eb6ce24d3df Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 9 Nov 2023 17:09:17 -0500 Subject: [PATCH 0794/1013] Update CHANGELOG.md --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf8211cb6..472c27841 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,11 @@ All notable changes to `laravel-permission` will be documented in this file ### What's Changed -- Reset teamId on octane by @erikn69 in https://github.com/spatie/laravel-permission/pull/2547 +- Reset teamId on Octane by @erikn69 in https://github.com/spatie/laravel-permission/pull/2547 NOTE: The `\Spatie\Permission\Listeners\OctaneReloadPermissions` listener introduced in 6.0.0 is removed in 6.1.0, because the logic is directly incorporated into the ServiceProvider now. + Thanks @jameshulse for the heads-up and code-review. + **Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.0.1...6.1.0 ## 6.0.1 - 2023-11-06 From c66c0de99a95a88288507fa96676b89625f54488 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Mon, 13 Nov 2023 16:34:57 -0500 Subject: [PATCH 0795/1013] Update dependabot-auto-merge.yml (#2555) --- .github/workflows/dependabot-auto-merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 60183c521..144a946ce 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -13,7 +13,7 @@ jobs: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v1.6.0 + uses: dependabot/fetch-metadata@v1 with: github-token: "${{ secrets.GITHUB_TOKEN }}" compat-lookup: true From 3bf179b7eb2b7912032c1e7ced73e92edcd8206d Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 19 Nov 2023 02:58:03 -0500 Subject: [PATCH 0796/1013] Add note about needing to implement canAny in User model --- docs/installation-lumen.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation-lumen.md b/docs/installation-lumen.md index 7709777e6..84189b345 100644 --- a/docs/installation-lumen.md +++ b/docs/installation-lumen.md @@ -66,7 +66,7 @@ php artisan migrate --- ## User Model -NOTE: Remember that Laravel's authorization layer requires that your `User` model implement the `Illuminate\Contracts\Auth\Access\Authorizable` contract. In Lumen you will then also need to use the `Laravel\Lumen\Auth\Authorizable` trait. +NOTE: Remember that Laravel's authorization layer requires that your `User` model implement the `Illuminate\Contracts\Auth\Access\Authorizable` contract. In Lumen you will then also need to use the `Laravel\Lumen\Auth\Authorizable` trait. And if you want to use middleware from this package, you will need to implement the `canAny()` method from [Illuminate\Foundation\Auth\Access\Authorizable](https://github.com/laravel/framework/blob/10.x/src/Illuminate/Foundation/Auth/Access/Authorizable.php) in your `User` model because Lumen doesn't include it in its `Authorizable` trait. --- ## User Table From 4011bf8068307bfc065fa33c7bd0e3dd179073a1 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 19 Nov 2023 12:14:41 -0500 Subject: [PATCH 0797/1013] Middleware limitations --- docs/installation-lumen.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/installation-lumen.md b/docs/installation-lumen.md index 84189b345..1364e727e 100644 --- a/docs/installation-lumen.md +++ b/docs/installation-lumen.md @@ -31,17 +31,19 @@ You will also need the `config/auth.php` file. If you don't already have it, cop cp vendor/laravel/lumen-framework/config/auth.php config/auth.php ``` -Then, in `bootstrap/app.php`, uncomment the `auth` middleware, and register this package's middleware: +Next, if you wish to use this package's middleware, clone whichever ones you want from `Spatie\Permission\Middleware` namespace into your own `App\Http\Middleware` namespace AND replace the `canAny()` call with `hasAnyPermission()` (because Lumen doesn't support `canAny()`). + +Then, in `bootstrap/app.php`, uncomment the `auth` middleware, and register the middleware you've created. For example: ```php $app->routeMiddleware([ 'auth' => App\Http\Middleware\Authenticate::class, - 'permission' => Spatie\Permission\Middleware\PermissionMiddleware::class, - 'role' => Spatie\Permission\Middleware\RoleMiddleware::class, + 'permission' => App\Http\Middleware\PermissionMiddleware::class, // cloned from Spatie\Permission\Middleware + 'role' => App\Http\Middleware\RoleMiddleware::class, // cloned from Spatie\Permission\Middleware ]); ``` -... and in the same file, in the ServiceProviders section, register the package configuration, service provider, and cache alias: +... and also in `bootstrap/app.php`, in the ServiceProviders section, register the package configuration, service provider, and cache alias: ```php $app->configure('permission'); @@ -66,7 +68,7 @@ php artisan migrate --- ## User Model -NOTE: Remember that Laravel's authorization layer requires that your `User` model implement the `Illuminate\Contracts\Auth\Access\Authorizable` contract. In Lumen you will then also need to use the `Laravel\Lumen\Auth\Authorizable` trait. And if you want to use middleware from this package, you will need to implement the `canAny()` method from [Illuminate\Foundation\Auth\Access\Authorizable](https://github.com/laravel/framework/blob/10.x/src/Illuminate/Foundation/Auth/Access/Authorizable.php) in your `User` model because Lumen doesn't include it in its `Authorizable` trait. +NOTE: Remember that Laravel's authorization layer requires that your `User` model implement the `Illuminate\Contracts\Auth\Access\Authorizable` contract. In Lumen you will then also need to use the `Laravel\Lumen\Auth\Authorizable` trait. Note that Lumen does not support the `User::canAny()` authorization method. --- ## User Table From eb61a65cc4e9e3359ddcd78f740ffe616979af20 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 19 Nov 2023 12:46:28 -0500 Subject: [PATCH 0798/1013] Add create statements to examples --- docs/basic-usage/wildcard-permissions.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/basic-usage/wildcard-permissions.md b/docs/basic-usage/wildcard-permissions.md index f1afc046d..c35ce0f5c 100644 --- a/docs/basic-usage/wildcard-permissions.md +++ b/docs/basic-usage/wildcard-permissions.md @@ -5,9 +5,9 @@ weight: 6 When enabled, wildcard permissions offers you a flexible representation for a variety of permission schemes. The idea behind wildcard permissions is inspired by the default permission implementation of - [Apache Shiro](https://shiro.apache.org/permissions.html). + [Apache Shiro](https://shiro.apache.org/permissions.html). See the Shiro documentation for more examples. -## Enabling Wildcard Feature +## Enabling Wildcard Features Wildcard permissions can be enabled in the permission config file: @@ -27,11 +27,11 @@ $permission = 'posts.create.1'; The meaning of each part of the string depends on the application layer. > You can use as many parts as you like. So you are not limited to the three-tiered structure, even though -this is the common use-case, representing {resource}.{action}.{target}. +this is the common use-case, representing `{resource}.{action}.{target}`. -> NOTE: You must actually create the permissions (eg: `posts.create.1`) before you can assign them or check for them. +> **NOTE: You must actually create the wildcarded permissions** (eg: `posts.create.1`) before you can assign them or check for them. -> NOTE: You must create any wildcard permission patterns (eg: `posts.create.*`) before you can assign them or check for them. +> **NOTE: You must create any wildcard permission patterns** (eg: `posts.create.*`) before you can assign them or check for them. ## Using Wildcards @@ -47,7 +47,7 @@ Permission::create(['name'=>'posts']); $user->givePermissionTo('posts'); ``` -Everyone who is assigned to this permission will be allowed every action on posts. It is not necessary to use a +Given the example above, everyone who is assigned to this permission will be allowed every action on posts. It is not necessary to use a wildcard on the last part of the string. This is automatically assumed. ```php @@ -55,13 +55,14 @@ wildcard on the last part of the string. This is automatically assumed. $user->can('posts.create'); $user->can('posts.edit'); $user->can('posts.delete'); -``` +``` +(Note that the `posts.create` and `posts.edit` and `posts.delete` permissions must also be created.) -## Meaning of the `*` Asterisk +## Meaning of the * Asterisk The `*` means "ALL". It does **not** mean "ANY". -Thus `can('post.*')` will only pass if the user has been assigned `post.*` explicitly. +Thus `can('post.*')` will only pass if the user has been assigned `post.*` explicitly, and the `post.*` Permission has been created. ## Subparts @@ -71,12 +72,15 @@ powerful feature that lets you create complex permission schemes. ```php // user can only do the actions create, update and view on both resources posts and users +Permission::create(['name'=>'posts,users.create,update,view']); $user->givePermissionTo('posts,users.create,update,view'); // user can do the actions create, update, view on any available resource +Permission::create(['name'=>'*.create,update,view']); $user->givePermissionTo('*.create,update,view'); // user can do any action on posts with ids 1, 4 and 6 +Permission::create(['name'=>'posts.*.1,4,6']); $user->givePermissionTo('posts.*.1,4,6'); ``` From 5e448d08a0c69cb29ed7dbd6eba92b77d2f5f506 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 19 Nov 2023 12:50:31 -0500 Subject: [PATCH 0799/1013] Reference Shiro docs for more examples --- docs/basic-usage/wildcard-permissions.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/basic-usage/wildcard-permissions.md b/docs/basic-usage/wildcard-permissions.md index c35ce0f5c..b1412f5cc 100644 --- a/docs/basic-usage/wildcard-permissions.md +++ b/docs/basic-usage/wildcard-permissions.md @@ -3,9 +3,10 @@ title: Wildcard permissions weight: 6 --- -When enabled, wildcard permissions offers you a flexible representation for a variety of permission schemes. The idea - behind wildcard permissions is inspired by the default permission implementation of - [Apache Shiro](https://shiro.apache.org/permissions.html). See the Shiro documentation for more examples. +When enabled, wildcard permissions offers you a flexible representation for a variety of permission schemes. + +The wildcard permissions implementation is inspired by the default permission implementation of + [Apache Shiro](https://shiro.apache.org/permissions.html). See the Shiro documentation for more examples and deeper explanation of the concepts. ## Enabling Wildcard Features @@ -84,4 +85,4 @@ Permission::create(['name'=>'posts.*.1,4,6']); $user->givePermissionTo('posts.*.1,4,6'); ``` -> As said before, the meaning of each part is determined by the application layer! So, you are free to use each part as you like. And you can use as many parts and subparts as you want. +> Remember: the meaning of each 'part' is determined by your application! So, you are free to use each part as you like. And you can use as many parts and subparts as you want. From 978cf240cca1556d2a12bf72b9167b8c9c9fdf08 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 19 Nov 2023 15:01:20 -0500 Subject: [PATCH 0800/1013] Do you really need a UI? --- docs/advanced-usage/ui-options.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/advanced-usage/ui-options.md b/docs/advanced-usage/ui-options.md index d9628e766..17f70b85f 100644 --- a/docs/advanced-usage/ui-options.md +++ b/docs/advanced-usage/ui-options.md @@ -5,11 +5,15 @@ weight: 11 ## Need a UI? -The package doesn't come with any screens out of the box, you should build that yourself. Here are some options to get you started: +The package doesn't come with any UI/screens out of the box, you should build that yourself. + +But: [do you really need a UI? Consider what Aaron and Joel have to say in this podcast episode](https://show.nocompromises.io/episodes/should-you-manage-roles-and-permissions-with-a-ui) + +If you decide you need a UI, even if it's not for creating/editing role/permission names, but just for controlling which Users have access to which roles/permissions, following are some options to get you started: - [Code With Tony - video series](https://www.youtube.com/watch?v=lGfV1ddMhHA) to create an admin panel for managing roles and permissions in Laravel 9. -- [FilamentPHP plugin](https://filamentphp.com/plugins/tharinda-rodrigo-spatie-roles-permissions) to manage roles and permissions using this package. +- [FilamentPHP plugin](https://filamentphp.com/plugins/tharinda-rodrigo-spatie-roles-permissions) to manage roles and permissions using this package. (There are a few other Filament plugins which do similarly; use whichever suits your needs best.) - If you'd like to build your own UI, and understand the underlying logic for Gates and Roles and Users, the [Laravel 6 User Login and Management With Roles](https://www.youtube.com/watch?v=7PpJsho5aak&list=PLxFwlLOncxFLazmEPiB4N0iYc3Dwst6m4) video series by Mark Twigg of Penguin Digital gives thorough coverage to the topic, the theory, and implementation of a basic Roles system, independent of this Permissions Package. From 8ab6bb1195750df55ef6f79dfb3ad46c73bb60c7 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 2 Dec 2023 19:05:44 -0500 Subject: [PATCH 0801/1013] Test suite updates --- phpunit.xml.dist | 5 +++-- tests/RoleWithNestingTest.php | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1e08d1014..bd9254886 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -12,7 +12,8 @@ - - + + + diff --git a/tests/RoleWithNestingTest.php b/tests/RoleWithNestingTest.php index 909f671de..c08476c20 100644 --- a/tests/RoleWithNestingTest.php +++ b/tests/RoleWithNestingTest.php @@ -10,10 +10,10 @@ class RoleWithNestingTest extends TestCase protected $useCustomModels = true; /** @var Role[] */ - protected $parent_roles = []; + protected array $parent_roles = []; /** @var Role[] */ - protected $child_roles = []; + protected array $child_roles = []; protected function setUp(): void { @@ -67,7 +67,7 @@ public function it_returns_correct_withCount_of_nested_roles($role_group, $index $role = $this->$role_group[$index]; $count_field_name = sprintf('%s_count', $relation); - $actualCount = intval(Role::withCount($relation)->find($role->getKey())->$count_field_name); + $actualCount = (int)Role::withCount($relation)->find($role->getKey())->$count_field_name; $this->assertSame( $expectedCount, @@ -76,7 +76,7 @@ public function it_returns_correct_withCount_of_nested_roles($role_group, $index ); } - public function roles_list() + public static function roles_list() { return [ ['parent_roles', 'has_no_children', 'children', 0], From 373dcb4b975c89003ec253c31047b0a65458acd4 Mon Sep 17 00:00:00 2001 From: drbyte Date: Sun, 3 Dec 2023 00:06:31 +0000 Subject: [PATCH 0802/1013] Fix styling --- tests/RoleWithNestingTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/RoleWithNestingTest.php b/tests/RoleWithNestingTest.php index c08476c20..fb28b8c18 100644 --- a/tests/RoleWithNestingTest.php +++ b/tests/RoleWithNestingTest.php @@ -67,7 +67,7 @@ public function it_returns_correct_withCount_of_nested_roles($role_group, $index $role = $this->$role_group[$index]; $count_field_name = sprintf('%s_count', $relation); - $actualCount = (int)Role::withCount($relation)->find($role->getKey())->$count_field_name; + $actualCount = (int) Role::withCount($relation)->find($role->getKey())->$count_field_name; $this->assertSame( $expectedCount, From 66638c1cf9eb137de5787c2bf44242db718f0ad3 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 2 Dec 2023 19:23:46 -0500 Subject: [PATCH 0803/1013] Clean up comment --- src/PermissionRegistrar.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 051e48b0f..3c3b0d7f0 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -187,7 +187,7 @@ public function clearClassPermissions() /** * Load permissions from cache - * This get cache and turns array into \Illuminate\Database\Eloquent\Collection + * And turns permissions array into a \Illuminate\Database\Eloquent\Collection */ private function loadPermissions(): void { From b6b9f4f1120cb5cfbebfc2dc9bf59ef1191cfe38 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 2 Dec 2023 19:25:55 -0500 Subject: [PATCH 0804/1013] Update to accept Laravel 11 and PHPUnit 10 --- .gitignore | 1 + composer.json | 12 ++++++------ phpunit.xml.dist | 10 +++++++--- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index da5ec4b46..da248ba1f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ composer.lock vendor tests/temp .idea +.phpunit.cache .phpunit.result.cache .php-cs-fixer.cache tests/CreatePermissionCustomTables.php diff --git a/composer.json b/composer.json index e7af6faa2..ab0515b8e 100644 --- a/composer.json +++ b/composer.json @@ -23,15 +23,15 @@ "homepage": "https://github.com/spatie/laravel-permission", "require": { "php": "^8.0", - "illuminate/auth": "^8.12|^9.0|^10.0", - "illuminate/container": "^8.12|^9.0|^10.0", - "illuminate/contracts": "^8.12|^9.0|^10.0", - "illuminate/database": "^8.12|^9.0|^10.0" + "illuminate/auth": "^8.12|^9.0|^10.0|^11.0", + "illuminate/container": "^8.12|^9.0|^10.0|^11.0", + "illuminate/contracts": "^8.12|^9.0|^10.0|^11.0", + "illuminate/database": "^8.12|^9.0|^10.0|^11.0" }, "require-dev": { "laravel/passport": "^11.0", - "orchestra/testbench": "^6.23|^7.0|^8.0", - "phpunit/phpunit": "^9.4" + "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0", + "phpunit/phpunit": "^9.4|^10.1" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/phpunit.xml.dist b/phpunit.xml.dist index bd9254886..96497e656 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,10 +1,14 @@ - - + + src/ - + tests From c24211a47bda2bdcb27e04114808ecad5fc25263 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Fri, 8 Dec 2023 20:39:42 -0500 Subject: [PATCH 0805/1013] fix duplicates on sync (#2574) --- src/Traits/HasPermissions.php | 7 ++++--- src/Traits/HasRoles.php | 7 ++++--- tests/HasPermissionsTest.php | 10 ++++++++++ tests/HasRolesTest.php | 10 ++++++++++ 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 2618b380b..1582e36d6 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -370,9 +370,10 @@ private function collectPermissions(...$permissions): array return $array; } - $this->ensureModelSharesGuard($permission); - - $array[] = $permission->getKey(); + if (! in_array($permission->getKey(), $array)) { + $this->ensureModelSharesGuard($permission); + $array[] = $permission->getKey(); + } return $array; }, []); diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index f02f7c160..a8f183c04 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -129,9 +129,10 @@ private function collectRoles(...$roles): array return $array; } - $this->ensureModelSharesGuard($role); - - $array[] = $role->getKey(); + if (! in_array($role->getKey(), $array)) { + $this->ensureModelSharesGuard($role); + $array[] = $role->getKey(); + } return $array; }, []); diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index 0eeb961a9..bc7b84f93 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -541,6 +541,16 @@ public function it_can_sync_multiple_permissions() $this->assertFalse($this->testUser->hasDirectPermission('edit-news')); } + /** @test */ + public function it_can_avoid_sync_duplicated_permissions() + { + $this->testUser->syncPermissions('edit-articles', 'edit-blog', 'edit-blog'); + + $this->assertTrue($this->testUser->hasDirectPermission('edit-articles')); + + $this->assertTrue($this->testUser->hasDirectPermission('edit-blog')); + } + /** @test */ public function it_can_sync_multiple_permissions_by_id() { diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index ec7bccc72..7b0e54e3c 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -259,6 +259,16 @@ public function it_can_sync_roles_from_a_string_on_a_permission() $this->assertTrue($this->testUserPermission->hasRole('testRole2')); } + /** @test */ + public function it_can_avoid_sync_duplicated_roles() + { + $this->testUser->syncRoles('testRole', 'testRole', 'testRole2'); + + $this->assertTrue($this->testUser->hasRole('testRole')); + + $this->assertTrue($this->testUser->hasRole('testRole2')); + } + /** @test */ public function it_can_sync_multiple_roles() { From 299dd2c9bce700ea641021c1aea0dfece25e541d Mon Sep 17 00:00:00 2001 From: drbyte Date: Sat, 9 Dec 2023 01:40:07 +0000 Subject: [PATCH 0806/1013] Fix styling --- src/Models/Permission.php | 6 +++--- src/Models/Role.php | 8 ++++---- src/PermissionRegistrar.php | 2 +- src/Traits/HasRoles.php | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 95df94fb6..d23d15ac2 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -86,7 +86,7 @@ public function users(): BelongsToMany * * @throws PermissionDoesNotExist */ - public static function findByName(string $name, string $guardName = null): PermissionContract + public static function findByName(string $name, ?string $guardName = null): PermissionContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); $permission = static::getPermission(['name' => $name, 'guard_name' => $guardName]); @@ -104,7 +104,7 @@ public static function findByName(string $name, string $guardName = null): Permi * * @throws PermissionDoesNotExist */ - public static function findById(int|string $id, string $guardName = null): PermissionContract + public static function findById(int|string $id, ?string $guardName = null): PermissionContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); $permission = static::getPermission([(new static())->getKeyName() => $id, 'guard_name' => $guardName]); @@ -121,7 +121,7 @@ public static function findById(int|string $id, string $guardName = null): Permi * * @return PermissionContract|Permission */ - public static function findOrCreate(string $name, string $guardName = null): PermissionContract + public static function findOrCreate(string $name, ?string $guardName = null): PermissionContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); $permission = static::getPermission(['name' => $name, 'guard_name' => $guardName]); diff --git a/src/Models/Role.php b/src/Models/Role.php index 4fd6e782e..4b2e48ff4 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -95,7 +95,7 @@ public function users(): BelongsToMany * * @throws RoleDoesNotExist */ - public static function findByName(string $name, string $guardName = null): RoleContract + public static function findByName(string $name, ?string $guardName = null): RoleContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); @@ -113,7 +113,7 @@ public static function findByName(string $name, string $guardName = null): RoleC * * @return RoleContract|Role */ - public static function findById(int|string $id, string $guardName = null): RoleContract + public static function findById(int|string $id, ?string $guardName = null): RoleContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); @@ -131,7 +131,7 @@ public static function findById(int|string $id, string $guardName = null): RoleC * * @return RoleContract|Role */ - public static function findOrCreate(string $name, string $guardName = null): RoleContract + public static function findOrCreate(string $name, ?string $guardName = null): RoleContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); @@ -176,7 +176,7 @@ protected static function findByParam(array $params = []): ?RoleContract * * @throws PermissionDoesNotExist|GuardDoesNotMatch */ - public function hasPermissionTo($permission, string $guardName = null): bool + public function hasPermissionTo($permission, ?string $guardName = null): bool { if ($this->getWildcardClass()) { return $this->hasWildcardPermission($permission, $guardName); diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 3c3b0d7f0..1300def8f 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -145,7 +145,7 @@ public function forgetCachedPermissions() return $this->cache->forget($this->cacheKey); } - public function forgetWildcardPermissionIndex(Model $record = null): void + public function forgetWildcardPermissionIndex(?Model $record = null): void { if ($record) { unset($this->wildcardPermissionsIndex[get_class($record)][$record->getKey()]); diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index a8f183c04..97f87ba9a 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -218,7 +218,7 @@ public function syncRoles(...$roles) * * @param string|int|array|Role|Collection|\BackedEnum $roles */ - public function hasRole($roles, string $guard = null): bool + public function hasRole($roles, ?string $guard = null): bool { $this->loadMissing('roles'); @@ -282,7 +282,7 @@ public function hasAnyRole(...$roles): bool * * @param string|array|Role|Collection|\BackedEnum $roles */ - public function hasAllRoles($roles, string $guard = null): bool + public function hasAllRoles($roles, ?string $guard = null): bool { $this->loadMissing('roles'); @@ -324,7 +324,7 @@ public function hasAllRoles($roles, string $guard = null): bool * * @param string|array|Role|Collection $roles */ - public function hasExactRoles($roles, string $guard = null): bool + public function hasExactRoles($roles, ?string $guard = null): bool { $this->loadMissing('roles'); From 31838377ca1896f45a766813ddee1b473bf7bb1e Mon Sep 17 00:00:00 2001 From: drbyte Date: Sat, 9 Dec 2023 01:42:13 +0000 Subject: [PATCH 0807/1013] Update CHANGELOG --- CHANGELOG.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 472c27841..d358d73a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,23 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.2.0 - 2023-12-09 + +### What's Changed + +* Skip duplicates on sync (was triggering Integrity Constraint Violation error) by @erikn69 in https://github.com/spatie/laravel-permission/pull/2574 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.1.0...6.2.0 + ## 6.1.0 - 2023-11-09 ### What's Changed -- Reset teamId on Octane by @erikn69 in https://github.com/spatie/laravel-permission/pull/2547 +- Reset teamId on Octane by @erikn69 in https://github.com/spatie/laravel-permission/pull/2547 NOTE: The `\Spatie\Permission\Listeners\OctaneReloadPermissions` listener introduced in 6.0.0 is removed in 6.1.0, because the logic is directly incorporated into the ServiceProvider now. - + Thanks @jameshulse for the heads-up and code-review. + **Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.0.1...6.1.0 @@ -725,6 +734,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -786,6 +796,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From f518fd548c51f4d9b1ccd8aa395d003e2022106e Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 19 Dec 2023 14:19:04 -0500 Subject: [PATCH 0808/1013] Update bug_report.md --- .github/ISSUE_TEMPLATE/bug_report.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 328f945dd..6b62c8f54 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,6 @@ --- name: Bug report -about: Report or reproducable bug +about: Report a reproducible bug title: '' labels: '' assignees: '' @@ -8,7 +8,7 @@ assignees: '' --- **Before creating a new bug report** -Please check if there isn't a similar issue on [the issue tracker](https://github.com/spatie/laravel-permission/issues) or in [the discussions](https://github.com/spatie/laravel-permission/discussions). +Please check that there isn't already a similar issue on [the issue tracker](https://github.com/spatie/laravel-permission/issues) or in [the discussions](https://github.com/spatie/laravel-permission/discussions). **Describe the bug** A clear and concise description of what the bug is. @@ -16,7 +16,7 @@ A clear and concise description of what the bug is. **Versions** You can use `composer show` to get the version numbers of: - spatie/laravel-permission package version: -- illuminate/framework package +- laravel/framework package PHP version: @@ -40,4 +40,5 @@ Add any other context about the problem here. **Environment (please complete the following information, because it helps us investigate better):** - OS: [e.g. macOS] - Version [e.g. 22] + From 4d119986c862ac0168b77338c85d8236bb559a88 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 24 Dec 2023 01:58:02 -0500 Subject: [PATCH 0809/1013] Octane: Clear wildcard permissions on Tick (#2583) Fixes #2575 --- src/PermissionRegistrar.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 1300def8f..56c7415a8 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -173,6 +173,7 @@ public function getWildcardPermissionIndex(Model $record): array public function clearPermissionsCollection(): void { $this->permissions = null; + $this->wildcardPermissionsIndex = []; } /** From 8f1bf1029f8ceb81d8d99e209703b51747298841 Mon Sep 17 00:00:00 2001 From: drbyte Date: Sun, 24 Dec 2023 07:00:02 +0000 Subject: [PATCH 0810/1013] Update CHANGELOG --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d358d73a1..5b6100624 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.3.0 - 2023-12-24 + +### What's Changed + +* Octane Fix: Clear wildcard permissions on Tick in https://github.com/spatie/laravel-permission/pull/2583 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.2.0...6.3.0 + ## 6.2.0 - 2023-12-09 ### What's Changed @@ -735,6 +743,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -797,6 +806,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From 7c1ca135434ecedfd2132edd63929f9f797947e3 Mon Sep 17 00:00:00 2001 From: Arne Breitsprecher Date: Sun, 24 Dec 2023 18:40:27 +0100 Subject: [PATCH 0811/1013] Update to use Larastan Org (#2585) --- .github/workflows/phpstan.yml | 2 +- phpstan.neon.dist | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 587e6c2d7..00e3e5efe 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -24,7 +24,7 @@ jobs: - name: Install larastan run: | - composer require "nunomaduro/larastan" --no-interaction --no-update + composer require "larastan/larastan" --no-interaction --no-update composer update --prefer-dist --no-interaction - name: Run PHPStan diff --git a/phpstan.neon.dist b/phpstan.neon.dist index f4b16051f..3a451a165 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ includes: - - ./vendor/nunomaduro/larastan/extension.neon + - ./vendor/larastan/larastan/extension.neon - phpstan-baseline.neon parameters: From 6e5d59cb9bd96c6637c404a124bdeab48c9fb346 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Tue, 26 Dec 2023 11:49:09 -0500 Subject: [PATCH 0812/1013] laravel-pint-action to major version tag (#2586) --- .github/workflows/fix-php-code-style-issues.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/fix-php-code-style-issues.yml b/.github/workflows/fix-php-code-style-issues.yml index 53c026f89..60dc90d6f 100644 --- a/.github/workflows/fix-php-code-style-issues.yml +++ b/.github/workflows/fix-php-code-style-issues.yml @@ -16,7 +16,7 @@ jobs: ref: ${{ github.head_ref }} - name: Fix PHP code style issues - uses: aglipanci/laravel-pint-action@2.3.0 + uses: aglipanci/laravel-pint-action@v2 - name: Commit changes uses: stefanzweifel/git-auto-commit-action@v5 From 16b8b478734b72a0a2ebbf407195aadffdcb4298 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 29 Dec 2023 12:19:29 -0500 Subject: [PATCH 0813/1013] Explain no-typo in Middleware namespace Closes #2588 --- docs/basic-usage/middleware.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index fee16f8ed..57290c68d 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -42,6 +42,7 @@ protected $middlewareAliases = [ **YOU SHOULD ALSO** set [the `$middlewarePriority` array](https://laravel.com/docs/master/middleware#sorting-middleware) to include this package's middleware before the `SubstituteBindings` middleware, else you may get *404 Not Found* responses when a *403 Not Authorized* response might be expected. +> See a typo? Note that since v6 the 'Middleware' namespace is singular. Prior to v6 it was 'Middlewares'. Time to upgrade your app! ## Middleware via Routes From 9b02e54c2b3aa009128b0df3099fb808fa36b85c Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 13 Jan 2024 19:06:35 -0500 Subject: [PATCH 0814/1013] Update Seeder docs with Factory States example --- docs/advanced-usage/seeding.md | 38 ++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/advanced-usage/seeding.md b/docs/advanced-usage/seeding.md index 0f40e7f27..207fbff42 100644 --- a/docs/advanced-usage/seeding.md +++ b/docs/advanced-usage/seeding.md @@ -18,6 +18,8 @@ You can optionally flush the cache before seeding by using the `SetUp()` method Or it can be done directly in a seeder class, as shown below. +## Roles/Permissions Seeder + Here is a sample seeder, which first clears the cache, creates permissions and then assigns permissions to roles (the order of these steps is intentional): ```php @@ -54,6 +56,42 @@ class RolesAndPermissionsSeeder extends Seeder } ``` +## User Seeding with Factories and States + +To use Factory States to assign roles after creating users: + +```php +// Factory: + public function definition() {...} + + public function active(): static + { + return $this->state(fn (array $attributes) => [ + 'status' => 1, + ]) + ->afterCreating(function (User $user) { + $user->assignRole('ActiveMember'); + }); + } + +// Seeder: +// To create 4 users using this 'active' state in a Seeder: +User::factory(4)->active()->create(); +``` + +To seed multiple users and then assign each of them a role, WITHOUT using Factory States: + +```php +// Seeder: +User::factory() + ->count(50) + ->create() + ->each(function ($user) { + $user->assignRole('Member'); + }); +``` + + ## Speeding up seeding for large data sets When seeding large quantities of roles or permissions you may consider using Eloquent's `insert` command instead of `create`, as this bypasses all the internal checks that this package does when calling `create` (including extra queries to verify existence, test guards, etc). From 0e593bb9e6808cc55c67f150878b0847f20b9036 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 19 Jan 2024 19:47:32 -0500 Subject: [PATCH 0815/1013] cross-name uuid with ulid/guid --- docs/advanced-usage/uuid.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index 79c1331bb..108ea2142 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -1,14 +1,16 @@ --- -title: UUID +title: UUID/ULID weight: 7 --- -If you're using UUIDs for your User models there are a few considerations to note. +If you're using UUIDs (ULID, GUID, etc) for your User models or Role/Permission models there are a few considerations to note. -> THIS IS NOT A FULL LESSON ON HOW TO IMPLEMENT UUIDs IN YOUR APP. +> NOTE: THIS IS NOT A FULL LESSON ON HOW TO IMPLEMENT UUIDs IN YOUR APP. Since each UUID implementation approach is different, some of these may or may not benefit you. As always, your implementation may vary. +We use "uuid" in the examples below. Adapt for ULID or GUID as needed. + ## Migrations You will need to update the `create_permission_tables.php` migration after creating it with `php artisan vendor:publish`. After making your edits, be sure to run the migration! From eea80901effa275000439f1346a9d667370f024b Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 27 Feb 2024 15:37:15 -0500 Subject: [PATCH 0816/1013] Update .gitattributes --- .gitattributes | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.gitattributes b/.gitattributes index 47d2cd035..b47d0893d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,16 +5,15 @@ /.github export-ignore /.gitattributes export-ignore /.gitignore export-ignore -/.travis.yml export-ignore +/.phpunit.result.cache export-ignore /phpunit.xml.dist export-ignore -/.scrutinizer.yml export-ignore /art export-ignore /docs export-ignore /tests export-ignore /.editorconfig export-ignore /.php_cs.dist.php export-ignore /phpstan* export-ignore -/.styleci.yml export-ignore +/psalm.xml export-ignore /CHANGELOG.md export-ignore /CONTRIBUTING.md export-ignore From 05cce017fe3ac78f60a3fce78c07fe6e8e6e6e52 Mon Sep 17 00:00:00 2001 From: Raheel Khan Date: Wed, 28 Feb 2024 13:11:20 +0500 Subject: [PATCH 0817/1013] add laravel 11 to workflow run tests (#2605) * add laravel 11 to workflow run tests * Passport 12 * override passport:install --------- Co-authored-by: Chris Brown Co-authored-by: drbyte --- .github/workflows/run-tests.yml | 8 +++++++- composer.json | 2 +- src/Guard.php | 4 ++-- tests/TestCase.php | 8 ++++++-- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 84e1a0f95..cd56424e6 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -10,9 +10,11 @@ jobs: fail-fast: false matrix: php: [8.3, 8.2, 8.1, 8.0] - laravel: ["^10.0", "^9.0", "^8.12"] + laravel: ["^11.0", "^10.0", "^9.0", "^8.12"] dependency-version: [prefer-lowest, prefer-stable] include: + - laravel: "^11.0" + testbench: 9.* - laravel: "^10.0" testbench: 8.* - laravel: "^9.0" @@ -20,6 +22,10 @@ jobs: - laravel: "^8.12" testbench: "^6.23" exclude: + - laravel: "^11.0" + php: 8.1 + - laravel: "^11.0" + php: 8.0 - laravel: "^10.0" php: 8.0 - laravel: "^8.12" diff --git a/composer.json b/composer.json index ab0515b8e..bd29fa40e 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "illuminate/database": "^8.12|^9.0|^10.0|^11.0" }, "require-dev": { - "laravel/passport": "^11.0", + "laravel/passport": "^11.0|^12.0", "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0", "phpunit/phpunit": "^9.4|^10.1" }, diff --git a/src/Guard.php b/src/Guard.php index dd5023148..4f697ce20 100644 --- a/src/Guard.php +++ b/src/Guard.php @@ -13,7 +13,7 @@ class Guard * Return a collection of guard names suitable for the $model, * as indicated by the presence of a $guard_name property or a guardName() method on the model. * - * @param string|Model $model model class object or name + * @param string|Model $model model class object or name */ public static function getNames($model): Collection { @@ -58,7 +58,7 @@ protected static function getConfigAuthGuards(string $class): Collection /** * Lookup a guard name relevant for the $class model and the current user. * - * @param string|Model $class model class object or name + * @param string|Model $class model class object or name * @return string guard name */ public static function getDefaultName($class): string diff --git a/tests/TestCase.php b/tests/TestCase.php index 48de05ed5..24af65bf9 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -203,8 +203,12 @@ protected function setUpPassport($app): void $app['config']->set('permission.use_passport_client_credentials', true); $app['config']->set('auth.guards.api', ['driver' => 'passport', 'provider' => 'users']); - $this->artisan('migrate'); - $this->artisan('passport:install'); + // mimic passport:install (must load migrations using our own call to loadMigrationsFrom() else rollbacks won't occur, and migrations will be left in skeleton directory + $this->artisan('passport:keys'); + $this->loadMigrationsFrom(__DIR__.'/../vendor/laravel/passport/database/migrations/'); + $provider = in_array('users', array_keys(config('auth.providers'))) ? 'users' : null; + $this->artisan('passport:client', ['--personal' => true, '--name' => config('app.name').' Personal Access Client']); + $this->artisan('passport:client', ['--password' => true, '--name' => config('app.name').' Password Grant Client', '--provider' => $provider]); $this->testClient = Client::create(['name' => 'Test', 'redirect' => 'https://example.com/', 'personal_access_client' => 0, 'password_client' => 0, 'revoked' => 0]); $this->testClientRole = $app[Role::class]->create(['name' => 'clientRole', 'guard_name' => 'api']); From edecbd8750b5223e462699e59202295e70cddc14 Mon Sep 17 00:00:00 2001 From: drbyte Date: Wed, 28 Feb 2024 08:14:24 +0000 Subject: [PATCH 0818/1013] Update CHANGELOG --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b6100624..a66d5ea07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,27 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.4.0 - 2024-02-28 + +* Laravel 11 Support + +### What's Changed + +* Add Laravel 11 to workflow run tests by @mraheelkhan in https://github.com/spatie/laravel-permission/pull/2605 +* And Passport 12 + +### Internals + +* Update to use Larastan Org by @arnebr in https://github.com/spatie/laravel-permission/pull/2585 +* laravel-pint-action to major version tag by @erikn69 in https://github.com/spatie/laravel-permission/pull/2586 + +### New Contributors + +* @arnebr made their first contribution in https://github.com/spatie/laravel-permission/pull/2585 +* @mraheelkhan made their first contribution in https://github.com/spatie/laravel-permission/pull/2605 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.3.0...6.4.0 + ## 6.3.0 - 2023-12-24 ### What's Changed @@ -744,6 +765,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -807,6 +829,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From e1bb52d2de03d002ee3f0b59324cc688f0ef53f7 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 28 Feb 2024 03:15:45 -0500 Subject: [PATCH 0819/1013] Update .gitattributes --- .gitattributes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index b47d0893d..07110aeb2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,7 +13,7 @@ /.editorconfig export-ignore /.php_cs.dist.php export-ignore /phpstan* export-ignore -/psalm.xml export-ignore +/psalm.xml export-ignore /CHANGELOG.md export-ignore /CONTRIBUTING.md export-ignore From 29b16ce4a1eaaa1b0eb7ab185fb035e662a5523b Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 28 Feb 2024 05:14:22 -0500 Subject: [PATCH 0820/1013] Update .gitattributes --- .gitattributes | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitattributes b/.gitattributes index 07110aeb2..9da33a6cd 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,7 +5,6 @@ /.github export-ignore /.gitattributes export-ignore /.gitignore export-ignore -/.phpunit.result.cache export-ignore /phpunit.xml.dist export-ignore /art export-ignore /docs export-ignore @@ -13,7 +12,6 @@ /.editorconfig export-ignore /.php_cs.dist.php export-ignore /phpstan* export-ignore -/psalm.xml export-ignore /CHANGELOG.md export-ignore /CONTRIBUTING.md export-ignore From 5243ff97aa2bcc2d7d636a9720cd5d39cc315290 Mon Sep 17 00:00:00 2001 From: Ali Sasani Date: Fri, 1 Mar 2024 22:34:49 +0330 Subject: [PATCH 0821/1013] [feat] simplify the definition of multiple Blade "if" directives (#2628) * [feat] simplify the definition of a Blade directives --------- Co-authored-by: erikn69 --- src/PermissionServiceProvider.php | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index a9011cd2b..a23c1eddd 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -122,32 +122,20 @@ public static function bladeMethodWrapper($method, $role, $guard = null): bool return auth($guard)->check() && auth($guard)->user()->{$method}($role); } - protected function registerBladeExtensions($bladeCompiler): void + protected function registerBladeExtensions(BladeCompiler $bladeCompiler): void { $bladeMethodWrapper = '\\Spatie\\Permission\\PermissionServiceProvider::bladeMethodWrapper'; - $bladeCompiler->directive('role', fn ($args) => ""); - $bladeCompiler->directive('elserole', fn ($args) => ""); - $bladeCompiler->directive('endrole', fn () => ''); + // permission checks + $bladeCompiler->if('haspermission', fn () => $bladeMethodWrapper('checkPermissionTo', ...func_get_args())); - $bladeCompiler->directive('haspermission', fn ($args) => ""); - $bladeCompiler->directive('elsehaspermission', fn ($args) => ""); - $bladeCompiler->directive('endhaspermission', fn () => ''); - - $bladeCompiler->directive('hasrole', fn ($args) => ""); - $bladeCompiler->directive('endhasrole', fn () => ''); - - $bladeCompiler->directive('hasanyrole', fn ($args) => ""); - $bladeCompiler->directive('endhasanyrole', fn () => ''); - - $bladeCompiler->directive('hasallroles', fn ($args) => ""); - $bladeCompiler->directive('endhasallroles', fn () => ''); - - $bladeCompiler->directive('unlessrole', fn ($args) => ""); + // role checks + $bladeCompiler->if('role', fn () => $bladeMethodWrapper('hasRole', ...func_get_args())); + $bladeCompiler->if('hasrole', fn () => $bladeMethodWrapper('hasRole', ...func_get_args())); + $bladeCompiler->if('hasanyrole', fn () => $bladeMethodWrapper('hasAnyRole', ...func_get_args())); + $bladeCompiler->if('hasallroles', fn () => $bladeMethodWrapper('hasAllRoles', ...func_get_args())); + $bladeCompiler->if('hasexactroles', fn () => $bladeMethodWrapper('hasExactRoles', ...func_get_args())); $bladeCompiler->directive('endunlessrole', fn () => ''); - - $bladeCompiler->directive('hasexactroles', fn ($args) => ""); - $bladeCompiler->directive('endhasexactroles', fn () => ''); } protected function registerMacroHelpers(): void From 1b1ba2bf849c66178841542a10f3765563df37d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 00:53:12 -0500 Subject: [PATCH 0822/1013] Bump ramsey/composer-install from 2 to 3 (#2630) --- .github/workflows/phpstan.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 00e3e5efe..609d55a6d 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -20,7 +20,7 @@ jobs: coverage: none - name: Install composer dependencies - uses: ramsey/composer-install@v2 + uses: ramsey/composer-install@v3 - name: Install larastan run: | From 3e4630f3c2160d952ad96023b32b6c186a62ec5f Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Tue, 5 Mar 2024 15:01:13 +0100 Subject: [PATCH 0823/1013] Update README.md --- README.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/README.md b/README.md index 25ff759bd..1a5e00473 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,6 @@ # Associate users with permissions and roles -### Sponsor - - - - - - -
If you want to quickly add authentication and authorization to Laravel projects, feel free to check Auth0's Laravel SDK and free plan at https://auth0.com/developers.
- - [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-permission.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-permission) [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/spatie/laravel-permission/run-tests-L8.yml?branch=main&label=Tests)](https://github.com/spatie/laravel-permission/actions?query=workflow%3ATests+branch%3Amain) [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-permission.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-permission) From e4fd5ca7be0d7f7654c1151ff3614eb28c3a19a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Benoit?= <39646949+Androlax2@users.noreply.github.com> Date: Wed, 6 Mar 2024 12:27:43 -0500 Subject: [PATCH 0824/1013] fix(team): Add nullable team_id (#2607) --- src/PermissionRegistrar.php | 2 +- src/helpers.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 56c7415a8..3dcaac17b 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -109,7 +109,7 @@ public function setPermissionsTeamId($id): void } /** - * @return int|string + * @return int|string|null */ public function getPermissionsTeamId() { diff --git a/src/helpers.php b/src/helpers.php index d1aae1ee2..55048d753 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -14,7 +14,7 @@ function getModelForGuard(string $guard) if (! function_exists('setPermissionsTeamId')) { /** - * @param int|string|\Illuminate\Database\Eloquent\Model $id + * @param int|string|null|\Illuminate\Database\Eloquent\Model $id */ function setPermissionsTeamId($id) { @@ -24,7 +24,7 @@ function setPermissionsTeamId($id) if (! function_exists('getPermissionsTeamId')) { /** - * @return int|string + * @return int|string|null */ function getPermissionsTeamId() { From 00edc6659c6df4b782767c556a8c93caefceb403 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 12 Mar 2024 17:36:37 -0400 Subject: [PATCH 0825/1013] Update with Breeze example --- docs/basic-usage/new-app.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index b3f112d0b..ab3eeca65 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -16,6 +16,12 @@ If you're new to Laravel or to any of the concepts mentioned here, you can learn ```sh cd ~/Sites laravel new mypermissionsdemo +# (Choose Laravel Breeze, choose Blade with Alpine) +# (choose your own dark-mode-support choice) +# (choose your desired testing framework) +# (say Yes to initialize a Git repo, so that you can track your code changes) +# (Choose SQLite) + cd mypermissionsdemo git init git add . @@ -38,8 +44,10 @@ php artisan migrate:fresh sed -i '' $'s/use HasFactory, Notifiable;/use HasFactory, Notifiable;\\\n use \\\\Spatie\\\\Permission\\\\Traits\\\\HasRoles;/' app/Models/User.php sed -i '' $'s/use HasApiTokens, HasFactory, Notifiable;/use HasApiTokens, HasFactory, Notifiable;\\\n use \\\\Spatie\\\\Permission\\\\Traits\\\\HasRoles;/' app/Models/User.php git add . && git commit -m "Add HasRoles trait" +``` -# Add Laravel's basic auth scaffolding +If you didn't install Laravel Breeze or Jetstream, add Laravel's basic auth scaffolding: +```php composer require laravel/ui --dev php artisan ui bootstrap --auth # npm install && npm run prod @@ -125,11 +133,10 @@ Super-Admins are a common feature. The following approach allows that when your - Add a Gate::before check in your `AuthServiceProvider`: ```diff ++ use Illuminate\Support\Facades\Gate; + public function boot() { - $this->registerPolicies(); - - // + // Implicitly grant "Super-Admin" role all permission checks using can() + Gate::before(function ($user, $ability) { From 5703bd92329902ee96a89321a010cd071192955c Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 12 Mar 2024 17:37:01 -0400 Subject: [PATCH 0826/1013] Include some simple Laravel 11 demo examples --- docs/basic-usage/new-app.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index ab3eeca65..14d319c17 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -155,6 +155,43 @@ Your app will have Models, Controllers, routes, Views, Factories, Policies, Test You can see examples of these in the demo app at https://github.com/drbyte/spatie-permissions-demo/ + +### Quick Examples +If you are creating a demo app for reporting a bug or getting help with troubleshooting something, skip this section and proceed to "Sharing" below. + +If this is your first app with this package, you may want some quick permission examples to see it in action. If you've set up your app using the instructions above, the following examples will work in conjunction with the users and permissions created in the seeder. + +Three users were created: test@example.com, admin@example.com, superadmin@example.com and the password for each is "password". + +`/resources/views/dashboard.php` +```diff +
+ {{ __("You're logged in!") }} +
++ @can('edit articles') ++ You can EDIT ARTICLES. ++ @endcan ++ @can('publish articles') ++ You can PUBLISH ARTICLES. ++ @endcan ++ @can('only super-admins can see this section') ++ Congratulations, you are a super-admin! ++ @endcan +``` +With the above code, when you login with each respective user, you will see different messages based on that access. + +Here's a routes example with Breeze and Laravel 11. +Edit `/routes/web.php`: +```diff +-Route::middleware('auth')->group(function () { ++Route::middleware('role_or_permission:publish articles')->group(function () { + Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); + Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); + Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); +}); +``` +With the above change, you will be unable to access the user "Profile" page unless you are logged in with "admin" or "super-admin". You could change `role_or_permission:publish_articles` to `role:writer` to make it only available to the "test" user. + ## Sharing To share your app on Github for easy collaboration: From 40eafbf5ef30141398647d01b828f2df3095676c Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 12 Mar 2024 17:37:19 -0400 Subject: [PATCH 0827/1013] Laravel 11 middleware --- docs/basic-usage/middleware.md | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index 57290c68d..ca6d49e3c 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -13,7 +13,7 @@ Route::group(['middleware' => ['can:publish articles']], function () { }); ``` -In Laravel v10.9 and up, you can also call this middleware with a static method. +Since Laravel v10.9, you can also call this middleware with a static method. ```php Route::group(['middleware' => [\Illuminate\Auth\Middleware\Authorize::using('publish articles')]], function () { @@ -24,9 +24,24 @@ Route::group(['middleware' => [\Illuminate\Auth\Middleware\Authorize::using('pub ## Package Middleware This package comes with `RoleMiddleware`, `PermissionMiddleware` and `RoleOrPermissionMiddleware` middleware. -You can add them inside your `app/Http/Kernel.php` file to be able to use them through aliases. -Note the property name difference between Laravel 10 and older versions of Laravel: +You can register their aliases for easy reference elsewhere in your app: + +> See a typo? Note that since v6 the 'Middleware' namespace is singular. Prior to v6 it was 'Middlewares'. Time to upgrade your app! + +In Laravel 11 open `/bootstrap/app.php` and register it there: + +```php + ->withMiddleware(function (Middleware $middleware) { + $middleware->alias([ + 'role' => \Spatie\Permission\Middleware\RoleMiddleware::class, + 'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class, + 'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class, + ]); + }) +``` + +In Laravel 9 and 10 you can add them in `app/Http/Kernel.php`: ```php // Laravel 9 uses $routeMiddleware = [ @@ -40,9 +55,9 @@ protected $middlewareAliases = [ ]; ``` -**YOU SHOULD ALSO** set [the `$middlewarePriority` array](https://laravel.com/docs/master/middleware#sorting-middleware) to include this package's middleware before the `SubstituteBindings` middleware, else you may get *404 Not Found* responses when a *403 Not Authorized* response might be expected. +### Middleware Priority +If your app is triggering *404 Not Found* responses when a *403 Not Authorized* response might be expected, it might be a middleware priority clash. Explore reordering priorities so that this package's middleware runs before Laravel's `SubstituteBindings` middleware. (See [Middleware docs](https://laravel.com/docs/master/middleware#sorting-middleware) ). In Laravel 11 you could explore `$middleware->prependToGroup()` instead. -> See a typo? Note that since v6 the 'Middleware' namespace is singular. Prior to v6 it was 'Middlewares'. Time to upgrade your app! ## Middleware via Routes From af37376a6061607b541ee90b150cc79bfb0d736d Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 12 Mar 2024 17:45:19 -0400 Subject: [PATCH 0828/1013] Update example syntax --- docs/basic-usage/new-app.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index 14d319c17..6834c717c 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -74,7 +74,7 @@ class PermissionsDemoSeeder extends Seeder * * @return void */ - public function run() + public function run(): void { // Reset cached roles and permissions app()[PermissionRegistrar::class]->forgetCachedPermissions(); @@ -137,7 +137,6 @@ Super-Admins are a common feature. The following approach allows that when your public function boot() { - + // Implicitly grant "Super-Admin" role all permission checks using can() + Gate::before(function ($user, $ability) { + if ($user->hasRole('Super-Admin')) { From f40a866abb415770593558152dcdbb372cfec886 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 12 Mar 2024 17:45:42 -0400 Subject: [PATCH 0829/1013] Add Laravel 11 training link --- docs/basic-usage/new-app.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index 6834c717c..87fcbfc04 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -9,7 +9,7 @@ If you want to just try out the features of this package you can get started wit The examples on this page are primarily added for assistance in creating a quick demo app for troubleshooting purposes, to post the repo on github for convenient sharing to collaborate or get support. -If you're new to Laravel or to any of the concepts mentioned here, you can learn more in the [Laravel documentation](https://laravel.com/docs/) and in the free videos at Laracasts such as with the [Laravel From Scratch series](https://laracasts.com/series/laravel-8-from-scratch/). +If you're new to Laravel or to any of the concepts mentioned here, you can learn more in the [Laravel documentation](https://laravel.com/docs/) and in the free videos at Laracasts such as with the [Laravel 11 in 30 days](https://laracasts.com/series/30-days-to-learn-laravel-11) or [Laravel 8 From Scratch](https://laracasts.com/series/laravel-8-from-scratch/) series. ### Initial setup: From ab7871a28c7821f9cf6de386d545d0447f073388 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 12 Mar 2024 20:48:02 -0400 Subject: [PATCH 0830/1013] Laravel 11 --- docs/installation-laravel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 39e8087ee..fd0cfb59f 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -9,7 +9,7 @@ Choose the version of this package that suits your Laravel version. Package Version | Laravel Version ----------------|----------- - ^6.0 | 8,9,10 (PHP 8.0+) + ^6.0 | 8,9,10,11 (PHP 8.0+) ^5.8 | 7,8,9,10 ^5.7 | 7,8,9 ^5.4-^5.6 | 7,8 From 15ab449cb5355c8e58e33c686085d8dfba474a9c Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 12 Mar 2024 21:04:02 -0400 Subject: [PATCH 0831/1013] Update link to example table --- docs/installation-lumen.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation-lumen.md b/docs/installation-lumen.md index 1364e727e..1cff50275 100644 --- a/docs/installation-lumen.md +++ b/docs/installation-lumen.md @@ -74,7 +74,7 @@ NOTE: Remember that Laravel's authorization layer requires that your `User` mode ## User Table NOTE: If you are working with a fresh install of Lumen, then you probably also need a migration file for your Users table. You can create your own, or you can copy a basic one from Laravel: -[https://github.com/laravel/laravel/blob/master/database/migrations/2014_10_12_000000_create_users_table.php](https://github.com/laravel/laravel/blob/master/database/migrations/2014_10_12_000000_create_users_table.php) +[https://github.com/laravel/laravel/blob/master/database/migrations/0001_01_01_000000_create_users_table.php](https://github.com/laravel/laravel/blob/master/database/migrations/0001_01_01_000000_create_users_table.php) (You will need to run `php artisan migrate` after adding this file.) From 9ef09aafea999268e4b342df085ba29e1833df35 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 12 Mar 2024 21:09:36 -0400 Subject: [PATCH 0832/1013] Highlight example in migration --- docs/prerequisites.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/prerequisites.md b/docs/prerequisites.md index 78773b379..88f3ad3e8 100644 --- a/docs/prerequisites.md +++ b/docs/prerequisites.md @@ -47,6 +47,12 @@ MySQL 8.0 limits index keys to 1000 characters. This package publishes a migrati Thus in your AppServiceProvider you will need to set `Schema::defaultStringLength(125)`. [See the Laravel Docs for instructions](https://laravel.com/docs/migrations#index-lengths-mysql-mariadb). +You may be able to bypass setting `defaultStringLength(125)` by editing the migration and specifying the `125` in 4 fields. There are 2 instances of this code snippet where you can explicitly set the `125`: +```php + $table->string('name'); // For MySQL 8.0 use string('name', 125); + $table->string('guard_name'); // For MySQL 8.0 use string('guard_name', 125); +``` + ## Note for apps using UUIDs/ULIDs/GUIDs This package expects the primary key of your `User` model to be an auto-incrementing `int`. If it is not, you may need to modify the `create_permission_tables` migration and/or modify the default configuration. See [https://spatie.be/docs/laravel-permission/advanced-usage/uuid](https://spatie.be/docs/laravel-permission/advanced-usage/uuid) for more information. From 359aae28c3c449471e74fb8c0fb23a4333e0ebe9 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 12 Mar 2024 21:24:17 -0400 Subject: [PATCH 0833/1013] Add clarity to cache-reset API usage --- docs/advanced-usage/cache.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/advanced-usage/cache.md b/docs/advanced-usage/cache.md index fc6b5e59a..588f77f42 100644 --- a/docs/advanced-usage/cache.md +++ b/docs/advanced-usage/cache.md @@ -10,9 +10,12 @@ Role and Permission data are cached to speed up performance. When you **use the built-in functions** for manipulating roles and permissions, the cache is automatically reset for you, and relations are automatically reloaded for the current model record: ```php +// When handling permissions assigned to roles: $role->givePermissionTo('edit articles'); $role->revokePermissionTo('edit articles'); $role->syncPermissions(params); + +// When linking roles to permissions: $permission->assignRole('writer'); $permission->removeRole('writer'); $permission->syncRoles(params); @@ -25,7 +28,7 @@ Additionally, because the Role and Permission models are Eloquent models which i **NOTE: User-specific role/permission assignments are kept in-memory since v4.4.0, so the cache-reset is no longer called since v5.1.0 when updating User-related assignments.** Examples: ```php -// These do not call a cache-reset, because the User-related assignments are in-memory. +// These operations on a User do not call a cache-reset, because the User-related assignments are in-memory. $user->assignRole('writer'); $user->removeRole('writer'); $user->syncRoles(params); From c102f2877f603ae4d34cb291997d5090790530dc Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 12 Mar 2024 21:25:21 -0400 Subject: [PATCH 0834/1013] Add import to example --- docs/advanced-usage/custom-permission-check.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/advanced-usage/custom-permission-check.md b/docs/advanced-usage/custom-permission-check.md index 54e9b3035..0f1c88728 100644 --- a/docs/advanced-usage/custom-permission-check.md +++ b/docs/advanced-usage/custom-permission-check.md @@ -18,6 +18,8 @@ You could, for example, create a `Gate::before()` method call to handle this: **app/Providers/AuthServiceProvider.php** ```php +use Illuminate\Support\Facades\Gate; + public function boot() { ... From 67181d22f960eea490a0b9ea2d8d530cfc600e2e Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 12 Mar 2024 21:29:48 -0400 Subject: [PATCH 0835/1013] Updated example --- docs/advanced-usage/seeding.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-usage/seeding.md b/docs/advanced-usage/seeding.md index 207fbff42..d4e58d810 100644 --- a/docs/advanced-usage/seeding.md +++ b/docs/advanced-usage/seeding.md @@ -29,7 +29,7 @@ use Spatie\Permission\Models\Permission; class RolesAndPermissionsSeeder extends Seeder { - public function run() + public function run(): void { // Reset cached roles and permissions app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions(); From 9e3cc860c54c1daa8462d8dd1d0fb3b9b230b74f Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 12 Mar 2024 21:33:35 -0400 Subject: [PATCH 0836/1013] Clarify configuration notes --- docs/advanced-usage/uuid.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index 108ea2142..c25f5b8d3 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -83,7 +83,7 @@ If you also want the roles and permissions to use a UUID for their `id` value, t ## Configuration (OPTIONAL) You might want to change the pivot table field name from `model_id` to `model_uuid`, just for semantic purposes. -For this, in the configuration file edit `column_names.model_morph_key`: +For this, in the `permissions.php` configuration file edit `column_names.model_morph_key`: - OPTIONAL: Change to `model_uuid` instead of the default `model_id`. ```diff @@ -105,10 +105,10 @@ For this, in the configuration file edit `column_names.model_morph_key`: + 'model_morph_key' => 'model_uuid', ], ``` -- If you extend the models into your app, be sure to list those models in your configuration file. See the Extending section of the documentation and the Models section below. +- If you extend the models into your app, be sure to list those models in your `permissions.php` configuration file. See the Extending section of the documentation and the Models section below. ## Models -If you want all the role/permission objects to have a UUID instead of an integer, you will need to Extend the default Role and Permission models into your own namespace in order to set some specific properties. (See the Extending section of the docs, where it explains requirements of Extending, as well as the configuration settings you need to update.) +If you want all the role/permission objects to have a UUID instead of an integer, you will need to Extend the default Role and Permission models into your own namespace in order to set some specific properties. (See the Extending section of the docs, where it explains requirements of Extending, as well as the `permissions.php` configuration settings you need to update.) Examples: From a21a4e3d377d672ee55d07ab9817e4147c3b4c5a Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 13 Mar 2024 14:43:37 -0400 Subject: [PATCH 0837/1013] Formatting updates for clarity --- docs/basic-usage/middleware.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index ca6d49e3c..67d6dc57a 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -23,14 +23,13 @@ Route::group(['middleware' => [\Illuminate\Auth\Middleware\Authorize::using('pub ## Package Middleware +**See a typo? Note that since v6 the _'Middleware'_ namespace is singular. Prior to v6 it was _'Middlewares'_. Time to upgrade your app!** + This package comes with `RoleMiddleware`, `PermissionMiddleware` and `RoleOrPermissionMiddleware` middleware. You can register their aliases for easy reference elsewhere in your app: -> See a typo? Note that since v6 the 'Middleware' namespace is singular. Prior to v6 it was 'Middlewares'. Time to upgrade your app! - In Laravel 11 open `/bootstrap/app.php` and register it there: - ```php ->withMiddleware(function (Middleware $middleware) { $middleware->alias([ @@ -42,7 +41,6 @@ In Laravel 11 open `/bootstrap/app.php` and register it there: ``` In Laravel 9 and 10 you can add them in `app/Http/Kernel.php`: - ```php // Laravel 9 uses $routeMiddleware = [ //protected $routeMiddleware = [ From e49aefe4f90c972e31e8a39ea020866d80db7791 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 13 Mar 2024 15:39:59 -0400 Subject: [PATCH 0838/1013] Formatting improvements --- docs/basic-usage/middleware.md | 105 +++++++++++---------------------- 1 file changed, 35 insertions(+), 70 deletions(-) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index 67d6dc57a..b555683b4 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -8,28 +8,22 @@ weight: 11 For checking against a single permission (see Best Practices) using `can`, you can use the built-in Laravel middleware provided by `\Illuminate\Auth\Middleware\Authorize::class` like this: ```php -Route::group(['middleware' => ['can:publish articles']], function () { - // -}); -``` - -Since Laravel v10.9, you can also call this middleware with a static method. +Route::group(['middleware' => ['can:publish articles']], function () { ... }); -```php -Route::group(['middleware' => [\Illuminate\Auth\Middleware\Authorize::using('publish articles')]], function () { - // -}); +// or with static method (requires Laravel 10.9+) +Route::group(['middleware' => [\Illuminate\Auth\Middleware\Authorize::using('publish articles')]], function () { ... }); ``` ## Package Middleware -**See a typo? Note that since v6 the _'Middleware'_ namespace is singular. Prior to v6 it was _'Middlewares'_. Time to upgrade your app!** +**See a typo? Note that since v6 the _'Middleware'_ namespace is singular. Prior to v6 it was _'Middlewares'_. Time to upgrade your implementation!** This package comes with `RoleMiddleware`, `PermissionMiddleware` and `RoleOrPermissionMiddleware` middleware. You can register their aliases for easy reference elsewhere in your app: -In Laravel 11 open `/bootstrap/app.php` and register it there: +In Laravel 11 open `/bootstrap/app.php` and register them there: + ```php ->withMiddleware(function (Middleware $middleware) { $middleware->alias([ @@ -41,6 +35,7 @@ In Laravel 11 open `/bootstrap/app.php` and register it there: ``` In Laravel 9 and 10 you can add them in `app/Http/Kernel.php`: + ```php // Laravel 9 uses $routeMiddleware = [ //protected $routeMiddleware = [ @@ -54,92 +49,62 @@ protected $middlewareAliases = [ ``` ### Middleware Priority -If your app is triggering *404 Not Found* responses when a *403 Not Authorized* response might be expected, it might be a middleware priority clash. Explore reordering priorities so that this package's middleware runs before Laravel's `SubstituteBindings` middleware. (See [Middleware docs](https://laravel.com/docs/master/middleware#sorting-middleware) ). In Laravel 11 you could explore `$middleware->prependToGroup()` instead. +If your app is triggering *404 Not Found* responses when a *403 Not Authorized* response might be expected, it might be a middleware priority clash. Explore reordering priorities so that this package's middleware runs before Laravel's `SubstituteBindings` middleware. (See [Middleware docs](https://laravel.com/docs/master/middleware#sorting-middleware) ). +In Laravel 11 you could explore `$middleware->prependToGroup()` instead. See the Laravel Documentation for details. -## Middleware via Routes -Then you can protect your routes using middleware rules: +## Using Middleware in Routes and Controllers -```php -Route::group(['middleware' => ['role:manager']], function () { - // -}); +After you have registered the aliases as shown above, you can use them in your Routes and Controllers much the same way you use any other middleware: -// for a specific guard: -Route::group(['middleware' => ['role:manager,api']], function () { - // -}); +### Routes -Route::group(['middleware' => ['permission:publish articles']], function () { - // -}); +```php +Route::group(['middleware' => ['role:manager']], function () { ... }); +Route::group(['middleware' => ['permission:publish articles']], function () { ... }); +Route::group(['middleware' => ['role_or_permission:publish articles']], function () { ... }); -Route::group(['middleware' => ['role:manager','permission:publish articles']], function () { - // -}); +// for a specific guard: +Route::group(['middleware' => ['role:manager,api']], function () { ... }); -Route::group(['middleware' => ['role_or_permission:publish articles']], function () { - // -}); +// multiple middleware +Route::group(['middleware' => ['role:manager','permission:publish articles']], function () { ... }); ``` You can specify multiple roles or permissions with a `|` (pipe) character, which is treated as `OR`: ```php -Route::group(['middleware' => ['role:manager|writer']], function () { - // -}); - -Route::group(['middleware' => ['permission:publish articles|edit articles']], function () { - // -}); +Route::group(['middleware' => ['role:manager|writer']], function () { ... }); +Route::group(['middleware' => ['permission:publish articles|edit articles']], function () { ... }); +Route::group(['middleware' => ['role_or_permission:manager|edit articles']], function () { ... }); // for a specific guard -Route::group(['middleware' => ['permission:publish articles|edit articles,api']], function () { - // -}); - -Route::group(['middleware' => ['role_or_permission:manager|edit articles']], function () { - // -}); +Route::group(['middleware' => ['permission:publish articles|edit articles,api']], function () { ... }); ``` -## Middleware with Controllers - -You can protect your controllers similarly, by setting desired middleware in the constructor: +### Controllers ```php public function __construct() { $this->middleware(['role:manager','permission:publish articles|edit articles']); -} -``` - -```php -public function __construct() -{ + // or $this->middleware(['role_or_permission:manager|edit articles']); + // or with specific guard + $this->middleware(['role_or_permission:manager|edit articles,api']); } ``` -(You can use Laravel's Model Policy feature with your controller methods. See the Model Policies section of these docs.) +You can also use Laravel's Model Policy feature in your controller methods. See the Model Policies section of these docs. -## Use middleware static methods +## Middleware via Static Methods -All of the middleware can also be applied by calling the static `using` method, -which accepts either a `|`-separated string or an array as input. +All of the middleware can also be applied by calling the static `using` method, which accepts either an array or a `|`-separated string as input. ```php -Route::group(['middleware' => [\Spatie\Permission\Middleware\RoleMiddleware::using('manager')]], function () { - // -}); - -Route::group(['middleware' => [\Spatie\Permission\Middleware\PermissionMiddleware::using('publish articles|edit articles')]], function () { - // -}); - -Route::group(['middleware' => [\Spatie\Permission\Middleware\RoleOrPermissionMiddleware::using(['manager', 'edit articles'])]], function () { - // -}); +Route::group(['middleware' => [\Spatie\Permission\Middleware\RoleMiddleware::using('manager')]], function () { ... }); +Route::group(['middleware' => [\Spatie\Permission\Middleware\PermissionMiddleware::using('publish articles|edit articles')]], function () { ... }); +Route::group(['middleware' => [\Spatie\Permission\Middleware\RoleOrPermissionMiddleware::using(['manager', 'edit articles'])]], function () { ... }); ``` + From 5d5049b8ec6ef4f46b6bbce4dc7f85aaef950f00 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 13 Mar 2024 16:14:16 -0400 Subject: [PATCH 0839/1013] Shorten title --- docs/basic-usage/middleware.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index b555683b4..5527a6540 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -1,5 +1,5 @@ --- -title: Using a Middleware +title: Middleware weight: 11 --- From cfbcb0e967df35ec0c6d03d5040536e3e57134d5 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 13 Mar 2024 16:14:46 -0400 Subject: [PATCH 0840/1013] Shorten title --- docs/basic-usage/artisan.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/artisan.md b/docs/basic-usage/artisan.md index 2091560de..0e259da0c 100644 --- a/docs/basic-usage/artisan.md +++ b/docs/basic-usage/artisan.md @@ -1,5 +1,5 @@ --- -title: Using artisan commands +title: Artisan Commands weight: 10 --- From 48769a23893ab9271f6d5d2eb6e648ec2123db77 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 19 Mar 2024 12:20:23 -0400 Subject: [PATCH 0841/1013] Explain canany --- docs/basic-usage/blade-directives.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/basic-usage/blade-directives.md b/docs/basic-usage/blade-directives.md index 3d95f8ec9..4ee4b7b38 100644 --- a/docs/basic-usage/blade-directives.md +++ b/docs/basic-usage/blade-directives.md @@ -24,6 +24,8 @@ When using a permission-name associated with permissions created in this package You can also use `@haspermission('permission-name')` or `@haspermission('permission-name', 'guard_name')` in similar fashion. With corresponding `@endhaspermission`. +There is no `@hasanypermission` directive: use `@canany` instead. + ## Roles As discussed in the Best Practices section of the docs, **it is strongly recommended to always use permission directives**, instead of role directives. From 0c234d6d08dba3d5d429d3a916ed74c587410677 Mon Sep 17 00:00:00 2001 From: Vytautas Smilingis Date: Thu, 21 Mar 2024 16:32:00 +0100 Subject: [PATCH 0842/1013] Update HasPermissions::collectPermissions() docblock (#2641) * Update HasPermissions::collectPermissions() docblock * Update HasRoles::collectRoles() docblock --------- Co-authored-by: Chris Brown --- src/Traits/HasPermissions.php | 2 +- src/Traits/HasRoles.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 1582e36d6..0bc409543 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -352,7 +352,7 @@ public function getAllPermissions(): Collection } /** - * Returns permissions ids as array keys + * Returns array of permissions ids * * @param string|int|array|Permission|Collection|\BackedEnum $permissions */ diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 97f87ba9a..d711a396c 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -111,7 +111,7 @@ public function scopeWithoutRole(Builder $query, $roles, $guard = null): Builder } /** - * Returns roles ids as array keys + * Returns array of role ids * * @param string|int|array|Role|Collection|\BackedEnum $roles */ From 170279843e708ce924a98445932b01c25f2864eb Mon Sep 17 00:00:00 2001 From: spman Date: Fri, 22 Mar 2024 00:19:35 +0800 Subject: [PATCH 0843/1013] Update role-permissions.md (#2631) * Revert #1122 --------- Co-authored-by: Chris Brown --- docs/basic-usage/role-permissions.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/basic-usage/role-permissions.md b/docs/basic-usage/role-permissions.md index ddfd0ce57..20eeedd5e 100644 --- a/docs/basic-usage/role-permissions.md +++ b/docs/basic-usage/role-permissions.md @@ -172,8 +172,3 @@ All these responses are collections of `Spatie\Permission\Models\Permission` obj If we follow the previous example, the first response will be a collection with the `delete article` permission and the second will be a collection with the `edit article` permission and the third will contain both. - - -## NOTE about using permission names in policies - -When calling `authorize()` for a policy method, if you have a permission named the same as one of those policy methods, your permission "name" will take precedence and not fire the policy. For this reason it may be wise to avoid naming your permissions the same as the methods in your policy. While you can define your own method names, you can read more about the defaults Laravel offers in Laravel's documentation at [Writing Policies](https://laravel.com/docs/authorization#writing-policies). From 7408b1c3b62e15419edf777549a804708d692900 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 10:26:46 -0400 Subject: [PATCH 0844/1013] Bump dependabot/fetch-metadata from 1 to 2 (#2642) Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 1 to 2. - [Release notes](https://github.com/dependabot/fetch-metadata/releases) - [Commits](https://github.com/dependabot/fetch-metadata/compare/v1...v2) --- updated-dependencies: - dependency-name: dependabot/fetch-metadata dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/dependabot-auto-merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 144a946ce..c8ac6efa1 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -13,7 +13,7 @@ jobs: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v1 + uses: dependabot/fetch-metadata@v2 with: github-token: "${{ secrets.GITHUB_TOKEN }}" compat-lookup: true From 44bbe88b74b43f9888629326c6d53a24ac65466a Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 2 Apr 2024 17:31:30 -0400 Subject: [PATCH 0845/1013] Note optional steps --- docs/basic-usage/new-app.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index 87fcbfc04..8ec16914d 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -23,11 +23,13 @@ laravel new mypermissionsdemo # (Choose SQLite) cd mypermissionsdemo + +# the following git commands are not needed if you Initialized a git repo while "laravel new" was running above: git init git add . git commit -m "Fresh Laravel Install" -# Environment +# These Environment steps are not needed if you already selected SQLite while "laravel new" was running above: cp -n .env.example .env sed -i '' 's/DB_CONNECTION=mysql/DB_CONNECTION=sqlite/' .env sed -i '' 's/DB_DATABASE=/#DB_DATABASE=/' .env @@ -71,8 +73,6 @@ class PermissionsDemoSeeder extends Seeder { /** * Create the initial roles and permissions. - * - * @return void */ public function run(): void { From 96d800ac460de71f82e8dab57e337131b325ddf5 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 2 Apr 2024 17:39:31 -0400 Subject: [PATCH 0846/1013] Add example for HasMiddleware interface in Laravel 11 Closes #2647 --- docs/basic-usage/middleware.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index 5527a6540..dfd147a3c 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -85,6 +85,18 @@ Route::group(['middleware' => ['permission:publish articles|edit articles,api']] ### Controllers +In Laravel 11, if your controller implements the `HasMiddleware` interface, you can register controller middleware using the `middleware()` method: + +```php +public static function middleware(): array +{ + return [ + 'role_or_permission:manager|edit articles', + ]; +} +``` + +Or, like in prior versions, you can register it in the constructor: ```php public function __construct() { From 8ed61aa42031f3cf06b5f68569af5e2e1e481284 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 2 Apr 2024 17:54:30 -0400 Subject: [PATCH 0847/1013] Clarify older versions --- docs/basic-usage/middleware.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index dfd147a3c..9f8aa2690 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -96,7 +96,7 @@ public static function middleware(): array } ``` -Or, like in prior versions, you can register it in the constructor: +In Laravel 10 and older, you can register it in the constructor: ```php public function __construct() { From d26563e7aed0cbe7ad983a4416425da922c0f209 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 3 Apr 2024 03:14:35 -0400 Subject: [PATCH 0848/1013] Add another example --- docs/basic-usage/middleware.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index 9f8aa2690..89115b139 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -85,13 +85,14 @@ Route::group(['middleware' => ['permission:publish articles|edit articles,api']] ### Controllers -In Laravel 11, if your controller implements the `HasMiddleware` interface, you can register controller middleware using the `middleware()` method: +In Laravel 11, if your controller implements the `HasMiddleware` interface, you can register [controller middleware](https://laravel.com/docs/11.x/controllers#controller-middleware) using the `middleware()` method: ```php public static function middleware(): array { return [ 'role_or_permission:manager|edit articles', + new Middleware('role:author', only: ['index']), ]; } ``` From 536d41d0c82e8fdbeeb2917ef9225b8059d1d96b Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 4 Apr 2024 00:12:47 -0400 Subject: [PATCH 0849/1013] Added more Laravel 11 examples --- docs/basic-usage/middleware.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index 89115b139..259e9214f 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -91,8 +91,11 @@ In Laravel 11, if your controller implements the `HasMiddleware` interface, you public static function middleware(): array { return [ + // examples with aliases, pipe-separated names, guards, etc: 'role_or_permission:manager|edit articles', new Middleware('role:author', only: ['index']), + new Middleware(\Spatie\Permission\Middleware\RoleMiddleware::using('manager'), except:['show']), + new Middleware(\Spatie\Permission\Middleware\PermissionMiddleware::using('delete records,api'), only:['destroy']), ]; } ``` @@ -101,8 +104,8 @@ In Laravel 10 and older, you can register it in the constructor: ```php public function __construct() { + // examples: $this->middleware(['role:manager','permission:publish articles|edit articles']); - // or $this->middleware(['role_or_permission:manager|edit articles']); // or with specific guard $this->middleware(['role_or_permission:manager|edit articles,api']); From a229ad5aa211b809839f3147f0f32ec71b5fef82 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Thu, 18 Apr 2024 13:56:57 -0500 Subject: [PATCH 0850/1013] Fix wrong octane event listener (#2656) --- src/PermissionServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index a23c1eddd..b3a77308c 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -96,7 +96,7 @@ protected function registerOctaneListener(): void $dispatcher = $this->app[Dispatcher::class]; // @phpstan-ignore-next-line - $dispatcher->listen(function (\Laravel\Octane\Events\OperationTerminated $event) { + $dispatcher->listen(function (\Laravel\Octane\Contracts\OperationTerminated $event) { // @phpstan-ignore-next-line $event->sandbox->make(PermissionRegistrar::class)->setPermissionsTeamId(null); }); From 1890d8b49516d84accb8ad8595f596612e135ce3 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 18 Apr 2024 17:26:05 -0400 Subject: [PATCH 0851/1013] [Docs] Fix syntax in docs for UUID migration changes --- docs/advanced-usage/uuid.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index c25f5b8d3..d2740f291 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -40,11 +40,11 @@ If you also want the roles and permissions to use a UUID for their `id` value, t }); Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) { -- $table->unsignedBigInteger(PermissionRegistrar::$pivotPermission); -+ $table->uuid(PermissionRegistrar::$pivotPermission); +- $table->unsignedBigInteger($pivotPermission); ++ $table->uuid($pivotPermission); $table->string('model_type'); //... - $table->foreign(PermissionRegistrar::$pivotPermission) + $table->foreign($pivotPermission) - ->references('id') // permission id + ->references('uuid') // permission id ->on($tableNames['permissions']) @@ -52,28 +52,28 @@ If you also want the roles and permissions to use a UUID for their `id` value, t //... Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames) { -- $table->unsignedBigInteger(PermissionRegistrar::$pivotRole); -+ $table->uuid(PermissionRegistrar::$pivotRole); +- $table->unsignedBigInteger($pivotRole); ++ $table->uuid($pivotRole); //... - $table->foreign(PermissionRegistrar::$pivotRole) + $table->foreign($pivotRole) - ->references('id') // role id + ->references('uuid') // role id ->on($tableNames['roles']) ->onDelete('cascade');//... Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames) { -- $table->unsignedBigInteger(PermissionRegistrar::$pivotPermission); -- $table->unsignedBigInteger(PermissionRegistrar::$pivotRole); -+ $table->uuid(PermissionRegistrar::$pivotPermission); -+ $table->uuid(PermissionRegistrar::$pivotRole); +- $table->unsignedBigInteger($pivotPermission); +- $table->unsignedBigInteger($pivotRole); ++ $table->uuid($pivotPermission); ++ $table->uuid($pivotRole); - $table->foreign(PermissionRegistrar::$pivotPermission) + $table->foreign($pivotPermission) - ->references('id') // permission id + ->references('uuid') // permission id ->on($tableNames['permissions']) ->onDelete('cascade'); - $table->foreign(PermissionRegistrar::$pivotRole) + $table->foreign($pivotRole) - ->references('id') // role id + ->references('uuid') // role id ->on($tableNames['roles']) From d191adae3c8e99251ffd979bd0e7c28f0bbe1597 Mon Sep 17 00:00:00 2001 From: drbyte Date: Thu, 18 Apr 2024 21:54:24 +0000 Subject: [PATCH 0852/1013] Update CHANGELOG --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a66d5ea07..9c36389fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,30 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.5.0 - 2024-04-18 + +### What's Changed + +* Octane: Fix wrong event listener by @erikn69 in https://github.com/spatie/laravel-permission/pull/2656 +* Teams: Add nullable team_id by @Androlax2 in https://github.com/spatie/laravel-permission/pull/2607 +* Blade: simplify the definition of multiple Blade "if" directives by @alissn in https://github.com/spatie/laravel-permission/pull/2628 +* DocBlocks: Update HasPermissions::collectPermissions() docblock by @Plytas in https://github.com/spatie/laravel-permission/pull/2641 + +#### Internals + +* Update role-permissions.md by @killjin in https://github.com/spatie/laravel-permission/pull/2631 +* Bump ramsey/composer-install from 2 to 3 by @dependabot in https://github.com/spatie/laravel-permission/pull/2630 +* Bump dependabot/fetch-metadata from 1 to 2 by @dependabot in https://github.com/spatie/laravel-permission/pull/2642 + +### New Contributors + +* @alissn made their first contribution in https://github.com/spatie/laravel-permission/pull/2628 +* @Androlax2 made their first contribution in https://github.com/spatie/laravel-permission/pull/2607 +* @Plytas made their first contribution in https://github.com/spatie/laravel-permission/pull/2641 +* @killjin made their first contribution in https://github.com/spatie/laravel-permission/pull/2631 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.4.0...6.5.0 + ## 6.4.0 - 2024-02-28 * Laravel 11 Support @@ -766,6 +790,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -830,6 +855,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From 13ff85b1fe61e8dcf7acff839a8cc7cc79685933 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 18 Apr 2024 18:33:13 -0400 Subject: [PATCH 0853/1013] Register about details (#2584) * Register package information in About command output * Fix styling --- src/PermissionServiceProvider.php | 27 +++++++++++++++++++++++++++ tests/CommandTest.php | 18 ++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index b3a77308c..1273b8c1c 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -2,10 +2,12 @@ namespace Spatie\Permission; +use Composer\InstalledVersions; use Illuminate\Contracts\Auth\Access\Gate; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Foundation\Application; use Illuminate\Filesystem\Filesystem; +use Illuminate\Foundation\Console\AboutCommand; use Illuminate\Routing\Route; use Illuminate\Support\Arr; use Illuminate\Support\Collection; @@ -38,6 +40,8 @@ public function boot() }); $this->app->singleton(PermissionRegistrar::class); + + $this->registerAbout(); } public function register() @@ -169,4 +173,27 @@ protected function getMigrationFileName(string $migrationFileName): string ->push($this->app->databasePath()."/migrations/{$timestamp}_{$migrationFileName}") ->first(); } + + protected function registerAbout(): void + { + if (! class_exists(InstalledVersions::class) || ! class_exists(AboutCommand::class)) { + return; + } + + $features = [ + 'Teams' => 'teams', + 'Wildcard-Permissions' => 'enable_wildcard_permission', + 'Octane-Listener' => 'register_octane_reset_listener', + 'Passport' => 'use_passport_client_credentials', + ]; + + AboutCommand::add('Spatie Permissions', fn () => [ + 'Features Enabled' => collect($features) + ->filter(fn (string $feature, string $name): bool => $this->app['config']->get("permission.{$feature}")) + ->keys() + ->whenEmpty(fn (Collection $collection) => $collection->push('Default')) + ->join(', '), + 'Version' => InstalledVersions::getPrettyVersion('spatie/laravel-permission'), + ]); + } } diff --git a/tests/CommandTest.php b/tests/CommandTest.php index 923cad4e0..d4ee4bd18 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -184,4 +184,22 @@ public function it_can_show_roles_by_teams() $this->assertRegExp('/\|\s+\|\s+testRole\s+\|\s+testRole_2\s+\|\s+testRole_Team\s+\|\s+testRole_Team\s+\|/', $output); } } + + /** @test */ + public function it_can_respond_to_about_command() + { + config()->set('permission.teams', true); + app(\Spatie\Permission\PermissionRegistrar::class)->initializeCache(); + + Artisan::call('about'); + + $output = Artisan::output(); + + $pattern = '/Spatie Permissions[ .\n]*Features Enabled[ .]*Teams[ .\n]*Version/'; + if (method_exists($this, 'assertMatchesRegularExpression')) { + $this->assertMatchesRegularExpression($pattern, $output); + } else { // phpUnit 9/8 + $this->assertRegExp($pattern, $output); + } + } } From 63b29ad227b53f1a7f91909dd62d9f9616f3b172 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 18 Apr 2024 22:49:29 -0400 Subject: [PATCH 0854/1013] Fix "about" tests --- src/PermissionServiceProvider.php | 7 ++++-- tests/CommandTest.php | 36 +++++++++++++++++++++++++++++-- tests/TestCase.php | 10 +++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 1273b8c1c..f6dfb8909 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -180,6 +180,7 @@ protected function registerAbout(): void return; } + // array format: 'Display Text' => 'boolean-config-key name' $features = [ 'Teams' => 'teams', 'Wildcard-Permissions' => 'enable_wildcard_permission', @@ -187,9 +188,11 @@ protected function registerAbout(): void 'Passport' => 'use_passport_client_credentials', ]; - AboutCommand::add('Spatie Permissions', fn () => [ + $config = $this->app['config']; + + AboutCommand::add('Spatie Permissions', static fn () => [ 'Features Enabled' => collect($features) - ->filter(fn (string $feature, string $name): bool => $this->app['config']->get("permission.{$feature}")) + ->filter(fn(string $feature, string $name): bool => $config->get("permission.{$feature}")) ->keys() ->whenEmpty(fn (Collection $collection) => $collection->push('Default')) ->join(', '), diff --git a/tests/CommandTest.php b/tests/CommandTest.php index d4ee4bd18..a5bd5b558 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -2,6 +2,8 @@ namespace Spatie\Permission\Tests; +use Composer\InstalledVersions; +use Illuminate\Foundation\Console\AboutCommand; use Illuminate\Support\Facades\Artisan; use Spatie\Permission\Models\Permission; use Spatie\Permission\Models\Role; @@ -186,13 +188,43 @@ public function it_can_show_roles_by_teams() } /** @test */ - public function it_can_respond_to_about_command() + public function it_can_respond_to_about_command_with_default() { - config()->set('permission.teams', true); + if (! class_exists(InstalledVersions::class) || ! class_exists(AboutCommand::class)) { + $this->markTestSkipped(); + } + if (! method_exists(AboutCommand::class, 'flushState')) { + $this->markTestSkipped(); + } + app(\Spatie\Permission\PermissionRegistrar::class)->initializeCache(); Artisan::call('about'); + $output = Artisan::output(); + + $pattern = '/Spatie Permissions[ .\n]*Features Enabled[ .]*Default[ .\n]*Version/'; + if (method_exists($this, 'assertMatchesRegularExpression')) { + $this->assertMatchesRegularExpression($pattern, $output); + } else { // phpUnit 9/8 + $this->assertRegExp($pattern, $output); + } + } + + /** @test */ + public function it_can_respond_to_about_command_with_teams() + { + if (! class_exists(InstalledVersions::class) || ! class_exists(AboutCommand::class)) { + $this->markTestSkipped(); + } + if (! method_exists(AboutCommand::class, 'flushState')) { + $this->markTestSkipped(); + } + app(\Spatie\Permission\PermissionRegistrar::class)->initializeCache(); + + config()->set('permission.teams', true); + + Artisan::call('about'); $output = Artisan::output(); $pattern = '/Spatie Permissions[ .\n]*Features Enabled[ .]*Teams[ .\n]*Version/'; diff --git a/tests/TestCase.php b/tests/TestCase.php index 24af65bf9..3a5532c8b 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,6 +3,7 @@ namespace Spatie\Permission\Tests; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Foundation\Console\AboutCommand; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Cache; @@ -79,6 +80,15 @@ protected function setUp(): void $this->setUpRoutes(); } + protected function tearDown(): void + { + parent::tearDown(); + + if (method_exists(AboutCommand::class, 'flushState')) { + AboutCommand::flushState(); + } + } + /** * @param \Illuminate\Foundation\Application $app * @return array From 11c1c60cfd20225c7330219d952f8def47bb8378 Mon Sep 17 00:00:00 2001 From: drbyte Date: Fri, 19 Apr 2024 02:49:54 +0000 Subject: [PATCH 0855/1013] Fix styling --- src/PermissionServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index f6dfb8909..b57c20ff7 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -192,7 +192,7 @@ protected function registerAbout(): void AboutCommand::add('Spatie Permissions', static fn () => [ 'Features Enabled' => collect($features) - ->filter(fn(string $feature, string $name): bool => $config->get("permission.{$feature}")) + ->filter(fn (string $feature, string $name): bool => $config->get("permission.{$feature}")) ->keys() ->whenEmpty(fn (Collection $collection) => $collection->push('Default')) ->join(', '), From 2c5369e3fbbfb52fae382d82549faa86a4e2a698 Mon Sep 17 00:00:00 2001 From: Gajos Date: Fri, 19 Apr 2024 05:22:50 +0200 Subject: [PATCH 0856/1013] feat(Roles): Support for casting role names to enums (#2616) * feat(Roles): Support for casting role names to enums --- src/Traits/HasRoles.php | 34 ++++++++++++++------ tests/HasRolesTest.php | 26 +++++++++++++-- tests/TestModels/Role.php | 14 ++++++++ tests/TestModels/TestRolePermissionsEnum.php | 2 ++ 4 files changed, 64 insertions(+), 12 deletions(-) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index d711a396c..c7a4b8010 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -228,6 +228,16 @@ public function hasRole($roles, ?string $guard = null): bool if ($roles instanceof \BackedEnum) { $roles = $roles->value; + + return $this->roles + ->when($guard, fn ($q) => $q->where('guard_name', $guard)) + ->contains(function ($role) use ($roles) { + if ($role->name instanceof \BackedEnum) { + return $role->name->value == $roles; + } + + return $role->name == $roles; + }); } if (is_int($roles) || PermissionRegistrar::isUid($roles)) { @@ -295,9 +305,7 @@ public function hasAllRoles($roles, ?string $guard = null): bool } if (is_string($roles)) { - return $guard - ? $this->roles->where('guard_name', $guard)->contains('name', $roles) - : $this->roles->contains('name', $roles); + return $this->hasRole($roles, $guard); } if ($roles instanceof Role) { @@ -312,17 +320,25 @@ public function hasAllRoles($roles, ?string $guard = null): bool return $role instanceof Role ? $role->name : $role; }); - return $roles->intersect( - $guard - ? $this->roles->where('guard_name', $guard)->pluck('name') - : $this->getRoleNames() - ) == $roles; + $roleNames = $guard + ? $this->roles->where('guard_name', $guard)->pluck('name') + : $this->getRoleNames(); + + $roleNames = $roleNames->transform(function ($roleName) { + if ($roleName instanceof \BackedEnum) { + return $roleName->value; + } + + return $roleName; + }); + + return $roles->intersect($roleNames) == $roles; } /** * Determine if the model has exactly all of the given role(s). * - * @param string|array|Role|Collection $roles + * @param string|array|Role|Collection|\BackedEnum $roles */ public function hasExactRoles($roles, ?string $guard = null): bool { diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 7b0e54e3c..a6ebef1d8 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -47,23 +47,43 @@ public function it_can_assign_and_remove_a_role_using_enums() { $enum1 = TestModels\TestRolePermissionsEnum::USERMANAGER; $enum2 = TestModels\TestRolePermissionsEnum::WRITER; + $enum3 = TestModels\TestRolePermissionsEnum::CASTED_ENUM_1; + $enum4 = TestModels\TestRolePermissionsEnum::CASTED_ENUM_2; app(Role::class)->findOrCreate($enum1->value, 'web'); app(Role::class)->findOrCreate($enum2->value, 'web'); + app(Role::class)->findOrCreate($enum3->value, 'web'); + app(Role::class)->findOrCreate($enum4->value, 'web'); $this->assertFalse($this->testUser->hasRole($enum1)); $this->assertFalse($this->testUser->hasRole($enum2)); + $this->assertFalse($this->testUser->hasRole($enum3)); + $this->assertFalse($this->testUser->hasRole($enum4)); + $this->assertFalse($this->testUser->hasRole('user-manager')); + $this->assertFalse($this->testUser->hasRole('writer')); + $this->assertFalse($this->testUser->hasRole('casted_enum-1')); + $this->assertFalse($this->testUser->hasRole('casted_enum-2')); $this->testUser->assignRole($enum1); $this->testUser->assignRole($enum2); + $this->testUser->assignRole($enum3); + $this->testUser->assignRole($enum4); $this->assertTrue($this->testUser->hasRole($enum1)); $this->assertTrue($this->testUser->hasRole($enum2)); + $this->assertTrue($this->testUser->hasRole($enum3)); + $this->assertTrue($this->testUser->hasRole($enum4)); - $this->assertTrue($this->testUser->hasAllRoles([$enum1, $enum2])); - $this->assertFalse($this->testUser->hasAllRoles([$enum1, $enum2, 'not exist'])); + $this->assertTrue($this->testUser->hasRole([$enum1, 'writer'])); + $this->assertTrue($this->testUser->hasRole([$enum3, 'casted_enum-2'])); - $this->assertTrue($this->testUser->hasExactRoles([$enum2, $enum1])); + $this->assertTrue($this->testUser->hasAllRoles([$enum1, $enum2, $enum3, $enum4])); + $this->assertTrue($this->testUser->hasAllRoles(['user-manager', 'writer', 'casted_enum-1', 'casted_enum-2'])); + $this->assertFalse($this->testUser->hasAllRoles([$enum1, $enum2, $enum3, $enum4, 'not exist'])); + $this->assertFalse($this->testUser->hasAllRoles(['user-manager', 'writer', 'casted_enum-1', 'casted_enum-2', 'not exist'])); + + $this->assertTrue($this->testUser->hasExactRoles([$enum4, $enum3, $enum2, $enum1])); + $this->assertTrue($this->testUser->hasExactRoles(['user-manager', 'writer', 'casted_enum-1', 'casted_enum-2'])); $this->testUser->removeRole($enum1); diff --git a/tests/TestModels/Role.php b/tests/TestModels/Role.php index e3b4d77ab..5bd2c55e0 100644 --- a/tests/TestModels/Role.php +++ b/tests/TestModels/Role.php @@ -17,6 +17,20 @@ class Role extends \Spatie\Permission\Models\Role const HIERARCHY_TABLE = 'roles_hierarchy'; + /** + * @return string|\BackedEnum + */ + public function getNameAttribute() + { + $name = $this->attributes['name']; + + if (str_contains($name, 'casted_enum')) { + return TestRolePermissionsEnum::from($name); + } + + return $name; + } + /** * @return BelongsToMany */ diff --git a/tests/TestModels/TestRolePermissionsEnum.php b/tests/TestModels/TestRolePermissionsEnum.php index 858b7f937..0f2badd42 100644 --- a/tests/TestModels/TestRolePermissionsEnum.php +++ b/tests/TestModels/TestRolePermissionsEnum.php @@ -28,6 +28,8 @@ enum TestRolePermissionsEnum: string case EDITOR = 'editor'; case USERMANAGER = 'user-manager'; case ADMIN = 'administrator'; + case CASTED_ENUM_1 = 'casted_enum-1'; + case CASTED_ENUM_2 = 'casted_enum-2'; case VIEWARTICLES = 'view articles'; case EDITARTICLES = 'edit articles'; From 96c276117a91951d851562b29e2767275c41c3de Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 18 Apr 2024 23:29:11 -0400 Subject: [PATCH 0857/1013] Fix permission:show uuid error #2581 (#2582) Use `getKeyName()` instead of `'id'` Fixes #2581 --- src/Commands/Show.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Commands/Show.php b/src/Commands/Show.php index e20a371ed..2cbfb04f5 100644 --- a/src/Commands/Show.php +++ b/src/Commands/Show.php @@ -40,12 +40,12 @@ public function handle() ->when($teamsEnabled, fn ($q) => $q->orderBy($team_key)) ->orderBy('name')->get()->mapWithKeys(fn ($role) => [ $role->name.'_'.($teamsEnabled ? ($role->$team_key ?: '') : '') => [ - 'permissions' => $role->permissions->pluck('id'), + 'permissions' => $role->permissions->pluck($permissionClass->getKeyName()), $team_key => $teamsEnabled ? $role->$team_key : null, ], ]); - $permissions = $permissionClass::whereGuardName($guard)->orderBy('name')->pluck('name', 'id'); + $permissions = $permissionClass::whereGuardName($guard)->orderBy('name')->pluck('name', $permissionClass->getKeyName()); $body = $permissions->map(fn ($permission, $id) => $roles->map( fn (array $role_data) => $role_data['permissions']->contains($id) ? ' ✔' : ' ·' From 7a39f044b682298025b85ce603bd0ee15837b416 Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Fri, 19 Apr 2024 00:47:07 -0300 Subject: [PATCH 0858/1013] feat: cover permission instance verification based on its own guard (#2608) --- src/Traits/HasPermissions.php | 1 + tests/HasPermissionsTest.php | 14 ++++++++++ tests/WildcardHasPermissionsTest.php | 41 ++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 0bc409543..a71da3ad5 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -231,6 +231,7 @@ protected function hasWildcardPermission($permission, $guardName = null): bool } if ($permission instanceof Permission) { + $guardName = $permission->guard_name ?? $guardName; $permission = $permission->name; } diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index bc7b84f93..164ee3dc1 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -20,6 +20,20 @@ public function it_can_assign_a_permission_to_a_user() $this->assertTrue($this->testUser->hasPermissionTo($this->testUserPermission)); } + + /** @test */ + public function it_can_assign_a_permission_to_a_user_with_a_non_default_guard() + { + $testUserPermission = app(Permission::class)->create([ + 'name' => 'edit-articles', + 'guard_name' => 'api', + ]); + + $this->testUser->givePermissionTo($testUserPermission); + + $this->assertTrue($this->testUser->hasPermissionTo($testUserPermission)); + } + /** @test */ public function it_throws_an_exception_when_assigning_a_permission_that_does_not_exist() { diff --git a/tests/WildcardHasPermissionsTest.php b/tests/WildcardHasPermissionsTest.php index 873f35a96..8db36c63b 100644 --- a/tests/WildcardHasPermissionsTest.php +++ b/tests/WildcardHasPermissionsTest.php @@ -31,6 +31,47 @@ public function it_can_check_wildcard_permission() $this->assertFalse($user1->hasPermissionTo('projects.view')); } + /** @test */ + public function it_can_check_wildcard_permission_for_a_non_default_guard() + { + app('config')->set('permission.enable_wildcard_permission', true); + + $user1 = User::create(['email' => 'user1@test.com']); + + $permission1 = Permission::create(['name' => 'articles.edit,view,create', 'guard_name' => 'api']); + $permission2 = Permission::create(['name' => 'news.*', 'guard_name' => 'api']); + $permission3 = Permission::create(['name' => 'posts.*', 'guard_name' => 'api']); + + $user1->givePermissionTo([$permission1, $permission2, $permission3]); + + $this->assertTrue($user1->hasPermissionTo('posts.create', 'api')); + $this->assertTrue($user1->hasPermissionTo('posts.create.123', 'api')); + $this->assertTrue($user1->hasPermissionTo('posts.*', 'api')); + $this->assertTrue($user1->hasPermissionTo('articles.view', 'api')); + $this->assertFalse($user1->hasPermissionTo('projects.view', 'api')); + } + + /** @test */ + public function it_can_check_wildcard_permission_from_instance_without_explicit_guard_argument() + { + app('config')->set('permission.enable_wildcard_permission', true); + + $user1 = User::create(['email' => 'user1@test.com']); + + $permission2 = Permission::create(['name' => 'articles.view']); + $permission1 = Permission::create(['name' => 'articles.edit', 'guard_name' => 'api']); + $permission3 = Permission::create(['name' => 'news.*', 'guard_name' => 'api']); + $permission4 = Permission::create(['name' => 'posts.*', 'guard_name' => 'api']); + + $user1->givePermissionTo([$permission1, $permission2, $permission3]); + + $this->assertTrue($user1->hasPermissionTo($permission1)); + $this->assertTrue($user1->hasPermissionTo($permission2)); + $this->assertTrue($user1->hasPermissionTo($permission3)); + $this->assertFalse($user1->hasPermissionTo($permission4)); + $this->assertFalse($user1->hasPermissionTo('articles.edit')); + } + /** * @test * From 6c529ddb990be9de19c507ed8683eaa28f39fdfb Mon Sep 17 00:00:00 2001 From: drbyte Date: Fri, 19 Apr 2024 03:47:32 +0000 Subject: [PATCH 0859/1013] Fix styling --- tests/HasPermissionsTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index 164ee3dc1..ea156a28e 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -20,12 +20,11 @@ public function it_can_assign_a_permission_to_a_user() $this->assertTrue($this->testUser->hasPermissionTo($this->testUserPermission)); } - /** @test */ public function it_can_assign_a_permission_to_a_user_with_a_non_default_guard() { $testUserPermission = app(Permission::class)->create([ - 'name' => 'edit-articles', + 'name' => 'edit-articles', 'guard_name' => 'api', ]); From 4b786324445622a2a7b3db7e94aa8a54598c5e54 Mon Sep 17 00:00:00 2001 From: drbyte Date: Fri, 19 Apr 2024 03:55:36 +0000 Subject: [PATCH 0860/1013] Update CHANGELOG --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c36389fa..e84ccf216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.6.0 - 2024-04-19 + +### What's Changed + +* Roles: Support for casting role names to enums by @gajosadrian in https://github.com/spatie/laravel-permission/pull/2616 +* Fix permission:show UUID error #2581 by @drbyte in https://github.com/spatie/laravel-permission/pull/2582 +* Cover WilcardPermission instance verification based on its own guard (Allow hasAllPermissions and hasAnyPermission to run on custom guard for WildcardPermission) by @AlexandreBellas in https://github.com/spatie/laravel-permission/pull/2608 +* Register Laravel "About" details by @drbyte in https://github.com/spatie/laravel-permission/pull/2584 + +### New Contributors + +* @gajosadrian made their first contribution in https://github.com/spatie/laravel-permission/pull/2616 +* @AlexandreBellas made their first contribution in https://github.com/spatie/laravel-permission/pull/2608 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.5.0...6.6.0 + ## 6.5.0 - 2024-04-18 ### What's Changed @@ -791,6 +807,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -856,6 +873,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From 17607924aa0aa89bc0153c2ce45ed7c55083367b Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 19 Apr 2024 08:35:28 -0400 Subject: [PATCH 0861/1013] Update Octane contract --- src/PermissionServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index b57c20ff7..1dc28f595 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -109,7 +109,7 @@ protected function registerOctaneListener(): void return; } // @phpstan-ignore-next-line - $dispatcher->listen(function (\Laravel\Octane\Events\OperationTerminated $event) { + $dispatcher->listen(function (\Laravel\Octane\Contracts\OperationTerminated $event) { // @phpstan-ignore-next-line $event->sandbox->make(PermissionRegistrar::class)->clearPermissionsCollection(); }); From da8390f45c1c7eaed72d3cef6d99146811ecf702 Mon Sep 17 00:00:00 2001 From: drbyte Date: Fri, 19 Apr 2024 12:39:03 +0000 Subject: [PATCH 0862/1013] Update CHANGELOG --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e84ccf216..319fe62d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.7.0 - 2024-04-19 + +### What's Changed + +- Fixed remaining Octane event contract. Update to #2656 in release `6.5.0` + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.6.0...6.7.0 + ## 6.6.0 - 2024-04-19 ### What's Changed @@ -808,6 +816,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -874,6 +883,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From ce67a8b0b08c9c54659984cdfd9ea7bac07920b3 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 19 Apr 2024 15:11:37 -0400 Subject: [PATCH 0863/1013] Update property typehint to match use Updates #2607 --- src/PermissionRegistrar.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 3dcaac17b..383629906 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -36,8 +36,7 @@ class PermissionRegistrar public string $teamsKey; - /** @var int|string */ - protected $teamId = null; + protected string|int|null $teamId = null; public string $cacheKey; From 3f64c251759324c18ef2b1c42a01dda21b75f932 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 19 Apr 2024 21:36:03 -0400 Subject: [PATCH 0864/1013] Add PHP 8.4 --- .github/workflows/run-tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index cd56424e6..842b1ca98 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - php: [8.3, 8.2, 8.1, 8.0] + php: [8.4, 8.3, 8.2, 8.1, 8.0] laravel: ["^11.0", "^10.0", "^9.0", "^8.12"] dependency-version: [prefer-lowest, prefer-stable] include: @@ -30,6 +30,8 @@ jobs: php: 8.0 - laravel: "^8.12" php: 8.3 + - laravel: "^8.12" + php: 8.4 name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} From da5c8bc931692fa0a1a0fbda1a5995c9379e4159 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 19 Apr 2024 21:38:27 -0400 Subject: [PATCH 0865/1013] Leave PHP 8.4 until dependencies support it --- .github/workflows/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 842b1ca98..38d97615f 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - php: [8.4, 8.3, 8.2, 8.1, 8.0] + php: [8.3, 8.2, 8.1, 8.0] laravel: ["^11.0", "^10.0", "^9.0", "^8.12"] dependency-version: [prefer-lowest, prefer-stable] include: From 51e3a7552896333ca5afd0ef44e006556cd52a54 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 2 May 2024 18:55:57 -0400 Subject: [PATCH 0866/1013] [Docs] Update Gate examples for Laravel 11 --- docs/basic-usage/super-admin.md | 27 ++++++------ docs/best-practices/using-policies.md | 62 ++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 16 deletions(-) diff --git a/docs/basic-usage/super-admin.md b/docs/basic-usage/super-admin.md index 35392772a..4eefee99d 100644 --- a/docs/basic-usage/super-admin.md +++ b/docs/basic-usage/super-admin.md @@ -11,21 +11,18 @@ Then you can implement the best-practice of primarily using permission-based con ## `Gate::before` If you want a "Super Admin" role to respond `true` to all permissions, without needing to assign all those permissions to a role, you can use [Laravel's `Gate::before()` method](https://laravel.com/docs/master/authorization#intercepting-gate-checks). For example: +In Laravel 11 this would go in the `boot()` method of `AppServiceProvider`: +In Laravel 10 and below it would go in the `boot()` method of `AuthServiceProvider.php`: ```php use Illuminate\Support\Facades\Gate; - -class AuthServiceProvider extends ServiceProvider +// ... +public function boot() { - public function boot() - { -//... - - // Implicitly grant "Super Admin" role all permissions - // This works in the app by using gate-related functions like auth()->user->can() and @can() - Gate::before(function ($user, $ability) { - return $user->hasRole('Super Admin') ? true : null; - }); - } + // Implicitly grant "Super Admin" role all permissions + // This works in the app by using gate-related functions like auth()->user->can() and @can() + Gate::before(function ($user, $ability) { + return $user->hasRole('Super Admin') ? true : null; + }); } ``` @@ -37,11 +34,11 @@ Jeffrey Way explains the concept of a super-admin (and a model owner, and model If you aren't using `Gate::before()` as described above, you could alternatively grant super-admin control by checking the role in individual Policy classes, using the `before()` method. -Here is an example from the [Laravel Documentation on Policy Filters](https://laravel.com/docs/master/authorization#policy-filters) +Here is an example from the [Laravel Documentation on Policy Filters](https://laravel.com/docs/master/authorization#policy-filters), where you can define `before()` in your Policy where needed: ```php -use App\Models\User; // could be any model - +use App\Models\User; // could be any Authorizable model + /** * Perform pre-authorization checks on the model. */ diff --git a/docs/best-practices/using-policies.md b/docs/best-practices/using-policies.md index 607f90e81..504042e09 100644 --- a/docs/best-practices/using-policies.md +++ b/docs/best-practices/using-policies.md @@ -9,4 +9,64 @@ Using Policies allows you to simplify things by abstracting your "control" rules Jeffrey Way explains the concept simply in the [Laravel 6 Authorization Filters](https://laracasts.com/series/laravel-6-from-scratch/episodes/51) and [policies](https://laracasts.com/series/laravel-6-from-scratch/episodes/63) videos and in other related lessons in that chapter. He also mentions how to set up a super-admin, both in a model policy and globally in your application. -You can find an example of implementing a model policy with this Laravel Permissions package in this demo app: [https://github.com/drbyte/spatie-permissions-demo/blob/master/app/Policies/PostPolicy.php](https://github.com/drbyte/spatie-permissions-demo/blob/master/app/Policies/PostPolicy.php) +Here's an example of a PostPolicy which could control access to Post model records: +```php +published) { + return true; + } + + // visitors cannot view unpublished items + if ($user === null) { + return false; + } + + // admin overrides published status + if ($user->can('view unpublished posts')) { + return true; + } + + // authors can view their own unpublished posts + return $user->id == $post->user_id; + } + + public function create(User $user) + { + return ($user->can('create posts')); + } + + public function update(User $user, Post $post) + { + if ($user->can('edit own posts')) { + return $user->id == $post->user_id; + } + + if ($user->can('edit all posts')) { + return true; + } + } + + public function delete(User $user, Post $post) + { + if ($user->can('delete own posts')) { + return $user->id == $post->user_id; + } + + if ($user->can('delete any post')) { + return true; + } + } +} +``` From 84f3138432903636273e3a79cd56eff381fbd312 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 10 May 2024 14:38:54 -0400 Subject: [PATCH 0867/1013] Update bug report template --- .github/ISSUE_TEMPLATE/1_Bug_report.yml | 55 +++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/bug_report.md | 44 -------------------- 2 files changed, 55 insertions(+), 44 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/1_Bug_report.yml delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.yml b/.github/ISSUE_TEMPLATE/1_Bug_report.yml new file mode 100644 index 000000000..96b2d6571 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1_Bug_report.yml @@ -0,0 +1,55 @@ +name: Bug Report +description: "Report a reproducible bug." +body: + - type: markdown + attributes: + value: | + Before creating a new Bug Report, please check that there isn't already a similar issue on [the issue tracker](https://github.com/spatie/laravel-permission/issues) or in [the discussions](https://github.com/spatie/laravel-permission/discussions). + Also, **many issues/questions/problems are already answered** in the [documentation](https://spatie.be/docs/laravel-permission) already. **Please be sure to check the docs** because it will save you time! + + - type: textarea + attributes: + label: Description + description: A clear and concise description of what the bug is. + validations: + required: true + - type: textarea + attributes: + label: Steps To Reproduce + description: How do you trigger this bug? Please walk us through it step by step. + value: | + 1. + 2. + 3. + ... + validations: + required: true + - type: input + attributes: + label: Example Application + description: "Here is a link to my Github repo containing a minimal Laravel application which shows my problem:" + - type: markdown + attributes: + value: | + You can use `composer show` to get package version numbers: + - type: input + attributes: + label: "Version of spatie/laravel-permission package:" + validations: + required: true + - type: input + attributes: + label: "Version of laravel/framework package:" + validations: + required: true + - type: input + attributes: + label: "PHP version:" + validations: + required: true + - type: input + attributes: + label: "Database engine and version:" + - type: input + attributes: + label: "OS: Windows/Mac/Linux version:" diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 6b62c8f54..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: Bug report -about: Report a reproducible bug -title: '' -labels: '' -assignees: '' - ---- - -**Before creating a new bug report** -Please check that there isn't already a similar issue on [the issue tracker](https://github.com/spatie/laravel-permission/issues) or in [the discussions](https://github.com/spatie/laravel-permission/discussions). - -**Describe the bug** -A clear and concise description of what the bug is. - -**Versions** -You can use `composer show` to get the version numbers of: -- spatie/laravel-permission package version: -- laravel/framework package - -PHP version: - -Database version: - - -**To Reproduce** -Steps to reproduce the behavior: - -Here is my example code and/or tests showing the problem in my app: - -**Example Application** -Here is a link to my Github repo containing a minimal Laravel application which shows my problem: - -**Expected behavior** -A clear and concise description of what you expected to happen. - - **Additional context** -Add any other context about the problem here. - -**Environment (please complete the following information, because it helps us investigate better):** - - OS: [e.g. macOS] - - Version [e.g. 22] - - From c99dca701d60e1a2d1f839c9bfd102b8098432fe Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 25 May 2024 21:54:23 -0400 Subject: [PATCH 0868/1013] Document workarounds for 1071 Specified key was too long; max key length is 1000 (or 767) bytes Fixes #2563 Ref #1689 --- .../create_permission_tables.php.stub | 10 +++++---- docs/prerequisites.md | 22 ++++++++++++++----- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/database/migrations/create_permission_tables.php.stub b/database/migrations/create_permission_tables.php.stub index b865d480c..9c7044b46 100644 --- a/database/migrations/create_permission_tables.php.stub +++ b/database/migrations/create_permission_tables.php.stub @@ -25,22 +25,24 @@ return new class extends Migration } Schema::create($tableNames['permissions'], function (Blueprint $table) { + //$table->engine('InnoDB'); $table->bigIncrements('id'); // permission id - $table->string('name'); // For MySQL 8.0 use string('name', 125); - $table->string('guard_name'); // For MySQL 8.0 use string('guard_name', 125); + $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format) + $table->string('guard_name'); // For MyISAM use string('guard_name', 25); $table->timestamps(); $table->unique(['name', 'guard_name']); }); Schema::create($tableNames['roles'], function (Blueprint $table) use ($teams, $columnNames) { + //$table->engine('InnoDB'); $table->bigIncrements('id'); // role id if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable(); $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index'); } - $table->string('name'); // For MySQL 8.0 use string('name', 125); - $table->string('guard_name'); // For MySQL 8.0 use string('guard_name', 125); + $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format) + $table->string('guard_name'); // For MyISAM use string('guard_name', 25); $table->timestamps(); if ($teams || config('permission.testing')) { $table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']); diff --git a/docs/prerequisites.md b/docs/prerequisites.md index 88f3ad3e8..0b0cf300b 100644 --- a/docs/prerequisites.md +++ b/docs/prerequisites.md @@ -43,14 +43,26 @@ This package publishes a `config/permission.php` file. If you already have a fil ## Schema Limitation in MySQL -MySQL 8.0 limits index keys to 1000 characters. This package publishes a migration which combines multiple columns in single index. With `utf8mb4` the 4-bytes-per-character requirement of `mb4` means the max length of the columns in the hybrid index can only be `125` characters. +Potential error message: "1071 Specified key was too long; max key length is 1000 bytes" -Thus in your AppServiceProvider you will need to set `Schema::defaultStringLength(125)`. [See the Laravel Docs for instructions](https://laravel.com/docs/migrations#index-lengths-mysql-mariadb). +MySQL 8.0 limits index key lengths, which might be too short for some compound indexes used by this package. +This package publishes a migration which combines multiple columns in a single index. With `utf8mb4` the 4-bytes-per-character requirement of `mb4` means the total length of the columns in the hybrid index can only be `25%` of that maximum index length. -You may be able to bypass setting `defaultStringLength(125)` by editing the migration and specifying the `125` in 4 fields. There are 2 instances of this code snippet where you can explicitly set the `125`: +- MyISAM tables limit the index to 1000 characters (which is only 250 total chars in `utf8mb4`) +- InnoDB tables using ROW_FORMAT of 'Redundant' or 'Compact' limit the index to 767 characters (which is only 191 total chars in `utf8mb4`) +- InnoDB tables using ROW_FORMAT of 'Dynamic' or 'Compressed' have a 3072 character limit (which is 768 total chars in `utf8mb4`). + +Depending on your MySQL or MariaDB configuration, you may implement one of the following approaches: + +1. Ideally, configure the database to use InnoDB by default, and use ROW FORMAT of 'Dynamic' by default for all new tables. (See [MySQL](https://dev.mysql.com/doc/refman/8.0/en/innodb-limits.html) and [MariaDB](https://mariadb.com/kb/en/innodb-dynamic-row-format/) docs.) + +2. OR if your app doesn't require a longer default, in your AppServiceProvider you can set `Schema::defaultStringLength(125)`. [See the Laravel Docs for instructions](https://laravel.com/docs/migrations#index-lengths-mysql-mariadb). This will have Laravel set all strings to 125 characters by default. + +3. OR you could edit the migration and specify a shorter length for 4 fields. Then in your app be sure to manually impose validation limits on any form fields related to these fields. +There are 2 instances of this code snippet where you can explicitly set the length.: ```php - $table->string('name'); // For MySQL 8.0 use string('name', 125); - $table->string('guard_name'); // For MySQL 8.0 use string('guard_name', 125); + $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format) + $table->string('guard_name'); // For MyISAM use string('guard_name', 25); ``` ## Note for apps using UUIDs/ULIDs/GUIDs From 650d7e9c82f39043b121961e18021439be1892ee Mon Sep 17 00:00:00 2001 From: erikn69 Date: Fri, 21 Jun 2024 15:13:11 -0500 Subject: [PATCH 0869/1013] Fix can't save the same model twice (#2658) --- src/Traits/HasPermissions.php | 6 ++++-- src/Traits/HasRoles.php | 6 ++++-- tests/HasPermissionsTest.php | 1 + tests/HasRolesTest.php | 1 + 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index a71da3ad5..4e38f8b0c 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -401,14 +401,16 @@ public function givePermissionTo(...$permissions) $model->unsetRelation('permissions'); } else { $class = \get_class($model); + $saved = false; $class::saved( - function ($object) use ($permissions, $model, $teamPivot) { - if ($model->getKey() != $object->getKey()) { + function ($object) use ($permissions, $model, $teamPivot, &$saved) { + if ($saved || $model->getKey() != $object->getKey()) { return; } $model->permissions()->attach($permissions, $teamPivot); $model->unsetRelation('permissions'); + $saved = true; } ); } diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index c7a4b8010..88013e850 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -159,14 +159,16 @@ public function assignRole(...$roles) $model->unsetRelation('roles'); } else { $class = \get_class($model); + $saved = false; $class::saved( - function ($object) use ($roles, $model, $teamPivot) { - if ($model->getKey() != $object->getKey()) { + function ($object) use ($roles, $model, $teamPivot, &$saved) { + if ($saved || $model->getKey() != $object->getKey()) { return; } $model->roles()->attach($roles, $teamPivot); $model->unsetRelation('roles'); + $saved = true; } ); } diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index ea156a28e..b785f01d1 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -636,6 +636,7 @@ public function it_can_sync_permissions_to_a_model_that_is_not_persisted() $user = new User(['email' => 'test@user.com']); $user->syncPermissions('edit-articles'); $user->save(); + $user->save(); // test save same model twice $this->assertTrue($user->hasPermissionTo('edit-articles')); diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index a6ebef1d8..9cc13a7c3 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -341,6 +341,7 @@ public function it_will_sync_roles_to_a_model_that_is_not_persisted() $user = new User(['email' => 'test@user.com']); $user->syncRoles([$this->testUserRole]); $user->save(); + $user->save(); // test save same model twice $this->assertTrue($user->hasRole($this->testUserRole)); From 41977f3c553b92c91563e195ae34d2689066f6ce Mon Sep 17 00:00:00 2001 From: drbyte Date: Fri, 21 Jun 2024 20:13:32 +0000 Subject: [PATCH 0870/1013] Fix styling --- tests/HasPermissionsTest.php | 2 +- tests/HasRolesTest.php | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index b785f01d1..4c9528d0e 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -636,7 +636,7 @@ public function it_can_sync_permissions_to_a_model_that_is_not_persisted() $user = new User(['email' => 'test@user.com']); $user->syncPermissions('edit-articles'); $user->save(); - $user->save(); // test save same model twice + $user->save(); // test save same model twice $this->assertTrue($user->hasPermissionTo('edit-articles')); diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 9cc13a7c3..93ee35749 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -341,7 +341,7 @@ public function it_will_sync_roles_to_a_model_that_is_not_persisted() $user = new User(['email' => 'test@user.com']); $user->syncRoles([$this->testUserRole]); $user->save(); - $user->save(); // test save same model twice + $user->save(); // test save same model twice $this->assertTrue($user->hasRole($this->testUserRole)); @@ -831,9 +831,7 @@ public function it_throws_an_exception_if_an_unsupported_type_is_passed_to_hasRo { $this->expectException(\TypeError::class); - $this->testUser->hasRole(new class - { - }); + $this->testUser->hasRole(new class {}); } /** @test */ From 3d248f82b0dea3463d1b25fc1fb5469d11635fff Mon Sep 17 00:00:00 2001 From: erikn69 Date: Fri, 21 Jun 2024 18:49:50 -0500 Subject: [PATCH 0871/1013] Fix phpstan (#2685) --- .github/workflows/phpstan.yml | 6 +++++- phpstan.neon.dist | 1 - src/Contracts/Permission.php | 1 + src/Contracts/Role.php | 1 + src/Traits/HasRoles.php | 10 ++++++---- 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 609d55a6d..75eb21d8d 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -5,6 +5,10 @@ on: paths: - '**.php' - 'phpstan.neon.dist' + pull_request: + paths: + - '**.php' + - 'phpstan.neon.dist' jobs: phpstan: @@ -16,7 +20,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.3 coverage: none - name: Install composer dependencies diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 3a451a165..022878407 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -11,7 +11,6 @@ parameters: - database/migrations/add_teams_fields.php.stub tmpDir: build/phpstan checkOctaneCompatibility: true - checkMissingIterableValueType: false ignoreErrors: - '#Unsafe usage of new static#' diff --git a/src/Contracts/Permission.php b/src/Contracts/Permission.php index 111d2ed2e..f706dbb8d 100644 --- a/src/Contracts/Permission.php +++ b/src/Contracts/Permission.php @@ -10,6 +10,7 @@ * @property string|null $guard_name * * @mixin \Spatie\Permission\Models\Permission + * @phpstan-require-extends \Spatie\Permission\Models\Permission */ interface Permission { diff --git a/src/Contracts/Role.php b/src/Contracts/Role.php index d00201a2a..aae0fff59 100644 --- a/src/Contracts/Role.php +++ b/src/Contracts/Role.php @@ -10,6 +10,7 @@ * @property string|null $guard_name * * @mixin \Spatie\Permission\Models\Role + * @phpstan-require-extends \Spatie\Permission\Models\Role */ interface Role { diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 88013e850..fa537725f 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -233,12 +233,14 @@ public function hasRole($roles, ?string $guard = null): bool return $this->roles ->when($guard, fn ($q) => $q->where('guard_name', $guard)) - ->contains(function ($role) use ($roles) { - if ($role->name instanceof \BackedEnum) { - return $role->name->value == $roles; + ->pluck('name') + ->contains(function ($name) use ($roles) { + /** @var string|\BackedEnum $name */ + if ($name instanceof \BackedEnum) { + return $name->value == $roles; } - return $role->name == $roles; + return $name == $roles; }); } From 5147997e92001bf6cc5cd207eac554bc8f16c02a Mon Sep 17 00:00:00 2001 From: drbyte Date: Fri, 21 Jun 2024 23:50:10 +0000 Subject: [PATCH 0872/1013] Fix styling --- src/Contracts/Permission.php | 1 + src/Contracts/Role.php | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Contracts/Permission.php b/src/Contracts/Permission.php index f706dbb8d..5446e501a 100644 --- a/src/Contracts/Permission.php +++ b/src/Contracts/Permission.php @@ -10,6 +10,7 @@ * @property string|null $guard_name * * @mixin \Spatie\Permission\Models\Permission + * * @phpstan-require-extends \Spatie\Permission\Models\Permission */ interface Permission diff --git a/src/Contracts/Role.php b/src/Contracts/Role.php index aae0fff59..7545b7582 100644 --- a/src/Contracts/Role.php +++ b/src/Contracts/Role.php @@ -10,6 +10,7 @@ * @property string|null $guard_name * * @mixin \Spatie\Permission\Models\Role + * * @phpstan-require-extends \Spatie\Permission\Models\Role */ interface Role From b34b5a3f7a5ebe2ce13de7fc84dd68a82d9b8a04 Mon Sep 17 00:00:00 2001 From: drbyte Date: Fri, 21 Jun 2024 23:52:01 +0000 Subject: [PATCH 0873/1013] Update CHANGELOG --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 319fe62d8..d08f1aa85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.8.0 - 2024-06-21 + +### What's Changed + +* Fix can't save the same model twice by @erikn69 in https://github.com/spatie/laravel-permission/pull/2658 +* Fix phpstan from #2616 by @erikn69 in https://github.com/spatie/laravel-permission/pull/2685 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.7.0...6.8.0 + ## 6.7.0 - 2024-04-19 ### What's Changed @@ -816,6 +825,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` 1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets. @@ -883,6 +893,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` ## 2.19.1 - 2018-09-14 From ee0817f0fdf8c7ad967e3725fe77a7e215e7babb Mon Sep 17 00:00:00 2001 From: Julian Gums Date: Sat, 22 Jun 2024 22:44:54 +0200 Subject: [PATCH 0874/1013] Use ->withPivot() for teamed relationships (#2679) * use ->withPivot for teamed relationships * add ->withPivot() method to permissions --- src/Traits/HasPermissions.php | 5 ++++- src/Traits/HasRoles.php | 8 +++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 4e38f8b0c..2514458d8 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -89,7 +89,10 @@ public function permissions(): BelongsToMany return $relation; } - return $relation->wherePivot(app(PermissionRegistrar::class)->teamsKey, getPermissionsTeamId()); + $teamsKey = app(PermissionRegistrar::class)->teamsKey; + $relation->withPivot($teamsKey); + + return $relation->wherePivot($teamsKey, getPermissionsTeamId()); } /** diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index fa537725f..3e63a9a2b 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -58,10 +58,12 @@ public function roles(): BelongsToMany if (! app(PermissionRegistrar::class)->teams) { return $relation; } + + $teamsKey = app(PermissionRegistrar::class)->teamsKey; + $relation->withPivot($teamsKey); + $teamField = config('permission.table_names.roles').'.'.$teamsKey; - $teamField = config('permission.table_names.roles').'.'.app(PermissionRegistrar::class)->teamsKey; - - return $relation->wherePivot(app(PermissionRegistrar::class)->teamsKey, getPermissionsTeamId()) + return $relation->wherePivot($teamsKey, getPermissionsTeamId()) ->where(fn ($q) => $q->whereNull($teamField)->orWhere($teamField, getPermissionsTeamId())); } From 5232e754734e0e8829be01b0303f0c73029ac8a9 Mon Sep 17 00:00:00 2001 From: drbyte Date: Sat, 22 Jun 2024 20:45:16 +0000 Subject: [PATCH 0875/1013] Fix styling --- src/Traits/HasRoles.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 3e63a9a2b..782ed20a1 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -58,7 +58,7 @@ public function roles(): BelongsToMany if (! app(PermissionRegistrar::class)->teams) { return $relation; } - + $teamsKey = app(PermissionRegistrar::class)->teamsKey; $relation->withPivot($teamsKey); $teamField = config('permission.table_names.roles').'.'.$teamsKey; From 854f87c0a751d5744a140f8a9162a6a6856b89a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Sz=C3=A9pe?= Date: Sat, 22 Jun 2024 22:51:32 +0200 Subject: [PATCH 0876/1013] Fix typos in changelog (#2686) --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d08f1aa85..f294540a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ All notable changes to `laravel-permission` will be documented in this file * Roles: Support for casting role names to enums by @gajosadrian in https://github.com/spatie/laravel-permission/pull/2616 * Fix permission:show UUID error #2581 by @drbyte in https://github.com/spatie/laravel-permission/pull/2582 -* Cover WilcardPermission instance verification based on its own guard (Allow hasAllPermissions and hasAnyPermission to run on custom guard for WildcardPermission) by @AlexandreBellas in https://github.com/spatie/laravel-permission/pull/2608 +* Cover WildcardPermission instance verification based on its own guard (Allow hasAllPermissions and hasAnyPermission to run on custom guard for WildcardPermission) by @AlexandreBellas in https://github.com/spatie/laravel-permission/pull/2608 * Register Laravel "About" details by @drbyte in https://github.com/spatie/laravel-permission/pull/2584 ### New Contributors @@ -538,7 +538,7 @@ Just a maintenance release. ## 5.2.0 - 2021-10-28 -- [V5] Fix detaching on all teams intstead of only current #1888 by @erikn69 in https://github.com/spatie/laravel-permission/pull/1890 +- [V5] Fix detaching on all teams instead of only current #1888 by @erikn69 in https://github.com/spatie/laravel-permission/pull/1890 - [V5] Add uuid compatibility support on teams by @erikn69 in https://github.com/spatie/laravel-permission/pull/1857 - Adds setRoleClass method to PermissionRegistrar by @timschwartz in https://github.com/spatie/laravel-permission/pull/1867 - Load permissions for preventLazyLoading by @bahramsadin in https://github.com/spatie/laravel-permission/pull/1884 @@ -1205,7 +1205,7 @@ BEST NOT TO USE v2.7.7 if you've changed tablenames in the config file. ** this version does not work in Laravel 5.1, please upgrade to version 1.5.1 of this package -- allowed `givePermissonTo` to accept multiple permissions +- allowed `givePermissionTo` to accept multiple permissions - allowed `assignRole` to accept multiple roles - added `syncPermissions`-method - added `syncRoles`-method From 3ebab01df0e0c040f84838f47a73c17e00e836c7 Mon Sep 17 00:00:00 2001 From: Jeremy Angele <131715596+angelej@users.noreply.github.com> Date: Sat, 22 Jun 2024 23:17:29 +0200 Subject: [PATCH 0877/1013] Update multiple-guards.md (#2659) * Update multiple-guards.md It took me a while to figure out, that you have to add `protected array $guard_name = ['web', 'admin'];` in order to allow an user to use roles / permissions from different guards. Especially since it's not a requirement to specify the `$guard_name`. * Add array example of multiple guards on User model --------- Co-authored-by: Chris Brown --- docs/basic-usage/multiple-guards.md | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/docs/basic-usage/multiple-guards.md b/docs/basic-usage/multiple-guards.md index 9a1911906..d9360d6cf 100644 --- a/docs/basic-usage/multiple-guards.md +++ b/docs/basic-usage/multiple-guards.md @@ -3,21 +3,21 @@ title: Using multiple guards weight: 9 --- -When using the default Laravel auth configuration all of the core methods of this package will work out of the box, no extra configuration required. +When using the default Laravel auth configuration all of the core methods of this package will work out of the box, with no extra configuration required. -However, when using multiple guards they will act like namespaces for your permissions and roles. Meaning every guard has its own set of permissions and roles that can be assigned to their user model. +However, when using multiple guards they will act like namespaces for your permissions and roles: Every guard has its own set of permissions and roles that can be assigned to its user model. ## The Downside To Multiple Guards -Note that this package requires you to register a permission name for each guard you want to authenticate with. So, "edit-article" would have to be created multiple times for each guard your app uses. An exception will be thrown if you try to authenticate against a non-existing permission+guard combination. Same for roles. +Note that this package requires you to register a permission name (same for roles) for each guard you want to authenticate with. So, "edit-article" would have to be created multiple times for each guard your app uses. An exception will be thrown if you try to authenticate against a non-existing permission+guard combination. Same for roles. -> **Tip**: If your app uses only a single guard, but is not `web` (Laravel's default, which shows "first" in the auth config file) then change the order of your listed guards in your `config/auth.php` to list your primary guard as the default and as the first in the list of defined guards. While you're editing that file, best to remove any guards you don't use, too. +> **Tip**: If your app uses only a single guard, but it is not `web` (Laravel's default, which shows "first" in the auth config file) then change the order of your listed guards in your `config/auth.php` to list your primary guard as the default and as the first in the list of defined guards. While you're editing that file, it is best to remove any guards you don't use, too. > > OR you could use the suggestion below to force the use of a single guard: ## Forcing Use Of A Single Guard -If your app structure does NOT differentiate between guards when it comes to roles/permissions, (ie: if ALL your roles/permissions are the SAME for ALL guards), you can override the `getDefaultGuardName` function by adding it to your User model, and specifying your desired guard_name. Then you only need to create roles/permissions for that single guard_name, not duplicating them. The example here sets it to `web`, but use whatever your application's default is: +If your app structure does NOT differentiate between guards when it comes to roles/permissions, (ie: if ALL your roles/permissions are the SAME for ALL guards), you can override the `getDefaultGuardName` function by adding it to your User model, and specifying your desired `$guard_name`. Then you only need to create roles/permissions for that single `$guard_name`, not duplicating them. The example here sets it to `web`, but use whatever your application's default is: ```php protected function getDefaultGuardName(): string { return 'web'; } @@ -46,16 +46,25 @@ $user->hasPermissionTo('publish articles', 'admin'); ``` > **Note**: When determining whether a role/permission is valid on a given model, it checks against the first matching guard in this order (it does NOT check role/permission for EACH possibility, just the first match): -- first the guardName() method if it exists on the model; -- then the `$guard_name` property if it exists on the model; +- first the guardName() method if it exists on the model (may return a string or array); +- then the `$guard_name` property if it exists on the model (may return a string or array); - then the first-defined guard/provider combination in the `auth.guards` config array that matches the loaded model's guard; - then the `auth.defaults.guard` config (which is the user's guard if they are logged in, else the default in the file). ## Assigning permissions and roles to guard users -You can use the same core methods to assign permissions and roles to users; just make sure the `guard_name` on the permission or role matches the guard of the user, otherwise a `GuardDoesNotMatch` or `Role/PermissionDoesNotExist` exception will be thrown. +You can use the same core methods to assign permissions and roles to users; just make sure the `guard_name` on the permission or role matches the guard of the user, otherwise a `GuardDoesNotMatch` or `Role/PermissionDoesNotExist` exception will be thrown. +If your User is able to consume multiple roles or permissions from different guards; make sure the User class's `$guard_name` property or `guardName()` method returns all allowed guards as an array: + +```php + protected $guard_name = ['web', 'admin']; +```` +or +```php + public function guardName() { return ['web', 'admin']; } +```` ## Using blade directives with multiple guards From 59b966fbb201c8dfc97dda31547c90a175f75b02 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 22 Jun 2024 19:04:28 -0400 Subject: [PATCH 0878/1013] Update docblock on $role->hasPermissionTo() to include BackedEnum Co-authored-by: Sander Muller --- src/Contracts/Role.php | 2 +- src/Models/Role.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Contracts/Role.php b/src/Contracts/Role.php index 7545b7582..31a67a082 100644 --- a/src/Contracts/Role.php +++ b/src/Contracts/Role.php @@ -44,7 +44,7 @@ public static function findOrCreate(string $name, ?string $guardName): self; /** * Determine if the user may perform the given permission. * - * @param string|\Spatie\Permission\Contracts\Permission $permission + * @param string|int|\Spatie\Permission\Contracts\Permission|\BackedEnum $permission */ public function hasPermissionTo($permission, ?string $guardName): bool; } diff --git a/src/Models/Role.php b/src/Models/Role.php index 4b2e48ff4..951355a1f 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -172,7 +172,7 @@ protected static function findByParam(array $params = []): ?RoleContract /** * Determine if the role may perform the given permission. * - * @param string|int|Permission|\BackedEnum $permission + * @param string|int|\Spatie\Permission\Contracts\Permission|\BackedEnum $permission * * @throws PermissionDoesNotExist|GuardDoesNotMatch */ From fe973a58b44380d0e8620107259b7bda22f70408 Mon Sep 17 00:00:00 2001 From: drbyte Date: Sat, 22 Jun 2024 23:04:52 +0000 Subject: [PATCH 0879/1013] Fix styling --- src/Models/Role.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Models/Role.php b/src/Models/Role.php index 951355a1f..86d81d379 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -172,7 +172,7 @@ protected static function findByParam(array $params = []): ?RoleContract /** * Determine if the role may perform the given permission. * - * @param string|int|\Spatie\Permission\Contracts\Permission|\BackedEnum $permission + * @param string|int|\Spatie\Permission\Contracts\Permission|\BackedEnum $permission * * @throws PermissionDoesNotExist|GuardDoesNotMatch */ From 80a32a10f7439fe381a5684e78e72cadd8fbd170 Mon Sep 17 00:00:00 2001 From: drbyte Date: Sat, 22 Jun 2024 23:26:39 +0000 Subject: [PATCH 0880/1013] Update CHANGELOG --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f294540a2..c3c7c41a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.9.0 - 2024-06-22 + +### What's Changed + +* Use `->withPivot()` for teamed relationships (allows `getPivotColumns()`) by @juliangums in https://github.com/spatie/laravel-permission/pull/2679 +* Update docblock on `$role->hasPermissionTo()` to include `BackedEnum` by @drbyte co-authored by @SanderMuller +* [Docs] Clarify that `$guard_name` can be an array by @angelej in https://github.com/spatie/laravel-permission/pull/2659 +* Fix misc typos in changelog by @szepeviktor in https://github.com/spatie/laravel-permission/pull/2686 + +### New Contributors + +* @angelej made their first contribution in https://github.com/spatie/laravel-permission/pull/2659 +* @SanderMuller made their first contribution in #2676 +* @szepeviktor made their first contribution in https://github.com/spatie/laravel-permission/pull/2686 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.8.0...6.9.0 + ## 6.8.0 - 2024-06-21 ### What's Changed @@ -825,6 +842,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` @@ -893,6 +911,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` From 29e498310aa565ce6e70d02b9ac45c77b40aab5d Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 20 Jul 2024 22:32:20 -0400 Subject: [PATCH 0881/1013] L11 note --- docs/advanced-usage/custom-permission-check.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-usage/custom-permission-check.md b/docs/advanced-usage/custom-permission-check.md index 0f1c88728..956fda047 100644 --- a/docs/advanced-usage/custom-permission-check.md +++ b/docs/advanced-usage/custom-permission-check.md @@ -16,7 +16,7 @@ Let's say that your application uses access tokens for authentication and when i You could, for example, create a `Gate::before()` method call to handle this: -**app/Providers/AuthServiceProvider.php** +**app/Providers/AuthServiceProvider.php** (or maybe `AppServiceProvider.php` since Laravel 11) ```php use Illuminate\Support\Facades\Gate; From 7a0b503acbc7e92e82f920626aba8e68552fec07 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 20 Jul 2024 22:34:34 -0400 Subject: [PATCH 0882/1013] L11 --- docs/basic-usage/new-app.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index 8ec16914d..04b52f9f4 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -130,7 +130,7 @@ php artisan migrate:fresh --seed --seeder=PermissionsDemoSeeder Super-Admins are a common feature. The following approach allows that when your Super-Admin user is logged in, all permission-checks in your app which call `can()` or `@can()` will return true. - Create a role named `Super-Admin`. (Or whatever name you wish; but use it consistently just like you must with any role name.) -- Add a Gate::before check in your `AuthServiceProvider`: +- Add a Gate::before check in your `AuthServiceProvider` (or `AppServiceProvider` since Laravel 11): ```diff + use Illuminate\Support\Facades\Gate; From f81fb020e0045735ca2ab56b91cc91fdd0de726c Mon Sep 17 00:00:00 2001 From: Mike Scott Date: Sat, 27 Jul 2024 00:20:09 +0100 Subject: [PATCH 0883/1013] Check for 'all' or 'any' permissions before specific permissions (#2694) Shouldn't the check for `edit all posts` or `delete any post` be done first, before checking if a user can edit or delete their own posts? The original code checked if the user can edit their own posts and, if so, would return false if they were not the post auther, **even though they had the permission to edit any post**. By performing the `all`/`any` check first, these permissions still work correctly when the user also has permissions to edit or delete their own posts. --- docs/best-practices/using-policies.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/best-practices/using-policies.md b/docs/best-practices/using-policies.md index 504042e09..5afc36026 100644 --- a/docs/best-practices/using-policies.md +++ b/docs/best-practices/using-policies.md @@ -49,24 +49,24 @@ class PostPolicy public function update(User $user, Post $post) { - if ($user->can('edit own posts')) { - return $user->id == $post->user_id; - } - if ($user->can('edit all posts')) { return true; } - } - public function delete(User $user, Post $post) - { - if ($user->can('delete own posts')) { + if ($user->can('edit own posts')) { return $user->id == $post->user_id; } + } + public function delete(User $user, Post $post) + { if ($user->can('delete any post')) { return true; } + + if ($user->can('delete own posts')) { + return $user->id == $post->user_id; + } } } ``` From 231530a5e07b1bb81f046331e4d077f0f002448b Mon Sep 17 00:00:00 2001 From: Levi Zoesch Sr Date: Wed, 14 Aug 2024 23:03:24 -0700 Subject: [PATCH 0884/1013] Update uuid.md (#2705) --- docs/advanced-usage/uuid.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index d2740f291..18fdd82f4 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -83,7 +83,7 @@ If you also want the roles and permissions to use a UUID for their `id` value, t ## Configuration (OPTIONAL) You might want to change the pivot table field name from `model_id` to `model_uuid`, just for semantic purposes. -For this, in the `permissions.php` configuration file edit `column_names.model_morph_key`: +For this, in the `permission.php` configuration file edit `column_names.model_morph_key`: - OPTIONAL: Change to `model_uuid` instead of the default `model_id`. ```diff From 43550a16901a2e35df7703eee5d36a7c8ea81228 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 26 Aug 2024 19:27:42 -0400 Subject: [PATCH 0885/1013] Change example user details to avoid clash with defaults --- docs/basic-usage/new-app.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index 04b52f9f4..23903c3ab 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -100,7 +100,7 @@ class PermissionsDemoSeeder extends Seeder // create demo users $user = \App\Models\User::factory()->create([ 'name' => 'Example User', - 'email' => 'test@example.com', + 'email' => 'tester@example.com', ]); $user->assignRole($role1); From 631799b6393206714e49cbca58e7d02b76ae7b29 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 26 Aug 2024 20:02:27 -0400 Subject: [PATCH 0886/1013] Updates regarding WithoutModelEvents --- docs/advanced-usage/seeding.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/advanced-usage/seeding.md b/docs/advanced-usage/seeding.md index d4e58d810..b40b10050 100644 --- a/docs/advanced-usage/seeding.md +++ b/docs/advanced-usage/seeding.md @@ -7,7 +7,7 @@ weight: 2 You may discover that it is best to flush this package's cache **BEFORE seeding, to avoid cache conflict errors**. -And if you use the `WithoutModelEvents` trait in your seeders, flush it **AFTER seeding as well**. +And if you use the `WithoutModelEvents` trait in your seeders, flush it **AFTER creating any roles/permissions as well, before assigning or granting them.**. ```php // reset cached roles and permissions @@ -40,6 +40,10 @@ class RolesAndPermissionsSeeder extends Seeder Permission::create(['name' => 'publish articles']); Permission::create(['name' => 'unpublish articles']); + // update cache to know about the newly created permissions (required if using WithoutModelEvents in seeders) + app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions(); + + // create roles and assign created permissions // this can be done as separate statements From fe70ca8e2b30ba00aa44eaee18fdb0cfec0a67dc Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 30 Aug 2024 04:03:15 -0400 Subject: [PATCH 0887/1013] Clarify that "super" access requires using Laravel Gate methods --- docs/basic-usage/super-admin.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/basic-usage/super-admin.md b/docs/basic-usage/super-admin.md index 4eefee99d..e4e2585b2 100644 --- a/docs/basic-usage/super-admin.md +++ b/docs/basic-usage/super-admin.md @@ -7,6 +7,8 @@ We strongly recommend that a Super-Admin be handled by setting a global `Gate::b Then you can implement the best-practice of primarily using permission-based controls (@can and $user->can, etc) throughout your app, without always having to check for "is this a super-admin" everywhere. **Best not to use role-checking (ie: `hasRole`) (except here in Gate/Policy rules) when you have Super Admin features like this.** +NOTE: Using this approach, you can/must call Laravel's standard `can()`, `canAny()`, `cannot()`, etc checks for permission authorization to get a correct Super response. Calls which bypass Laravel's Gate (such as a direct call to `->hasPermissionTo()`) will not go through the Gate, and will not get the Super response. + ## `Gate::before` If you want a "Super Admin" role to respond `true` to all permissions, without needing to assign all those permissions to a role, you can use [Laravel's `Gate::before()` method](https://laravel.com/docs/master/authorization#intercepting-gate-checks). For example: From 4e5c1b3c11cf90a3209304cf8670971c5e2fbcae Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 30 Aug 2024 16:25:52 -0400 Subject: [PATCH 0888/1013] Note about `database` cache store dependencies --- docs/advanced-usage/seeding.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/advanced-usage/seeding.md b/docs/advanced-usage/seeding.md index b40b10050..30dc21bf2 100644 --- a/docs/advanced-usage/seeding.md +++ b/docs/advanced-usage/seeding.md @@ -18,6 +18,10 @@ You can optionally flush the cache before seeding by using the `SetUp()` method Or it can be done directly in a seeder class, as shown below. +## Database Cache Store + +TIP: If you have `CACHE_STORE=database` set in your `.env`, remember that [you must install Laravel's cache tables via a migration before performing any cache operations](https://laravel.com/docs/cache#prerequisites-database). If you fail to install those migrations, you'll run into errors like `Call to a member function perform() on null` when the cache store attempts to purge or update the cache. This package does strategic cache resets in various places, so may trigger that error if your app's cache dependencies aren't set up. + ## Roles/Permissions Seeder Here is a sample seeder, which first clears the cache, creates permissions and then assigns permissions to roles (the order of these steps is intentional): From 3d2a3d2e9c764293d781dad60b539df937284851 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 30 Aug 2024 16:28:42 -0400 Subject: [PATCH 0889/1013] Database cache dependencies --- docs/advanced-usage/cache.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/advanced-usage/cache.md b/docs/advanced-usage/cache.md index 588f77f42..6c3a3fe6a 100644 --- a/docs/advanced-usage/cache.md +++ b/docs/advanced-usage/cache.md @@ -48,6 +48,8 @@ php artisan permission:cache-reset ## Cache Configuration Settings +This package allows you to customize cache-related operations via its config file. In most cases the defaults are fine; however, in a multitenancy situation you may wish to do some cache-prefix overrides when switching tenants. See below for more details. + ### Cache Expiration Time The default cache `expiration_time` is `24 hours`. @@ -82,10 +84,15 @@ Setting `'cache.store' => 'array'` in `config/permission.php` will effectively d Alternatively, in development mode you can bypass ALL of Laravel's caching between visits by setting `CACHE_DRIVER=array` in `.env`. You can see an example of this in the default `phpunit.xml` file that comes with a new Laravel install. Of course, don't do this in production though! -## File cache driver +## File cache Store This situation is not specific to this package, but is mentioned here due to the common question being asked. -If you are using the `File` cache driver and run into problems clearing the cache, it is most likely because your filesystem's permissions are preventing the PHP CLI from altering the cache files because the PHP-FPM process is running as a different user. +If you are using the `File` cache Store and run into problems clearing the cache, it is most likely because your filesystem's permissions are preventing the PHP CLI from altering the cache files because the PHP-FPM process is running as a different user. Work with your server administrator to fix filesystem ownership on your cache files. + +## Database cache Store + +TIP: If you have `CACHE_STORE=database` set in your `.env`, remember that [you must install Laravel's cache tables via a migration before performing any cache operations](https://laravel.com/docs/cache#prerequisites-database). If you fail to install those migrations, you'll run into errors like `Call to a member function perform() on null` when the cache store attempts to purge or update the cache. This package does strategic cache resets in various places, so may trigger that error if your app's cache dependencies aren't set up. + From 0068a219970da2bf778bb14d8612d62fd68dd010 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 30 Aug 2024 16:31:12 -0400 Subject: [PATCH 0890/1013] Database cache reminder --- docs/installation-laravel.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index fd0cfb59f..f7cb644f9 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -53,6 +53,8 @@ Package Version | Laravel Version - **If you are using MySQL 8**, look at the migration files for notes about MySQL 8 to set/limit the index key length, and edit accordingly. If you get `ERROR: 1071 Specified key was too long` then you need to do this. + - **If you are using CACHE_STORE=database**, be sure to [install Laravel's cache migration](https://laravel.com/docs/cache#prerequisites-database), else you will encounter cache errors. + 7. **Clear your config cache**. This package requires access to the `permission` config settings in order to run migrations. If you've been caching configurations locally, clear your config cache with either of these commands: php artisan optimize:clear From 36bb36777fb8ad5928b806cd63178aff06efeaa0 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 30 Aug 2024 17:03:50 -0400 Subject: [PATCH 0891/1013] Gate notes --- docs/basic-usage/direct-permissions.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/basic-usage/direct-permissions.md b/docs/basic-usage/direct-permissions.md index d8533b9b9..944392b0f 100644 --- a/docs/basic-usage/direct-permissions.md +++ b/docs/basic-usage/direct-permissions.md @@ -13,6 +13,8 @@ HOWEVER, If you have reason to directly assign individual permissions to specifi ## Direct Permissions to Users +### Giving/Revoking direct permissions + A permission can be given to any user: ```php @@ -37,6 +39,15 @@ Or revoke & add new permissions in one go: $user->syncPermissions(['edit articles', 'delete articles']); ``` +## Checking Direct Permissions +Like all permissions assigned via roles, you can check if a user has a permission by using Laravel's default `can` function. This will also allow you to use Super-Admin features provided by Laravel's Gate: + +```php +$user->can('edit articles'); +``` + +NOTE: The following `hasPermissionTo`, `hasAnyPermission`, `hasAllPermissions` functions do not support Super-Admin functionality. Use `can`, `canAny`, `canAll` instead. + You can check if a user has a permission: ```php @@ -68,9 +79,3 @@ You may also pass integers to lookup by permission id ```php $user->hasAnyPermission(['edit articles', 1, 5]); ``` - -Like all permissions assigned via roles, you can check if a user has a permission by using Laravel's default `can` function: - -```php -$user->can('edit articles'); -``` From 514cb7ead7f8a7db3804bb017d51e4a0edf8aff4 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 30 Aug 2024 17:13:35 -0400 Subject: [PATCH 0892/1013] Clarify internal has methods vs Gate can methods --- docs/basic-usage/super-admin.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/basic-usage/super-admin.md b/docs/basic-usage/super-admin.md index e4e2585b2..0603db2bd 100644 --- a/docs/basic-usage/super-admin.md +++ b/docs/basic-usage/super-admin.md @@ -7,7 +7,14 @@ We strongly recommend that a Super-Admin be handled by setting a global `Gate::b Then you can implement the best-practice of primarily using permission-based controls (@can and $user->can, etc) throughout your app, without always having to check for "is this a super-admin" everywhere. **Best not to use role-checking (ie: `hasRole`) (except here in Gate/Policy rules) when you have Super Admin features like this.** -NOTE: Using this approach, you can/must call Laravel's standard `can()`, `canAny()`, `cannot()`, etc checks for permission authorization to get a correct Super response. Calls which bypass Laravel's Gate (such as a direct call to `->hasPermissionTo()`) will not go through the Gate, and will not get the Super response. +## Gate::before/Policy::before vs HasPermissionTo / HasAnyPermission / HasDirectPermission / HasAllPermissions +IMPORTANT: +The Gate::before is the best approach for Super-Admin functionality, and aligns well with the described "Best Practices" of using roles as a way of grouping permissions, and assigning that access to Users. Using this approach, you can/must call Laravel's standard `can()`, `canAny()`, `cannot()`, etc checks for permission authorization to get a correct Super response. + +### HasPermissionTo, HasAllPermissions, HasAnyPermission, HasDirectPermission +Calls to this package's internal API which bypass Laravel's Gate (such as a direct call to `->hasPermissionTo()`) will not go through the Gate, and thus will not get the Super response, unless you have actually added that specific permission to the Super-Admin "role". + +The only reason for giving specific permissions to a Super-Admin role is if you intend to call the `has` methods directly instead of the Gate's `can()` methods. ## `Gate::before` From b881c1a64e6017dbdb354495d7c774969cccd5c0 Mon Sep 17 00:00:00 2001 From: mraheelkhan Date: Wed, 4 Sep 2024 09:24:29 +0000 Subject: [PATCH 0893/1013] Fix styling --- src/Models/Permission.php | 2 +- src/Models/Role.php | 2 +- src/Traits/HasPermissions.php | 2 +- tests/HasPermissionsTest.php | 6 +++--- tests/PermissionMiddlewareTest.php | 14 +++++++------- tests/RoleMiddlewareTest.php | 14 +++++++------- tests/RoleOrPermissionMiddlewareTest.php | 14 +++++++------- tests/TestCase.php | 4 ++-- tests/TestHelper.php | 4 ++-- tests/WildcardMiddlewareTest.php | 10 +++++----- 10 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index d23d15ac2..fd54b8ca0 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -107,7 +107,7 @@ public static function findByName(string $name, ?string $guardName = null): Perm public static function findById(int|string $id, ?string $guardName = null): PermissionContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); - $permission = static::getPermission([(new static())->getKeyName() => $id, 'guard_name' => $guardName]); + $permission = static::getPermission([(new static)->getKeyName() => $id, 'guard_name' => $guardName]); if (! $permission) { throw PermissionDoesNotExist::withId($id, $guardName); diff --git a/src/Models/Role.php b/src/Models/Role.php index 86d81d379..e29e2d449 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -117,7 +117,7 @@ public static function findById(int|string $id, ?string $guardName = null): Role { $guardName = $guardName ?? Guard::getDefaultName(static::class); - $role = static::findByParam([(new static())->getKeyName() => $id, 'guard_name' => $guardName]); + $role = static::findByParam([(new static)->getKeyName() => $id, 'guard_name' => $guardName]); if (! $role) { throw RoleDoesNotExist::withId($id, $guardName); diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 2514458d8..25d506088 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -190,7 +190,7 @@ public function filterPermission($permission, $guardName = null) } if (! $permission instanceof Permission) { - throw new PermissionDoesNotExist(); + throw new PermissionDoesNotExist; } return $permission; diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index 4c9528d0e..ce197b16c 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -263,7 +263,7 @@ public function it_throws_an_exception_when_calling_hasPermissionTo_with_an_inva $this->expectException(PermissionDoesNotExist::class); - $user->hasPermissionTo(new \stdClass()); + $user->hasPermissionTo(new \stdClass); } /** @test */ @@ -283,7 +283,7 @@ public function it_throws_an_exception_when_calling_hasDirectPermission_with_an_ $this->expectException(PermissionDoesNotExist::class); - $user->hasDirectPermission(new \stdClass()); + $user->hasDirectPermission(new \stdClass); } /** @test */ @@ -405,7 +405,7 @@ public function it_throws_an_exception_when_the_permission_does_not_exist_for_th /** @test */ public function it_can_reject_a_user_that_does_not_have_any_permissions_at_all() { - $user = new User(); + $user = new User; $this->assertFalse($user->hasPermissionTo('edit-articles')); } diff --git a/tests/PermissionMiddlewareTest.php b/tests/PermissionMiddlewareTest.php index aefc7849e..354f1f8b1 100644 --- a/tests/PermissionMiddlewareTest.php +++ b/tests/PermissionMiddlewareTest.php @@ -24,7 +24,7 @@ protected function setUp(): void { parent::setUp(); - $this->permissionMiddleware = new PermissionMiddleware(); + $this->permissionMiddleware = new PermissionMiddleware; } /** @test */ @@ -305,8 +305,8 @@ public function the_required_permissions_can_be_fetched_from_the_exception() $requiredPermissions = []; try { - $this->permissionMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); + $this->permissionMiddleware->handle(new Request, function () { + return (new Response)->setContent(''); }, 'some-permission'); } catch (UnauthorizedException $e) { $message = $e->getMessage(); @@ -326,8 +326,8 @@ public function the_required_permissions_can_be_displayed_in_the_exception() $message = null; try { - $this->permissionMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); + $this->permissionMiddleware->handle(new Request, function () { + return (new Response)->setContent(''); }, 'some-permission'); } catch (UnauthorizedException $e) { $message = $e->getMessage(); @@ -342,8 +342,8 @@ public function use_not_existing_custom_guard_in_permission() $class = null; try { - $this->permissionMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); + $this->permissionMiddleware->handle(new Request, function () { + return (new Response)->setContent(''); }, 'edit-articles', 'xxx'); } catch (InvalidArgumentException $e) { $class = get_class($e); diff --git a/tests/RoleMiddlewareTest.php b/tests/RoleMiddlewareTest.php index 7ffc3a5c6..780277068 100644 --- a/tests/RoleMiddlewareTest.php +++ b/tests/RoleMiddlewareTest.php @@ -22,7 +22,7 @@ protected function setUp(): void { parent::setUp(); - $this->roleMiddleware = new RoleMiddleware(); + $this->roleMiddleware = new RoleMiddleware; } /** @test */ @@ -238,8 +238,8 @@ public function the_required_roles_can_be_fetched_from_the_exception() $requiredRoles = []; try { - $this->roleMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); + $this->roleMiddleware->handle(new Request, function () { + return (new Response)->setContent(''); }, 'some-role'); } catch (UnauthorizedException $e) { $message = $e->getMessage(); @@ -259,8 +259,8 @@ public function the_required_roles_can_be_displayed_in_the_exception() $message = null; try { - $this->roleMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); + $this->roleMiddleware->handle(new Request, function () { + return (new Response)->setContent(''); }, 'some-role'); } catch (UnauthorizedException $e) { $message = $e->getMessage(); @@ -275,8 +275,8 @@ public function use_not_existing_custom_guard_in_role() $class = null; try { - $this->roleMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); + $this->roleMiddleware->handle(new Request, function () { + return (new Response)->setContent(''); }, 'testRole', 'xxx'); } catch (InvalidArgumentException $e) { $class = get_class($e); diff --git a/tests/RoleOrPermissionMiddlewareTest.php b/tests/RoleOrPermissionMiddlewareTest.php index 4d473aa4f..617c4c5af 100644 --- a/tests/RoleOrPermissionMiddlewareTest.php +++ b/tests/RoleOrPermissionMiddlewareTest.php @@ -23,7 +23,7 @@ protected function setUp(): void { parent::setUp(); - $this->roleOrPermissionMiddleware = new RoleOrPermissionMiddleware(); + $this->roleOrPermissionMiddleware = new RoleOrPermissionMiddleware; } /** @test */ @@ -177,8 +177,8 @@ public function use_not_existing_custom_guard_in_role_or_permission() $class = null; try { - $this->roleOrPermissionMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); + $this->roleOrPermissionMiddleware->handle(new Request, function () { + return (new Response)->setContent(''); }, 'testRole', 'xxx'); } catch (InvalidArgumentException $e) { $class = get_class($e); @@ -242,8 +242,8 @@ public function the_required_permissions_or_roles_can_be_fetched_from_the_except $requiredRolesOrPermissions = []; try { - $this->roleOrPermissionMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); + $this->roleOrPermissionMiddleware->handle(new Request, function () { + return (new Response)->setContent(''); }, 'some-permission|some-role'); } catch (UnauthorizedException $e) { $message = $e->getMessage(); @@ -264,8 +264,8 @@ public function the_required_permissions_or_roles_can_be_displayed_in_the_except $message = null; try { - $this->roleOrPermissionMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); + $this->roleOrPermissionMiddleware->handle(new Request, function () { + return (new Response)->setContent(''); }, 'some-permission|some-role'); } catch (UnauthorizedException $e) { $message = $e->getMessage(); diff --git a/tests/TestCase.php b/tests/TestCase.php index 3a5532c8b..9e5e36c34 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -292,7 +292,7 @@ public function runMiddleware($middleware, $permission, $guard = null, bool $cli try { return $middleware->handle($request, function () { - return (new Response())->setContent(''); + return (new Response)->setContent(''); }, $permission, $guard)->status(); } catch (UnauthorizedException $e) { return $e->getStatusCode(); @@ -312,7 +312,7 @@ public function getRouter() public function getRouteResponse() { return function () { - return (new Response())->setContent(''); + return (new Response)->setContent(''); }; } diff --git a/tests/TestHelper.php b/tests/TestHelper.php index 5b8efa5cd..5eddab214 100644 --- a/tests/TestHelper.php +++ b/tests/TestHelper.php @@ -16,8 +16,8 @@ class TestHelper public function testMiddleware($middleware, $parameter) { try { - return $middleware->handle(new Request(), function () { - return (new Response())->setContent(''); + return $middleware->handle(new Request, function () { + return (new Response)->setContent(''); }, $parameter)->status(); } catch (HttpException $e) { return $e->getStatusCode(); diff --git a/tests/WildcardMiddlewareTest.php b/tests/WildcardMiddlewareTest.php index dc359fe49..596913707 100644 --- a/tests/WildcardMiddlewareTest.php +++ b/tests/WildcardMiddlewareTest.php @@ -23,11 +23,11 @@ protected function setUp(): void { parent::setUp(); - $this->roleMiddleware = new RoleMiddleware(); + $this->roleMiddleware = new RoleMiddleware; - $this->permissionMiddleware = new PermissionMiddleware(); + $this->permissionMiddleware = new PermissionMiddleware; - $this->roleOrPermissionMiddleware = new RoleOrPermissionMiddleware(); + $this->roleOrPermissionMiddleware = new RoleOrPermissionMiddleware; app('config')->set('permission.enable_wildcard_permission', true); } @@ -146,8 +146,8 @@ public function the_required_permissions_can_be_fetched_from_the_exception() $requiredPermissions = []; try { - $this->permissionMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); + $this->permissionMiddleware->handle(new Request, function () { + return (new Response)->setContent(''); }, 'permission.some'); } catch (UnauthorizedException $e) { $requiredPermissions = $e->getRequiredPermissions(); From df0ebb997d77ae01df0bf4a52073b61b17859606 Mon Sep 17 00:00:00 2001 From: Raheel Khan <30007262+mraheelkhan@users.noreply.github.com> Date: Wed, 4 Sep 2024 14:27:19 +0500 Subject: [PATCH 0894/1013] Add PR links to upgrade guide --- docs/upgrading.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index da3d4423d..2d56f9b18 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -39,7 +39,7 @@ eg: if you have a custom model you will need to make changes, including accessin Be sure to compare your custom models with originals to see what else may have changed. -3. Model and Contract/Interface updates. The Role and Permission Models and Contracts/Interfaces have been updated with syntax changes to method signatures. Update any models you have extended, or contracts implemented, accordingly. See PR #2380 and #2480 for some of the specifics. +3. Model and Contract/Interface updates. The Role and Permission Models and Contracts/Interfaces have been updated with syntax changes to method signatures. Update any models you have extended, or contracts implemented, accordingly. See PR [#2380](https://github.com/spatie/laravel-permission/pull/2380) and [#2480](https://github.com/spatie/laravel-permission/pull/2480) for some of the specifics. 4. Migrations WILL need to be upgraded. (They have been updated to anonymous-class syntax that was introduced in Laravel 8, AND some structural coding changes in the registrar class changed the way we extracted configuration settings in the migration files.) There are no changes to the package's structure since v5, so if you had not customized it from the original then replacing the contents of the file should be enough. (Usually the only customization is if you've switched to UUIDs or customized MySQL index name lengths.) **If you get the following error, it means your migration file needs upgrading: `Error: Access to undeclared static property Spatie\Permission\PermissionRegistrar::$pivotPermission`** From 28577dc646784600d822eb95f9bef5b8fa7632a9 Mon Sep 17 00:00:00 2001 From: Galang Aidil Akbar Date: Sat, 14 Sep 2024 16:34:50 +0700 Subject: [PATCH 0895/1013] Make return type nullable --- docs/basic-usage/super-admin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/super-admin.md b/docs/basic-usage/super-admin.md index 0603db2bd..00a2af27f 100644 --- a/docs/basic-usage/super-admin.md +++ b/docs/basic-usage/super-admin.md @@ -51,7 +51,7 @@ use App\Models\User; // could be any Authorizable model /** * Perform pre-authorization checks on the model. */ -public function before(User $user, string $ability): bool|null +public function before(User $user, string $ability): ?bool { if ($user->hasRole('Super Admin')) { return true; From 01d2e33aaaeafd208ab2be664cf989dcbfb7f84c Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 16 Sep 2024 11:45:58 -0400 Subject: [PATCH 0896/1013] Rename PermissionRegistarTest.php to PermissionRegistrarTest.php --- .../{PermissionRegistarTest.php => PermissionRegistrarTest.php} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/{PermissionRegistarTest.php => PermissionRegistrarTest.php} (99%) diff --git a/tests/PermissionRegistarTest.php b/tests/PermissionRegistrarTest.php similarity index 99% rename from tests/PermissionRegistarTest.php rename to tests/PermissionRegistrarTest.php index d5ed03547..115926d4c 100644 --- a/tests/PermissionRegistarTest.php +++ b/tests/PermissionRegistrarTest.php @@ -10,7 +10,7 @@ use Spatie\Permission\Tests\TestModels\Permission as TestPermission; use Spatie\Permission\Tests\TestModels\Role as TestRole; -class PermissionRegistarTest extends TestCase +class PermissionRegistrarTest extends TestCase { /** @test */ public function it_can_clear_loaded_permissions_collection() From 44083f48b4cb7c1c1ffe32b9e339141261ef0cfb Mon Sep 17 00:00:00 2001 From: drbyte Date: Mon, 16 Sep 2024 15:46:24 +0000 Subject: [PATCH 0897/1013] Fix styling --- src/Models/Permission.php | 2 +- src/Models/Role.php | 2 +- src/Traits/HasPermissions.php | 2 +- tests/HasPermissionsTest.php | 6 +++--- tests/PermissionMiddlewareTest.php | 14 +++++++------- tests/RoleMiddlewareTest.php | 14 +++++++------- tests/RoleOrPermissionMiddlewareTest.php | 14 +++++++------- tests/TestCase.php | 4 ++-- tests/TestHelper.php | 4 ++-- tests/WildcardMiddlewareTest.php | 10 +++++----- 10 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index d23d15ac2..fd54b8ca0 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -107,7 +107,7 @@ public static function findByName(string $name, ?string $guardName = null): Perm public static function findById(int|string $id, ?string $guardName = null): PermissionContract { $guardName = $guardName ?? Guard::getDefaultName(static::class); - $permission = static::getPermission([(new static())->getKeyName() => $id, 'guard_name' => $guardName]); + $permission = static::getPermission([(new static)->getKeyName() => $id, 'guard_name' => $guardName]); if (! $permission) { throw PermissionDoesNotExist::withId($id, $guardName); diff --git a/src/Models/Role.php b/src/Models/Role.php index 86d81d379..e29e2d449 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -117,7 +117,7 @@ public static function findById(int|string $id, ?string $guardName = null): Role { $guardName = $guardName ?? Guard::getDefaultName(static::class); - $role = static::findByParam([(new static())->getKeyName() => $id, 'guard_name' => $guardName]); + $role = static::findByParam([(new static)->getKeyName() => $id, 'guard_name' => $guardName]); if (! $role) { throw RoleDoesNotExist::withId($id, $guardName); diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 2514458d8..25d506088 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -190,7 +190,7 @@ public function filterPermission($permission, $guardName = null) } if (! $permission instanceof Permission) { - throw new PermissionDoesNotExist(); + throw new PermissionDoesNotExist; } return $permission; diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index 4c9528d0e..ce197b16c 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -263,7 +263,7 @@ public function it_throws_an_exception_when_calling_hasPermissionTo_with_an_inva $this->expectException(PermissionDoesNotExist::class); - $user->hasPermissionTo(new \stdClass()); + $user->hasPermissionTo(new \stdClass); } /** @test */ @@ -283,7 +283,7 @@ public function it_throws_an_exception_when_calling_hasDirectPermission_with_an_ $this->expectException(PermissionDoesNotExist::class); - $user->hasDirectPermission(new \stdClass()); + $user->hasDirectPermission(new \stdClass); } /** @test */ @@ -405,7 +405,7 @@ public function it_throws_an_exception_when_the_permission_does_not_exist_for_th /** @test */ public function it_can_reject_a_user_that_does_not_have_any_permissions_at_all() { - $user = new User(); + $user = new User; $this->assertFalse($user->hasPermissionTo('edit-articles')); } diff --git a/tests/PermissionMiddlewareTest.php b/tests/PermissionMiddlewareTest.php index aefc7849e..354f1f8b1 100644 --- a/tests/PermissionMiddlewareTest.php +++ b/tests/PermissionMiddlewareTest.php @@ -24,7 +24,7 @@ protected function setUp(): void { parent::setUp(); - $this->permissionMiddleware = new PermissionMiddleware(); + $this->permissionMiddleware = new PermissionMiddleware; } /** @test */ @@ -305,8 +305,8 @@ public function the_required_permissions_can_be_fetched_from_the_exception() $requiredPermissions = []; try { - $this->permissionMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); + $this->permissionMiddleware->handle(new Request, function () { + return (new Response)->setContent(''); }, 'some-permission'); } catch (UnauthorizedException $e) { $message = $e->getMessage(); @@ -326,8 +326,8 @@ public function the_required_permissions_can_be_displayed_in_the_exception() $message = null; try { - $this->permissionMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); + $this->permissionMiddleware->handle(new Request, function () { + return (new Response)->setContent(''); }, 'some-permission'); } catch (UnauthorizedException $e) { $message = $e->getMessage(); @@ -342,8 +342,8 @@ public function use_not_existing_custom_guard_in_permission() $class = null; try { - $this->permissionMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); + $this->permissionMiddleware->handle(new Request, function () { + return (new Response)->setContent(''); }, 'edit-articles', 'xxx'); } catch (InvalidArgumentException $e) { $class = get_class($e); diff --git a/tests/RoleMiddlewareTest.php b/tests/RoleMiddlewareTest.php index 7ffc3a5c6..780277068 100644 --- a/tests/RoleMiddlewareTest.php +++ b/tests/RoleMiddlewareTest.php @@ -22,7 +22,7 @@ protected function setUp(): void { parent::setUp(); - $this->roleMiddleware = new RoleMiddleware(); + $this->roleMiddleware = new RoleMiddleware; } /** @test */ @@ -238,8 +238,8 @@ public function the_required_roles_can_be_fetched_from_the_exception() $requiredRoles = []; try { - $this->roleMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); + $this->roleMiddleware->handle(new Request, function () { + return (new Response)->setContent(''); }, 'some-role'); } catch (UnauthorizedException $e) { $message = $e->getMessage(); @@ -259,8 +259,8 @@ public function the_required_roles_can_be_displayed_in_the_exception() $message = null; try { - $this->roleMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); + $this->roleMiddleware->handle(new Request, function () { + return (new Response)->setContent(''); }, 'some-role'); } catch (UnauthorizedException $e) { $message = $e->getMessage(); @@ -275,8 +275,8 @@ public function use_not_existing_custom_guard_in_role() $class = null; try { - $this->roleMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); + $this->roleMiddleware->handle(new Request, function () { + return (new Response)->setContent(''); }, 'testRole', 'xxx'); } catch (InvalidArgumentException $e) { $class = get_class($e); diff --git a/tests/RoleOrPermissionMiddlewareTest.php b/tests/RoleOrPermissionMiddlewareTest.php index 4d473aa4f..617c4c5af 100644 --- a/tests/RoleOrPermissionMiddlewareTest.php +++ b/tests/RoleOrPermissionMiddlewareTest.php @@ -23,7 +23,7 @@ protected function setUp(): void { parent::setUp(); - $this->roleOrPermissionMiddleware = new RoleOrPermissionMiddleware(); + $this->roleOrPermissionMiddleware = new RoleOrPermissionMiddleware; } /** @test */ @@ -177,8 +177,8 @@ public function use_not_existing_custom_guard_in_role_or_permission() $class = null; try { - $this->roleOrPermissionMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); + $this->roleOrPermissionMiddleware->handle(new Request, function () { + return (new Response)->setContent(''); }, 'testRole', 'xxx'); } catch (InvalidArgumentException $e) { $class = get_class($e); @@ -242,8 +242,8 @@ public function the_required_permissions_or_roles_can_be_fetched_from_the_except $requiredRolesOrPermissions = []; try { - $this->roleOrPermissionMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); + $this->roleOrPermissionMiddleware->handle(new Request, function () { + return (new Response)->setContent(''); }, 'some-permission|some-role'); } catch (UnauthorizedException $e) { $message = $e->getMessage(); @@ -264,8 +264,8 @@ public function the_required_permissions_or_roles_can_be_displayed_in_the_except $message = null; try { - $this->roleOrPermissionMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); + $this->roleOrPermissionMiddleware->handle(new Request, function () { + return (new Response)->setContent(''); }, 'some-permission|some-role'); } catch (UnauthorizedException $e) { $message = $e->getMessage(); diff --git a/tests/TestCase.php b/tests/TestCase.php index 3a5532c8b..9e5e36c34 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -292,7 +292,7 @@ public function runMiddleware($middleware, $permission, $guard = null, bool $cli try { return $middleware->handle($request, function () { - return (new Response())->setContent(''); + return (new Response)->setContent(''); }, $permission, $guard)->status(); } catch (UnauthorizedException $e) { return $e->getStatusCode(); @@ -312,7 +312,7 @@ public function getRouter() public function getRouteResponse() { return function () { - return (new Response())->setContent(''); + return (new Response)->setContent(''); }; } diff --git a/tests/TestHelper.php b/tests/TestHelper.php index 5b8efa5cd..5eddab214 100644 --- a/tests/TestHelper.php +++ b/tests/TestHelper.php @@ -16,8 +16,8 @@ class TestHelper public function testMiddleware($middleware, $parameter) { try { - return $middleware->handle(new Request(), function () { - return (new Response())->setContent(''); + return $middleware->handle(new Request, function () { + return (new Response)->setContent(''); }, $parameter)->status(); } catch (HttpException $e) { return $e->getStatusCode(); diff --git a/tests/WildcardMiddlewareTest.php b/tests/WildcardMiddlewareTest.php index dc359fe49..596913707 100644 --- a/tests/WildcardMiddlewareTest.php +++ b/tests/WildcardMiddlewareTest.php @@ -23,11 +23,11 @@ protected function setUp(): void { parent::setUp(); - $this->roleMiddleware = new RoleMiddleware(); + $this->roleMiddleware = new RoleMiddleware; - $this->permissionMiddleware = new PermissionMiddleware(); + $this->permissionMiddleware = new PermissionMiddleware; - $this->roleOrPermissionMiddleware = new RoleOrPermissionMiddleware(); + $this->roleOrPermissionMiddleware = new RoleOrPermissionMiddleware; app('config')->set('permission.enable_wildcard_permission', true); } @@ -146,8 +146,8 @@ public function the_required_permissions_can_be_fetched_from_the_exception() $requiredPermissions = []; try { - $this->permissionMiddleware->handle(new Request(), function () { - return (new Response())->setContent(''); + $this->permissionMiddleware->handle(new Request, function () { + return (new Response)->setContent(''); }, 'permission.some'); } catch (UnauthorizedException $e) { $requiredPermissions = $e->getRequiredPermissions(); From 78122ea5746c82c6b78652419a323924f30b5e74 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Wed, 18 Sep 2024 00:36:48 -0500 Subject: [PATCH 0898/1013] Only show error if cache key exists and forgetCachedPermissions fail (#2707) --- src/Commands/CacheReset.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Commands/CacheReset.php b/src/Commands/CacheReset.php index 180d16972..7cfc65ae7 100644 --- a/src/Commands/CacheReset.php +++ b/src/Commands/CacheReset.php @@ -13,9 +13,12 @@ class CacheReset extends Command public function handle() { - if (app(PermissionRegistrar::class)->forgetCachedPermissions()) { + $permissionRegistrar = app(PermissionRegistrar::class); + $cacheExists = $permissionRegistrar->getCacheRepository()->has($permissionRegistrar->cacheKey); + + if ($permissionRegistrar->forgetCachedPermissions()) { $this->info('Permission cache flushed.'); - } else { + } else if ($cacheExists) { $this->error('Unable to flush cache.'); } } From 92b287c842ab128bb96d67376bfa1dffd94f1db0 Mon Sep 17 00:00:00 2001 From: drbyte Date: Wed, 18 Sep 2024 05:37:08 +0000 Subject: [PATCH 0899/1013] Fix styling --- src/Commands/CacheReset.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/CacheReset.php b/src/Commands/CacheReset.php index 7cfc65ae7..85f7c24eb 100644 --- a/src/Commands/CacheReset.php +++ b/src/Commands/CacheReset.php @@ -18,7 +18,7 @@ public function handle() if ($permissionRegistrar->forgetCachedPermissions()) { $this->info('Permission cache flushed.'); - } else if ($cacheExists) { + } elseif ($cacheExists) { $this->error('Unable to flush cache.'); } } From 581c628bf7f5d6a645743ca9b8df78614646584f Mon Sep 17 00:00:00 2001 From: kamil_wojtalak <48104288+KamilWojtalak@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:12:22 +0200 Subject: [PATCH 0900/1013] normalize variable naming --- docs/basic-usage/basic-usage.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/basic-usage/basic-usage.md b/docs/basic-usage/basic-usage.md index ea0d9e172..bd8f2acb0 100644 --- a/docs/basic-usage/basic-usage.md +++ b/docs/basic-usage/basic-usage.md @@ -98,11 +98,11 @@ The scope can accept a string, a `\Spatie\Permission\Models\Permission` object o Since Role and Permission models are extended from Eloquent models, basic Eloquent calls can be used as well: ```php -$all_users_with_all_their_roles = User::with('roles')->get(); -$all_users_with_all_their_direct_permissions = User::with('permissions')->get(); -$all_roles_in_database = Role::all()->pluck('name'); -$users_without_any_roles = User::doesntHave('roles')->get(); -$all_roles_except_a_and_b = Role::whereNotIn('name', ['role A', 'role B'])->get(); +$allUsersWithAllTheirRoles = User::with('roles')->get(); +$allUsersWithAllTheirDirectPermissions = User::with('permissions')->get(); +$allRolesInDatabase = Role::all()->pluck('name'); +$usersWithoutAnyRoles = User::doesntHave('roles')->get(); +$allRolesExceptAandB = Role::whereNotIn('name', ['role A', 'role B'])->get(); ``` ## Counting Users Having A Role From a6cfb37c395a364124cde7b392899bf2d89c1d78 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 19 Sep 2024 17:26:25 -0400 Subject: [PATCH 0901/1013] Update policy code examples --- docs/best-practices/using-policies.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/best-practices/using-policies.md b/docs/best-practices/using-policies.md index 5afc36026..c967ca9db 100644 --- a/docs/best-practices/using-policies.md +++ b/docs/best-practices/using-policies.md @@ -22,7 +22,7 @@ class PostPolicy { use HandlesAuthorization; - public function view(?User $user, Post $post) + public function view(?User $user, Post $post): bool { if ($post->published) { return true; @@ -42,12 +42,12 @@ class PostPolicy return $user->id == $post->user_id; } - public function create(User $user) + public function create(User $user): bool { return ($user->can('create posts')); } - public function update(User $user, Post $post) + public function update(User $user, Post $post): bool { if ($user->can('edit all posts')) { return true; @@ -58,7 +58,7 @@ class PostPolicy } } - public function delete(User $user, Post $post) + public function delete(User $user, Post $post): bool { if ($user->can('delete any post')) { return true; From 05f620c903f632a9ed26cb5e81d1c5e86601669c Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 19 Sep 2024 17:27:09 -0400 Subject: [PATCH 0902/1013] [Docs] Update to note Laravel 11.23 updates --- docs/basic-usage/enums.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/basic-usage/enums.md b/docs/basic-usage/enums.md index 2de63642b..493c9380b 100644 --- a/docs/basic-usage/enums.md +++ b/docs/basic-usage/enums.md @@ -47,7 +47,7 @@ enum RolesEnum: string ## Creating Roles/Permissions using Enums -When creating roles/permissions, you cannot pass a Enum name directly, because Eloquent expects a string for the name. +When **creating** roles/permissions, you cannot pass an Enum name directly, because Eloquent expects a string for the name. You must manually convert the name to its value in order to pass the correct string to Eloquent for the role/permission name. @@ -62,7 +62,7 @@ Same with creating Permissions. In your application code, when checking for authorization using features of this package, you can use `MyEnum::NAME` directly in most cases, without passing `->value` to convert to a string. -There will be times where you will need to manually fallback to adding `->value` (eg: `MyEnum::NAME->value`) when using features that aren't aware of Enum support. This will occur when you need to pass `string` values instead of an `Enum`, such as when interacting with Laravel's Gate via the `can()` methods/helpers (eg: `can`, `canAny`, etc). +There may occasionally be times where you will need to manually fallback to adding `->value` (eg: `MyEnum::NAME->value`) when using features that aren't aware of Enum support, such as when you need to pass `string` values instead of an `Enum` to a function that doesn't recognize Enums (Prior to Laravel v11.23.0 the framework didn't support Enums when interacting with Gate via the `can()` methods/helpers (eg: `can`, `canAny`, etc)). Examples: ```php @@ -70,7 +70,7 @@ Examples: $user->hasPermissionTo(PermissionsEnum::VIEWPOSTS); $user->hasPermissionTo(PermissionsEnum::VIEWPOSTS->value); -// when calling Gate features, such as Model Policies, etc +// when calling Gate features, such as Model Policies, etc, prior to Laravel v11.23.0 $user->can(PermissionsEnum::VIEWPOSTS->value); $model->can(PermissionsEnum::VIEWPOSTS->value); From 43bc084157aeb603c0262a148838126a7a343e01 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 18 Oct 2024 17:00:21 -0400 Subject: [PATCH 0903/1013] Add clarity around using custom guard --- docs/basic-usage/blade-directives.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/basic-usage/blade-directives.md b/docs/basic-usage/blade-directives.md index 4ee4b7b38..56629be32 100644 --- a/docs/basic-usage/blade-directives.md +++ b/docs/basic-usage/blade-directives.md @@ -22,6 +22,13 @@ You can use `@can`, `@cannot`, `@canany`, and `@guest` to test for permission-re When using a permission-name associated with permissions created in this package, you can use `@can('permission-name', 'guard_name')` if you need to check against a specific guard. +Example: +```php +@can('edit articles', 'guard_name') + // +@endcan +``` + You can also use `@haspermission('permission-name')` or `@haspermission('permission-name', 'guard_name')` in similar fashion. With corresponding `@endhaspermission`. There is no `@hasanypermission` directive: use `@canany` instead. From 0562f29c32da3de0483c467249def95614693a2c Mon Sep 17 00:00:00 2001 From: Wyatt Castaneda Date: Sat, 19 Oct 2024 23:32:52 -0700 Subject: [PATCH 0904/1013] Update teams-permissions.md Provide an example of pushing your custom middleware before the SubstituteBindings middleware in the applications middleware stack. In the new Laravel skeleton, this method provides a much cleaner approach than copying all default middlewares in the `bootstrap\app.php` `withMiddleware` method. --- docs/basic-usage/teams-permissions.md | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/basic-usage/teams-permissions.md b/docs/basic-usage/teams-permissions.md index f4a0d729e..ad220a1ac 100644 --- a/docs/basic-usage/teams-permissions.md +++ b/docs/basic-usage/teams-permissions.md @@ -56,6 +56,34 @@ class TeamsPermission **YOU MUST ALSO** set [the `$middlewarePriority` array](https://laravel.com/docs/master/middleware#sorting-middleware) in `app/Http/Kernel.php` to include your custom middleware before the `SubstituteBindings` middleware, else you may get *404 Not Found* responses when a *403 Not Authorized* response might be expected. +For example, in Laravel 11.27+ you can add something similiar to the `boot` method of your `AppServiceProvider`. + +```php +use App\Http\Middleware\YourCustomMiddlewareClass; +use Illuminate\Foundation\Http\Kernel; +use Illuminate\Routing\Middleware\SubstituteBindings; +use Illuminate\Support\ServiceProvider; + +class AppServiceProvider extends ServiceProvider +{ + public function register(): void + { + // + } + + public function boot(): void + { + /** @var Kernel $kernel */ + $kernel = app()->make(Kernel::class); + + $kernel->addToMiddlewarePriorityBefore( + SubstituteBindings::class, + YourCustomMiddlewareClass::class, + ); + } +} +``` + ## Roles Creating When creating a role you can pass the `team_id` as an optional parameter From d97529b30b1530dd7cd1ca2d5aecec46ad0cc378 Mon Sep 17 00:00:00 2001 From: Marc Leonhard Date: Tue, 22 Oct 2024 08:47:38 +0200 Subject: [PATCH 0905/1013] Update using-policies.md Remove not needed brackets in return statement --- docs/best-practices/using-policies.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/best-practices/using-policies.md b/docs/best-practices/using-policies.md index c967ca9db..648ad1971 100644 --- a/docs/best-practices/using-policies.md +++ b/docs/best-practices/using-policies.md @@ -44,7 +44,7 @@ class PostPolicy public function create(User $user): bool { - return ($user->can('create posts')); + return $user->can('create posts'); } public function update(User $user, Post $post): bool From 7cd173b74b77af25b785651612fd263b4e0dc07b Mon Sep 17 00:00:00 2001 From: erikn69 Date: Wed, 30 Oct 2024 14:54:37 -0500 Subject: [PATCH 0906/1013] Fix GuardDoesNotMatch should accept collection --- src/Models/Role.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Models/Role.php b/src/Models/Role.php index e29e2d449..6bd47b75f 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -185,7 +185,7 @@ public function hasPermissionTo($permission, ?string $guardName = null): bool $permission = $this->filterPermission($permission, $guardName); if (! $this->getGuardNames()->contains($permission->guard_name)) { - throw GuardDoesNotMatch::create($permission->guard_name, $guardName ?? $this->getGuardNames()); + throw GuardDoesNotMatch::create($permission->guard_name, $guardName ? collect([$guardName]) : $this->getGuardNames()); } return $this->permissions->contains($permission->getKeyName(), $permission->getKey()); From eb8795c39ea3c9a401b5fef720c52ab2ca095e82 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Wed, 30 Oct 2024 15:00:32 -0500 Subject: [PATCH 0907/1013] PHP 8.4 tests --- .github/workflows/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 38d97615f..842b1ca98 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - php: [8.3, 8.2, 8.1, 8.0] + php: [8.4, 8.3, 8.2, 8.1, 8.0] laravel: ["^11.0", "^10.0", "^9.0", "^8.12"] dependency-version: [prefer-lowest, prefer-stable] include: From 4772ff80d38c388a736972bdd63a0c0f8a429162 Mon Sep 17 00:00:00 2001 From: Paul Radt Date: Thu, 31 Oct 2024 09:13:22 +0100 Subject: [PATCH 0908/1013] Improve performance by using clone in stead of the newInstance method --- src/PermissionRegistrar.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 383629906..a73223762 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -360,7 +360,7 @@ private function getHydratedPermissionCollection(): Collection $permissionInstance = new ($this->getPermissionClass())(); return Collection::make(array_map( - fn ($item) => $permissionInstance->newInstance([], true) + fn ($item) => (clone $permissionInstance) ->setRawAttributes($this->aliasedArray(array_diff_key($item, ['r' => 0])), true) ->setRelation('roles', $this->getHydratedRoleCollection($item['r'] ?? [])), $this->permissions['permissions'] @@ -379,7 +379,7 @@ private function hydrateRolesCache(): void $roleInstance = new ($this->getRoleClass())(); array_map(function ($item) use ($roleInstance) { - $role = $roleInstance->newInstance([], true) + $role = (clone $roleInstance) ->setRawAttributes($this->aliasedArray($item), true); $this->cachedRoles[$role->getKey()] = $role; }, $this->permissions['roles']); From 5f483f51204bd17b89372e03ca3472e1cebac8bf Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 4 Nov 2024 08:27:43 -0800 Subject: [PATCH 0909/1013] Fix typo Fixed a very serious issue <3 --- src/PermissionRegistrar.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index a73223762..45d5d2238 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -199,7 +199,7 @@ private function loadPermissions(): void $this->cacheKey, $this->cacheExpirationTime, fn () => $this->getSerializedPermissionsForCache() ); - // fallback for old cache method, must be removed on next mayor version + // fallback for old cache method, must be removed on next major version if (! isset($this->permissions['alias'])) { $this->forgetCachedPermissions(); $this->loadPermissions(); From e8b97e350cdf8be233f48470c2172919d7cfc73d Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 4 Nov 2024 18:43:28 -0500 Subject: [PATCH 0910/1013] Remove v5 cache fallback Ref #1912 --- src/PermissionRegistrar.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 45d5d2238..643ed92ea 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -199,14 +199,6 @@ private function loadPermissions(): void $this->cacheKey, $this->cacheExpirationTime, fn () => $this->getSerializedPermissionsForCache() ); - // fallback for old cache method, must be removed on next major version - if (! isset($this->permissions['alias'])) { - $this->forgetCachedPermissions(); - $this->loadPermissions(); - - return; - } - $this->alias = $this->permissions['alias']; $this->hydrateRolesCache(); From 2444bb914a52c570c00ae8c94e096a58e01b2317 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 5 Nov 2024 12:30:49 -0500 Subject: [PATCH 0911/1013] Include larastan (#2755) * Include larastan --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index bd29fa40e..bd7d0299a 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ "illuminate/database": "^8.12|^9.0|^10.0|^11.0" }, "require-dev": { + "larastan/larastan": "^1.0|^2.0", "laravel/passport": "^11.0|^12.0", "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0", "phpunit/phpunit": "^9.4|^10.1" From 93477f5cc2d1ae9730254349443f0f07df694d63 Mon Sep 17 00:00:00 2001 From: drbyte Date: Tue, 5 Nov 2024 17:38:35 +0000 Subject: [PATCH 0912/1013] Update CHANGELOG --- CHANGELOG.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3c7c41a1..d50a3b359 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,44 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.10.0 - 2024-11-05 + +### What's Changed + +* Fix `GuardDoesNotMatch should accept collection` by @erikn69 in https://github.com/spatie/laravel-permission/pull/2748 +* Improve performance for hydrated collections by @inserve-paul in https://github.com/spatie/laravel-permission/pull/2749 +* Only show error if `cache key exists` and `forgetCachedPermissions` fails by @erikn69 in https://github.com/spatie/laravel-permission/pull/2707 +* Remove v5 cache fallback alias by @drbyte in https://github.com/spatie/laravel-permission/pull/2754 +* Include `Larastan` in `dev` by @drbyte in https://github.com/spatie/laravel-permission/pull/2755 + +#### Docs + +* [Docs example] Check for 'all' or 'any' permissions before specific permissions by @ceilidhboy in https://github.com/spatie/laravel-permission/pull/2694 +* [Docs] Fix typo in uuid.md by @levizoesch in https://github.com/spatie/laravel-permission/pull/2705 +* [Docs] Upgrade Guide - Add PR links to upgrade guide by @mraheelkhan in https://github.com/spatie/laravel-permission/pull/2716 +* [Docs] use more modern syntax for nullable return type by @galangaidilakbar in https://github.com/spatie/laravel-permission/pull/2719 +* [Docs] camelCase variable naming in example by @KamilWojtalak in https://github.com/spatie/laravel-permission/pull/2723 +* [Docs] Update using-policies.md by @marcleonhard in https://github.com/spatie/laravel-permission/pull/2741 +* [Docs] Example of pushing custom middleware before SubstituteBindings middleware by @WyattCast44 in https://github.com/spatie/laravel-permission/pull/2740 + +#### Other + +* PHP 8.4 tests by @erikn69 in https://github.com/spatie/laravel-permission/pull/2747 +* Fix comment typo by @machacekmartin in https://github.com/spatie/laravel-permission/pull/2753 + +### New Contributors + +* @ceilidhboy made their first contribution in https://github.com/spatie/laravel-permission/pull/2694 +* @levizoesch made their first contribution in https://github.com/spatie/laravel-permission/pull/2705 +* @galangaidilakbar made their first contribution in https://github.com/spatie/laravel-permission/pull/2719 +* @KamilWojtalak made their first contribution in https://github.com/spatie/laravel-permission/pull/2723 +* @marcleonhard made their first contribution in https://github.com/spatie/laravel-permission/pull/2741 +* @WyattCast44 made their first contribution in https://github.com/spatie/laravel-permission/pull/2740 +* @inserve-paul made their first contribution in https://github.com/spatie/laravel-permission/pull/2749 +* @machacekmartin made their first contribution in https://github.com/spatie/laravel-permission/pull/2753 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.9.0...6.10.0 + ## 6.9.0 - 2024-06-22 ### What's Changed @@ -843,6 +881,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` @@ -912,6 +951,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` From 8bb69d6d67387f7a00d93a2f5fab98860f06e704 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Fri, 8 Nov 2024 13:45:41 -0500 Subject: [PATCH 0913/1013] Fix: #2749 bug "Can no longer delete permissions" (#2759) * Fix: #2749 bug * Test added --- src/PermissionRegistrar.php | 4 ++-- tests/PermissionTest.php | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 643ed92ea..cc2e1ca5f 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -349,7 +349,7 @@ private function getSerializedRoleRelation($permission): array private function getHydratedPermissionCollection(): Collection { - $permissionInstance = new ($this->getPermissionClass())(); + $permissionInstance = (new ($this->getPermissionClass())())->newInstance([], true); return Collection::make(array_map( fn ($item) => (clone $permissionInstance) @@ -368,7 +368,7 @@ private function getHydratedRoleCollection(array $roles): Collection private function hydrateRolesCache(): void { - $roleInstance = new ($this->getRoleClass())(); + $roleInstance = (new ($this->getRoleClass())())->newInstance([], true); array_map(function ($item) use ($roleInstance) { $role = (clone $roleInstance) diff --git a/tests/PermissionTest.php b/tests/PermissionTest.php index b23094544..d43645730 100644 --- a/tests/PermissionTest.php +++ b/tests/PermissionTest.php @@ -67,4 +67,15 @@ public function it_is_retrievable_by_id() $this->assertEquals($this->testUserPermission->id, $permission_by_id->id); } + + /** @test */ + public function it_can_delete_hydrated_permissions() + { + $this->reloadPermissions(); + + $permission = app(Permission::class)->findByName($this->testUserPermission->name); + $permission->delete(); + + $this->assertCount(0, app(Permission::class)->where($this->testUserPermission->getKeyName(), $this->testUserPermission->getKey())->get()); + } } From 516e7bde2f2f7e75a51cb11bdae7759ecb03f4b6 Mon Sep 17 00:00:00 2001 From: drbyte Date: Fri, 8 Nov 2024 18:48:33 +0000 Subject: [PATCH 0914/1013] Update CHANGELOG --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d50a3b359..094cdbc55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.10.1 - 2024-11-08 + +### What's Changed + +* Fix #2749 regression bug in `6.10.0` : "Can no longer delete permissions" by @erikn69 in https://github.com/spatie/laravel-permission/pull/2759 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.10.0...6.10.1 + ## 6.10.0 - 2024-11-05 ### What's Changed @@ -882,6 +890,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` @@ -952,6 +961,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` From 4981e716642d11988bd0402160c14f18c2f389d8 Mon Sep 17 00:00:00 2001 From: Ken <26869657+ken-tam@users.noreply.github.com> Date: Tue, 19 Nov 2024 08:47:52 -0800 Subject: [PATCH 0915/1013] Update uuid.md (#2764) fix class location --- docs/advanced-usage/uuid.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md index 18fdd82f4..806df99d6 100644 --- a/docs/advanced-usage/uuid.md +++ b/docs/advanced-usage/uuid.md @@ -165,7 +165,7 @@ And edit `config/permission.php` */ - 'permission' => Spatie\Permission\Models\Permission::class -+ 'permission' => App\Models\Permission::class, ++ 'permission' => \App\Models\Permission::class, /* * When using the "HasRoles" trait from this package, we need to know which @@ -177,7 +177,7 @@ And edit `config/permission.php` */ - 'role' => Spatie\Permission\Models\Role::class, -+ 'role' => App\Models\Role::class, ++ 'role' => \App\Models\Role::class, ], ``` From 52119a132fbc210ebb0dc07a5eb672df6cc1bb6c Mon Sep 17 00:00:00 2001 From: Iwobi Okwudili Frank <102930821+frankliniwobi@users.noreply.github.com> Date: Wed, 27 Nov 2024 22:25:28 +0100 Subject: [PATCH 0916/1013] [Docs] Include Laravel 11 example in exceptions.md (#2768) * include a documentation for customizing exception in laravel 11 --------- Co-authored-by: Chris Brown --- docs/advanced-usage/exceptions.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/advanced-usage/exceptions.md b/docs/advanced-usage/exceptions.md index 291f85feb..231159107 100644 --- a/docs/advanced-usage/exceptions.md +++ b/docs/advanced-usage/exceptions.md @@ -3,14 +3,14 @@ title: Exceptions weight: 3 --- -If you need to override exceptions thrown by this package, you can simply use normal [Laravel practices for handling exceptions](https://laravel.com/docs/10.x/errors#rendering-exceptions). +If you need to override exceptions thrown by this package, you can simply use normal [Laravel practices for handling exceptions](https://laravel.com/docs/errors#rendering-exceptions). An example is shown below for your convenience, but nothing here is specific to this package other than the name of the exception. You can find all the exceptions added by this package in the code here: [https://github.com/spatie/laravel-permission/tree/main/src/Exceptions](https://github.com/spatie/laravel-permission/tree/main/src/Exceptions) -**app/Exceptions/Handler.php** +**Laravel 10: app/Exceptions/Handler.php** ```php public function register() @@ -23,3 +23,16 @@ public function register() }); } ``` + +**Laravel 11: bootstrap/app.php** +```php + +->withExceptions(function (Exceptions $exceptions) { + $exceptions->render(function (\Spatie\Permission\Exceptions\UnauthorizedException $e, $request) { + return response()->json([ + 'responseMessage' => 'You do not have the required authorization.', + 'responseStatus' => 403, + ]); + }); +} +``` From a002fbacb2cde1350142138862910ba41b2090d6 Mon Sep 17 00:00:00 2001 From: Iman Date: Tue, 3 Dec 2024 22:05:49 +0330 Subject: [PATCH 0917/1013] Update upgrading.md `composer update` will upgrade all the packages which can be problematic or unwanted. Other non-technical changes are suggested by Grammarly. --- docs/upgrading.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index 2d56f9b18..f0b58f64c 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -7,9 +7,9 @@ weight: 6 ALL upgrades of this package should follow these steps: -1. Composer. Upgrading between major versions of this package always require the usual Composer steps: +1. Composer. Upgrading between major versions of this package always requires the usual Composer steps: - Update your `composer.json` to specify the new major version, for example: `^6.0` - - Then run `composer update`. + - Then run `composer update spatie/laravel-permission`. 2. Migrations. Compare the `migration` file stubs in the NEW version of this package against the migrations you've already run inside your app. If necessary, create a new migration (by hand) to apply any new database changes. @@ -32,16 +32,16 @@ There are a few breaking-changes when upgrading to v6, but most of them won't af For guidance with upgrading your extended models, your migrations, your routes, etc, see the **Upgrade Essentials** section at the top of this file. -1. Due to the improved ULID/UUID/GUID support, any package methods which accept a Permission or Role `id` must pass that `id` as an `integer`. If you pass it as a numeric string, the functions will attempt to lookup the role/permission as a string. In such cases you may see errors such as `There is no permission named '123' for guard 'web'.` (where `'123'` is being treated as a string because it was passed as a string instead of as an integer). This also applies to arrays of id's: if it's an array of strings we will do a lookup on the name instead of on the id. **This will mostly only affect UI pages** because an HTML Request is received as string data. **The solution is simple:** if you're passing integers to a form field, then convert them back to integers when using that field's data for calling functions to grant/assign/sync/remove/revoke permissions and roles. One way to convert an array of permissions `id`'s from strings to integers is: `collect($validated['permission'])->map(fn($val)=>(int)$val)` +1. Due to the improved ULID/UUID/GUID support, any package methods which accept a Permission or Role `id` must pass that `id` as an `integer`. If you pass it as a numeric string, the functions will attempt to look up the role/permission as a string. In such cases, you may see errors such as `There is no permission named '123' for guard 'web'.` (where `'123'` is being treated as a string because it was passed as a string instead of as an integer). This also applies to arrays of id's: if it's an array of strings we will do a lookup on the name instead of on the id. **This will mostly only affect UI pages** because an HTML Request is received as string data. **The solution is simple:** if you're passing integers to a form field, then convert them back to integers when using that field's data for calling functions to grant/assign/sync/remove/revoke permissions and roles. One way to convert an array of permissions `id`'s from strings to integers is: `collect($validated['permission'])->map(fn($val)=>(int)$val)` 2. If you have overridden the `getPermissionClass()` or `getRoleClass()` methods or have custom Models, you will need to revisit those customizations. See PR #2368 for details. eg: if you have a custom model you will need to make changes, including accessing the model using `$this->permissionClass::` syntax (eg: using `::` instead of `->`) in all the overridden methods that make use of the models. - Be sure to compare your custom models with originals to see what else may have changed. + Be sure to compare your custom models with the originals to see what else may have changed. 3. Model and Contract/Interface updates. The Role and Permission Models and Contracts/Interfaces have been updated with syntax changes to method signatures. Update any models you have extended, or contracts implemented, accordingly. See PR [#2380](https://github.com/spatie/laravel-permission/pull/2380) and [#2480](https://github.com/spatie/laravel-permission/pull/2480) for some of the specifics. -4. Migrations WILL need to be upgraded. (They have been updated to anonymous-class syntax that was introduced in Laravel 8, AND some structural coding changes in the registrar class changed the way we extracted configuration settings in the migration files.) There are no changes to the package's structure since v5, so if you had not customized it from the original then replacing the contents of the file should be enough. (Usually the only customization is if you've switched to UUIDs or customized MySQL index name lengths.) +4. Migrations WILL need to be upgraded. (They have been updated to anonymous-class syntax that was introduced in Laravel 8, AND some structural coding changes in the registrar class changed the way we extracted configuration settings in the migration files.) There are no changes to the package's structure since v5, so if you had not customized it from the original then replacing the contents of the file should be enough. (Usually, the only customization is if you've switched to UUIDs or customized MySQL index name lengths.) **If you get the following error, it means your migration file needs upgrading: `Error: Access to undeclared static property Spatie\Permission\PermissionRegistrar::$pivotPermission`** 5. MIDDLEWARE: @@ -52,7 +52,7 @@ eg: if you have a custom model you will need to make changes, including accessin 3. In the unlikely event that you have customized the Wildcard Permissions feature by extending the `WildcardPermission` model, please note that the public interface has changed significantly and you will need to update your extended model with the new method signatures. -6. Test suites. If you have tests which manually clear the permission cache and re-register permissions, you no longer need to call `\Spatie\Permission\PermissionRegistrar::class)->registerPermissions();`. In fact, **calls to `->registerPermissions()` MUST be deleted from your tests**. +6. Test suites. If you have tests that manually clear the permission cache and re-register permissions, you no longer need to call `\Spatie\Permission\PermissionRegistrar::class)->registerPermissions();`. In fact, **calls to `->registerPermissions()` MUST be deleted from your tests**. (Calling `app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();` after creating roles and permissions in migrations and factories and seeders is still okay and encouraged.) From ee71301a43c0b7e91e01e59895679a952d7a0c87 Mon Sep 17 00:00:00 2001 From: Robert Brodie Date: Thu, 26 Dec 2024 10:47:28 -0500 Subject: [PATCH 0918/1013] Replace php-cs-fixer with Laravel Pint (#2780) * Replace php-cs-fixer with Laravel Pint * Require laravel/pint ^1.0 to support PHP 8.0 like the main package --- composer.json | 3 ++- pint.json | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 pint.json diff --git a/composer.json b/composer.json index bd7d0299a..b6d404d75 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "require-dev": { "larastan/larastan": "^1.0|^2.0", "laravel/passport": "^11.0|^12.0", + "laravel/pint": "^1.0", "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0", "phpunit/phpunit": "^9.4|^10.1" }, @@ -65,7 +66,7 @@ }, "scripts": { "test": "phpunit", - "format": "php-cs-fixer fix --allow-risky=yes", + "format": "pint", "analyse": "phpstan analyse" } } diff --git a/pint.json b/pint.json new file mode 100644 index 000000000..ea56b7b88 --- /dev/null +++ b/pint.json @@ -0,0 +1,6 @@ +{ + "preset": "laravel", + "rules": { + "php_unit_method_casing": false + } +} From b7ab3b774f4ede85aabd058bf06e43caaaf8988a Mon Sep 17 00:00:00 2001 From: m3skalina <43817736+m3skalina@users.noreply.github.com> Date: Thu, 2 Jan 2025 18:29:29 +0100 Subject: [PATCH 0919/1013] Update passport.md semicolon added --- docs/basic-usage/passport.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/passport.md b/docs/basic-usage/passport.md index 1926f1273..9bfe86d50 100644 --- a/docs/basic-usage/passport.md +++ b/docs/basic-usage/passport.md @@ -29,7 +29,7 @@ class Client extends BaseClient implements AuthorizableContract public function guardName() { - return 'api' + return 'api'; } } ``` From c1284ba07e74b05cfc63cd2958411ce659451609 Mon Sep 17 00:00:00 2001 From: Mike Date: Sun, 5 Jan 2025 19:39:18 +0100 Subject: [PATCH 0920/1013] Update new-app.md The user created is tester@example.com; not test@example.com --- docs/basic-usage/new-app.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index 23903c3ab..7f1ed8361 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -160,7 +160,7 @@ If you are creating a demo app for reporting a bug or getting help with troubles If this is your first app with this package, you may want some quick permission examples to see it in action. If you've set up your app using the instructions above, the following examples will work in conjunction with the users and permissions created in the seeder. -Three users were created: test@example.com, admin@example.com, superadmin@example.com and the password for each is "password". +Three users were created: tester@example.com, admin@example.com, superadmin@example.com and the password for each is "password". `/resources/views/dashboard.php` ```diff From d4b9db425badb874742d09de19fa6e3ee8d6dd29 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 10 Jan 2025 14:14:19 -0500 Subject: [PATCH 0921/1013] [Docs] Add related article --- docs/best-practices/roles-vs-permissions.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/best-practices/roles-vs-permissions.md b/docs/best-practices/roles-vs-permissions.md index ad94432aa..033d3eb52 100644 --- a/docs/best-practices/roles-vs-permissions.md +++ b/docs/best-practices/roles-vs-permissions.md @@ -32,3 +32,6 @@ Summary: Sometimes certain groups of `route` rules may make best sense to group them around a `role`, but still, whenever possible, there is less overhead used if you can check against a specific `permission` instead. +### FURTHER READING: + +[@joelclermont](https://github.com/joelclermont) at [masteringlaravel.io](https://masteringlaravel.io/daily) offers similar guidance in his post about [Treating Feature Access As Data, Not Code](https://masteringlaravel.io/daily/2025-01-09-treat-feature-access-as-data-not-code) From ba95ccd32e23add451b71212c929de014627e085 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 10 Jan 2025 14:20:05 -0500 Subject: [PATCH 0922/1013] Clarification for Laravel 11 optional provider registration Replaces and Closes #2786 --- docs/installation-laravel.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index f7cb644f9..07e4ab54b 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -28,14 +28,7 @@ Package Version | Laravel Version composer require spatie/laravel-permission -4. Optional: The service provider will automatically get registered. Or you may manually add the service provider in your `config/app.php` file: - - ``` - 'providers' => [ - // ... - Spatie\Permission\PermissionServiceProvider::class, - ]; - ``` +4. Optional: The **`Spatie\Permission\PermissionServiceProvider::class`** service provider will automatically get registered. Or you may manually add the service provider to the array in your `config/providers.php` (or `config/app.php` in Laravel 10 or older) file. 5. **You should publish** [the migration](https://github.com/spatie/laravel-permission/blob/main/database/migrations/create_permission_tables.php.stub) and the [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/main/config/permission.php) with: From ecd39dc304bf853456c99ad95b8906873b3b37d4 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 28 Jan 2025 14:01:35 -0500 Subject: [PATCH 0923/1013] Update package compatibilities --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index b6d404d75..8e7ea5cc4 100644 --- a/composer.json +++ b/composer.json @@ -29,11 +29,11 @@ "illuminate/database": "^8.12|^9.0|^10.0|^11.0" }, "require-dev": { - "larastan/larastan": "^1.0|^2.0", + "larastan/larastan": "^1.0|^2.0|^3.0", "laravel/passport": "^11.0|^12.0", "laravel/pint": "^1.0", "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0", - "phpunit/phpunit": "^9.4|^10.1" + "phpunit/phpunit": "^9.4|^10.1|^11.5" }, "minimum-stability": "dev", "prefer-stable": true, From 1b67ee8d3c9faef6cf1b1cd8a3450261d36f1b08 Mon Sep 17 00:00:00 2001 From: Curious Team Date: Thu, 30 Jan 2025 01:13:39 +0600 Subject: [PATCH 0924/1013] Update installation-laravel.md to fix providers.php location. 4. Optional: The **`Spatie\Permission\PermissionServiceProvider::class`** service provider will automatically get registered. Or you may manually add the service provider to the array in your `bootstrap/providers.php` (or `config/app.php` in Laravel 10 or older) file. --- docs/installation-laravel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 07e4ab54b..54bac7b4b 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -28,7 +28,7 @@ Package Version | Laravel Version composer require spatie/laravel-permission -4. Optional: The **`Spatie\Permission\PermissionServiceProvider::class`** service provider will automatically get registered. Or you may manually add the service provider to the array in your `config/providers.php` (or `config/app.php` in Laravel 10 or older) file. +4. Optional: The **`Spatie\Permission\PermissionServiceProvider::class`** service provider will automatically get registered. Or you may manually add the service provider to the array in your `bootstrap/providers.php` (or `config/app.php` in Laravel 10 or older) file. 5. **You should publish** [the migration](https://github.com/spatie/laravel-permission/blob/main/database/migrations/create_permission_tables.php.stub) and the [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/main/config/permission.php) with: From 34744b866c0d021dbc9077481693607893ea28c8 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 30 Jan 2025 15:19:23 -0500 Subject: [PATCH 0925/1013] phpstan - ignore --- src/PermissionServiceProvider.php | 1 + src/Traits/HasPermissions.php | 1 + src/Traits/HasRoles.php | 1 + 3 files changed, 3 insertions(+) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 1dc28f595..31a029f48 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -144,6 +144,7 @@ protected function registerBladeExtensions(BladeCompiler $bladeCompiler): void protected function registerMacroHelpers(): void { + // @phpstan-ignore-next-line if (! method_exists(Route::class, 'macro')) { // Lumen return; } diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 25d506088..8147cac60 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -348,6 +348,7 @@ public function getAllPermissions(): Collection /** @var Collection $permissions */ $permissions = $this->permissions; + // @phpstan-ignore-next-line if (method_exists($this, 'roles')) { $permissions = $permissions->merge($this->getPermissionsViaRoles()); } diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 782ed20a1..4a332739d 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -127,6 +127,7 @@ private function collectRoles(...$roles): array } $role = $this->getStoredRole($role); + // @phpstan-ignore-next-line if (! $role instanceof Role) { return $array; } From a629566b6624a3167ff7d95129f946b36cb8c39e Mon Sep 17 00:00:00 2001 From: drbyte <404472+drbyte@users.noreply.github.com> Date: Thu, 30 Jan 2025 20:19:49 +0000 Subject: [PATCH 0926/1013] Fix styling --- config/permission.php | 4 ++-- tests/CommandTest.php | 2 +- tests/HasPermissionsTest.php | 6 +++--- tests/HasPermissionsWithCustomModelsTest.php | 2 +- tests/HasRolesTest.php | 6 +++--- tests/TeamHasRolesTest.php | 8 ++++---- tests/TestCase.php | 8 ++++---- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/config/permission.php b/config/permission.php index 2a520f351..74c6402d2 100644 --- a/config/permission.php +++ b/config/permission.php @@ -75,8 +75,8 @@ /* * Change this if you want to name the related pivots other than defaults */ - 'role_pivot_key' => null, //default 'role_id', - 'permission_pivot_key' => null, //default 'permission_id', + 'role_pivot_key' => null, // default 'role_id', + 'permission_pivot_key' => null, // default 'permission_id', /* * Change this if you want to name the related model primary key other than diff --git a/tests/CommandTest.php b/tests/CommandTest.php index a5bd5b558..4fbb6fa3a 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -149,7 +149,7 @@ public function it_can_setup_teams_upgrade() $AddTeamsFields = require $matchingFiles[count($matchingFiles) - 1]; $AddTeamsFields->up(); - $AddTeamsFields->up(); //test upgrade teams migration fresh + $AddTeamsFields->up(); // test upgrade teams migration fresh Role::create(['name' => 'new-role', 'team_test_id' => 1]); $role = Role::where('name', 'new-role')->first(); diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index ce197b16c..e5657536f 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -654,7 +654,7 @@ public function it_does_not_run_unnecessary_sqls_when_assigning_new_permissions( $this->testUser->syncPermissions($this->testUserPermission, $permission2); DB::disableQueryLog(); - $this->assertSame(2, count(DB::getQueryLog())); //avoid unnecessary sqls + $this->assertSame(2, count(DB::getQueryLog())); // avoid unnecessary sqls } /** @test */ @@ -676,7 +676,7 @@ public function calling_givePermissionTo_before_saving_object_doesnt_interfere_w $this->assertTrue($user2->fresh()->hasPermissionTo('edit-articles')); $this->assertFalse($user2->fresh()->hasPermissionTo('edit-news')); - $this->assertSame(2, count(DB::getQueryLog())); //avoid unnecessary sync + $this->assertSame(2, count(DB::getQueryLog())); // avoid unnecessary sync } /** @test */ @@ -698,7 +698,7 @@ public function calling_syncPermissions_before_saving_object_doesnt_interfere_wi $this->assertTrue($user2->fresh()->hasPermissionTo('edit-articles')); $this->assertFalse($user2->fresh()->hasPermissionTo('edit-news')); - $this->assertSame(2, count(DB::getQueryLog())); //avoid unnecessary sync + $this->assertSame(2, count(DB::getQueryLog())); // avoid unnecessary sync } /** @test */ diff --git a/tests/HasPermissionsWithCustomModelsTest.php b/tests/HasPermissionsWithCustomModelsTest.php index 60aa481e2..d26b92286 100644 --- a/tests/HasPermissionsWithCustomModelsTest.php +++ b/tests/HasPermissionsWithCustomModelsTest.php @@ -123,7 +123,7 @@ public function it_does_detach_roles_and_users_when_force_deleting() $this->testUserPermission->forceDelete(); DB::disableQueryLog(); - $this->assertSame(3 + $this->resetDatabaseQuery, count(DB::getQueryLog())); //avoid detach permissions on permissions + $this->assertSame(3 + $this->resetDatabaseQuery, count(DB::getQueryLog())); // avoid detach permissions on permissions $permission = Permission::withTrashed()->find($permission_id); diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 93ee35749..c5f78169e 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -359,7 +359,7 @@ public function it_does_not_run_unnecessary_sqls_when_assigning_new_roles() $this->testUser->syncRoles($this->testUserRole, $role2); DB::disableQueryLog(); - $this->assertSame(2, count(DB::getQueryLog())); //avoid unnecessary sqls + $this->assertSame(2, count(DB::getQueryLog())); // avoid unnecessary sqls } /** @test */ @@ -381,7 +381,7 @@ public function calling_syncRoles_before_saving_object_doesnt_interfere_with_oth $this->assertTrue($user2->fresh()->hasRole('testRole2')); $this->assertFalse($user2->fresh()->hasRole('testRole')); - $this->assertSame(2, count(DB::getQueryLog())); //avoid unnecessary sync + $this->assertSame(2, count(DB::getQueryLog())); // avoid unnecessary sync } /** @test */ @@ -403,7 +403,7 @@ public function calling_assignRole_before_saving_object_doesnt_interfere_with_ot $this->assertTrue($admin_user->fresh()->hasRole('testRole2')); $this->assertFalse($admin_user->fresh()->hasRole('testRole')); - $this->assertSame(2, count(DB::getQueryLog())); //avoid unnecessary sync + $this->assertSame(2, count(DB::getQueryLog())); // avoid unnecessary sync } /** @test */ diff --git a/tests/TeamHasRolesTest.php b/tests/TeamHasRolesTest.php index 16d8cdd6d..6ad51ec4e 100644 --- a/tests/TeamHasRolesTest.php +++ b/tests/TeamHasRolesTest.php @@ -39,9 +39,9 @@ public function it_deletes_pivot_table_entries_when_deleting_models() /** @test */ public function it_can_assign_same_and_different_roles_on_same_user_different_teams() { - app(Role::class)->create(['name' => 'testRole3']); //team_test_id = 1 by main class + app(Role::class)->create(['name' => 'testRole3']); // team_test_id = 1 by main class app(Role::class)->create(['name' => 'testRole3', 'team_test_id' => 2]); - app(Role::class)->create(['name' => 'testRole4', 'team_test_id' => null]); //global role + app(Role::class)->create(['name' => 'testRole4', 'team_test_id' => null]); // global role $testRole3Team1 = app(Role::class)->where(['name' => 'testRole3', 'team_test_id' => 1])->first(); $testRole3Team2 = app(Role::class)->where(['name' => 'testRole3', 'team_test_id' => 2])->first(); @@ -66,7 +66,7 @@ public function it_can_assign_same_and_different_roles_on_same_user_different_te $this->testUser->assignRole('testRole3', 'testRole4'); $this->assertTrue($this->testUser->hasExactRoles(['testRole', 'testRole2', 'testRole3', 'testRole4'])); - $this->assertTrue($this->testUser->hasRole($testRole3Team1)); //testRole3 team=1 + $this->assertTrue($this->testUser->hasRole($testRole3Team1)); // testRole3 team=1 $this->assertTrue($this->testUser->hasRole($testRole4NoTeam)); // global role team=null setPermissionsTeamId(2); @@ -77,7 +77,7 @@ public function it_can_assign_same_and_different_roles_on_same_user_different_te $this->testUser->getRoleNames()->sort()->values() ); $this->assertTrue($this->testUser->hasExactRoles(['testRole', 'testRole3'])); - $this->assertTrue($this->testUser->hasRole($testRole3Team2)); //testRole3 team=2 + $this->assertTrue($this->testUser->hasRole($testRole3Team2)); // testRole3 team=2 $this->testUser->assignRole('testRole4'); $this->assertTrue($this->testUser->hasExactRoles(['testRole', 'testRole3', 'testRole4'])); $this->assertTrue($this->testUser->hasRole($testRole4NoTeam)); // global role team=null diff --git a/tests/TestCase.php b/tests/TestCase.php index 9e5e36c34..b6ec94970 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -112,7 +112,7 @@ protected function getEnvironmentSetUp($app) { $app['config']->set('permission.register_permission_check_method', true); $app['config']->set('permission.teams', $this->hasTeams); - $app['config']->set('permission.testing', true); //fix sqlite + $app['config']->set('permission.testing', true); // fix sqlite $app['config']->set('permission.column_names.model_morph_key', 'model_test_id'); $app['config']->set('permission.column_names.team_foreign_key', 'team_test_id'); $app['config']->set('database.default', 'sqlite'); @@ -143,8 +143,8 @@ protected function getEnvironmentSetUp($app) // FOR MANUAL TESTING OF ALTERNATE CACHE STORES: // $app['config']->set('cache.default', 'array'); - //Laravel supports: array, database, file - //requires extensions: apc, memcached, redis, dynamodb, octane + // Laravel supports: array, database, file + // requires extensions: apc, memcached, redis, dynamodb, octane } /** @@ -282,7 +282,7 @@ public function setUpRoutes(): void }); } - ////// TEST HELPERS + // //// TEST HELPERS public function runMiddleware($middleware, $permission, $guard = null, bool $client = false) { $request = new Request; From 3cea227902152c43b31bf403cdaf60d8fd2b41b3 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 30 Jan 2025 15:26:56 -0500 Subject: [PATCH 0927/1013] Add configurable team resolver for permission team id (#2790) * Add Permissions Team Resolver Implementation - Introduced a new `PermissionsTeamResolver` interface to standardize team ID handling for permissions. - Implemented `DefaultPermissionsTeamResolver` class to manage team IDs. - Updated `permission.php` configuration to include `team_resolver` setting. - Modified `PermissionRegistrar` to utilize the new team resolver for setting and getting permissions team IDs. --- config/permission.php | 6 +++++ src/Contracts/PermissionsTeamResolver.php | 18 +++++++++++++ src/DefaultTeamResolver.php | 31 +++++++++++++++++++++++ src/PermissionRegistrar.php | 13 +++++----- 4 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 src/Contracts/PermissionsTeamResolver.php create mode 100644 src/DefaultTeamResolver.php diff --git a/config/permission.php b/config/permission.php index 74c6402d2..c05c52645 100644 --- a/config/permission.php +++ b/config/permission.php @@ -122,6 +122,12 @@ 'teams' => false, + + /* + * The class to use to resolve the permissions team id + */ + 'team_resolver' => \Spatie\Permission\DefaultTeamResolver::class, + /* * Passport Client Credentials Grant * When set to true the package will use Passports Client to check permissions diff --git a/src/Contracts/PermissionsTeamResolver.php b/src/Contracts/PermissionsTeamResolver.php new file mode 100644 index 000000000..1c8fe8985 --- /dev/null +++ b/src/Contracts/PermissionsTeamResolver.php @@ -0,0 +1,18 @@ +getKey(); + } + $this->teamId = $id; + } + + /** + * @return int|string|null + */ + public function getPermissionsTeamId() : int|string|null + { + return $this->teamId; + } +} diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index cc2e1ca5f..92e6edc4e 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Spatie\Permission\Contracts\Permission; +use Spatie\Permission\Contracts\PermissionsTeamResolver; use Spatie\Permission\Contracts\Role; class PermissionRegistrar @@ -34,9 +35,9 @@ class PermissionRegistrar public bool $teams; - public string $teamsKey; + protected PermissionsTeamResolver $teamResolver; - protected string|int|null $teamId = null; + public string $teamsKey; public string $cacheKey; @@ -55,6 +56,7 @@ public function __construct(CacheManager $cacheManager) { $this->permissionClass = config('permission.models.permission'); $this->roleClass = config('permission.models.role'); + $this->teamResolver = new (config('permission.team_resolver', DefaultTeamResolver::class)); $this->cacheManager = $cacheManager; $this->initializeCache(); @@ -101,10 +103,7 @@ protected function getCacheStoreFromConfig(): Repository */ public function setPermissionsTeamId($id): void { - if ($id instanceof \Illuminate\Database\Eloquent\Model) { - $id = $id->getKey(); - } - $this->teamId = $id; + $this->teamResolver->setPermissionsTeamId($id); } /** @@ -112,7 +111,7 @@ public function setPermissionsTeamId($id): void */ public function getPermissionsTeamId() { - return $this->teamId; + return $this->teamResolver->getPermissionsTeamId(); } /** From f9f8c230a4e61dfcc81273d1485b05dc2082badc Mon Sep 17 00:00:00 2001 From: drbyte <404472+drbyte@users.noreply.github.com> Date: Thu, 30 Jan 2025 20:27:22 +0000 Subject: [PATCH 0928/1013] Fix styling --- config/permission.php | 1 - src/Contracts/PermissionsTeamResolver.php | 5 +---- src/DefaultTeamResolver.php | 5 +---- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/config/permission.php b/config/permission.php index c05c52645..c3b69a5ec 100644 --- a/config/permission.php +++ b/config/permission.php @@ -122,7 +122,6 @@ 'teams' => false, - /* * The class to use to resolve the permissions team id */ diff --git a/src/Contracts/PermissionsTeamResolver.php b/src/Contracts/PermissionsTeamResolver.php index 1c8fe8985..780cd2a1d 100644 --- a/src/Contracts/PermissionsTeamResolver.php +++ b/src/Contracts/PermissionsTeamResolver.php @@ -4,10 +4,7 @@ interface PermissionsTeamResolver { - /** - * @return int|string|null - */ - public function getPermissionsTeamId() : int|string|null; + public function getPermissionsTeamId(): int|string|null; /** * Set the team id for teams/groups support, this id is used when querying permissions/roles diff --git a/src/DefaultTeamResolver.php b/src/DefaultTeamResolver.php index 93bf5481a..889898e2c 100644 --- a/src/DefaultTeamResolver.php +++ b/src/DefaultTeamResolver.php @@ -21,10 +21,7 @@ public function setPermissionsTeamId($id): void $this->teamId = $id; } - /** - * @return int|string|null - */ - public function getPermissionsTeamId() : int|string|null + public function getPermissionsTeamId(): int|string|null { return $this->teamId; } From 1da8aeedb1f84c49d5df01f5f1637b00e60c77ce Mon Sep 17 00:00:00 2001 From: drbyte <404472+drbyte@users.noreply.github.com> Date: Thu, 30 Jan 2025 20:32:26 +0000 Subject: [PATCH 0929/1013] Update CHANGELOG --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 094cdbc55..62c8daaac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,38 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.11.0 - 2025-01-30 + +### What's Changed + +* Add configurable team resolver for permission team id (helpful for Jetstream, etc) by @adrenallen in https://github.com/spatie/laravel-permission/pull/2790 + +### Internals + +* Replace php-cs-fixer with Laravel Pint by @bobbrodie in https://github.com/spatie/laravel-permission/pull/2780 + +### Documentation Updates + +* [Docs] Include namespace in example in uuid.md by @ken-tam in https://github.com/spatie/laravel-permission/pull/2764 +* [Docs] Include Laravel 11 example in exceptions.md by @frankliniwobi in https://github.com/spatie/laravel-permission/pull/2768 +* [Docs] Fix typo in code example in passport.md by @m3skalina in https://github.com/spatie/laravel-permission/pull/2782 +* [Docs] Correct username in new-app.md by @trippodi in https://github.com/spatie/laravel-permission/pull/2785 +* [Docs] Add composer specificity by @imanghafoori1 in https://github.com/spatie/laravel-permission/pull/2772 +* [Docs] Update installation-laravel.md to fix providers.php location. by @curiousteam in https://github.com/spatie/laravel-permission/pull/2796 + +### New Contributors + +* @ken-tam made their first contribution in https://github.com/spatie/laravel-permission/pull/2764 +* @frankliniwobi made their first contribution in https://github.com/spatie/laravel-permission/pull/2768 +* @bobbrodie made their first contribution in https://github.com/spatie/laravel-permission/pull/2780 +* @m3skalina made their first contribution in https://github.com/spatie/laravel-permission/pull/2782 +* @trippodi made their first contribution in https://github.com/spatie/laravel-permission/pull/2785 +* @imanghafoori1 made their first contribution in https://github.com/spatie/laravel-permission/pull/2772 +* @curiousteam made their first contribution in https://github.com/spatie/laravel-permission/pull/2796 +* @adrenallen made their first contribution in https://github.com/spatie/laravel-permission/pull/2790 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.10.1...6.11.0 + ## 6.10.1 - 2024-11-08 ### What's Changed @@ -891,6 +923,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` @@ -962,6 +995,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` From f1d198a2e929be1185700146c725a703832716f1 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 30 Jan 2025 15:49:18 -0500 Subject: [PATCH 0930/1013] Remove larastan as dependency Larastan composer is not updated yet to support latest Laravel, so leaving Larastan out of this package's dependencies allows the package to be used/tested regardless of larastan's availability. The phpstan workflow already will load larastan when needed, so remains compatible with this change. --- composer.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 8e7ea5cc4..f9adb59a4 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,6 @@ "illuminate/database": "^8.12|^9.0|^10.0|^11.0" }, "require-dev": { - "larastan/larastan": "^1.0|^2.0|^3.0", "laravel/passport": "^11.0|^12.0", "laravel/pint": "^1.0", "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0", @@ -67,6 +66,6 @@ "scripts": { "test": "phpunit", "format": "pint", - "analyse": "phpstan analyse" + "analyse": "echo 'Checking dependencies...' && composer require --dev larastan/larastan && phpstan analyse" } } From cbeecfc0452ee8e588fceba45cc18c2e94cd3abd Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 30 Jan 2025 15:52:16 -0500 Subject: [PATCH 0931/1013] Add Laravel 12 --- composer.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index f9adb59a4..c04c5bff9 100644 --- a/composer.json +++ b/composer.json @@ -23,15 +23,15 @@ "homepage": "https://github.com/spatie/laravel-permission", "require": { "php": "^8.0", - "illuminate/auth": "^8.12|^9.0|^10.0|^11.0", - "illuminate/container": "^8.12|^9.0|^10.0|^11.0", - "illuminate/contracts": "^8.12|^9.0|^10.0|^11.0", - "illuminate/database": "^8.12|^9.0|^10.0|^11.0" + "illuminate/auth": "^8.12|^9.0|^10.0|^11.0|^12.0", + "illuminate/container": "^8.12|^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^8.12|^9.0|^10.0|^11.0|^12.0", + "illuminate/database": "^8.12|^9.0|^10.0|^11.0|^12.0" }, "require-dev": { "laravel/passport": "^11.0|^12.0", "laravel/pint": "^1.0", - "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0", + "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0|^10.0", "phpunit/phpunit": "^9.4|^10.1|^11.5" }, "minimum-stability": "dev", From 89c64bf0eab641f45fb34026e90f01ed47653469 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 30 Jan 2025 15:53:03 -0500 Subject: [PATCH 0932/1013] [Docs] Laravel 12 --- docs/installation-laravel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 54bac7b4b..c3a6646ce 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -9,7 +9,7 @@ Choose the version of this package that suits your Laravel version. Package Version | Laravel Version ----------------|----------- - ^6.0 | 8,9,10,11 (PHP 8.0+) + ^6.0 | 8,9,10,11,12 (PHP 8.0+) ^5.8 | 7,8,9,10 ^5.7 | 7,8,9 ^5.4-^5.6 | 7,8 From 722e6428e4fc1f9e2c4fcb4f2f5fd3eba843ba83 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 30 Jan 2025 16:03:31 -0500 Subject: [PATCH 0933/1013] Add Laravel 12 to GH workflows --- .github/workflows/run-tests.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 842b1ca98..3f47884ba 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -10,9 +10,11 @@ jobs: fail-fast: false matrix: php: [8.4, 8.3, 8.2, 8.1, 8.0] - laravel: ["^11.0", "^10.0", "^9.0", "^8.12"] + laravel: ["^12.0", "^11.0", "^10.0", "^9.0", "^8.12"] dependency-version: [prefer-lowest, prefer-stable] include: + - laravel: "^12.0" + testbench: 10.* - laravel: "^11.0" testbench: 9.* - laravel: "^10.0" @@ -22,6 +24,10 @@ jobs: - laravel: "^8.12" testbench: "^6.23" exclude: + - laravel: "^12.0" + php: 8.1 + - laravel: "^12.0" + php: 8.0 - laravel: "^11.0" php: 8.1 - laravel: "^11.0" @@ -52,7 +58,7 @@ jobs: - name: Install dependencies run: | - composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:>=4.3.4" "mockery/mockery:^1.3.2" "nesbot/carbon:>=2.62.1" --no-interaction --no-update + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:>=4.3.4" "mockery/mockery:^1.3.2" "nesbot/carbon:>=2.72.6" --no-interaction --no-update composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction - name: Execute tests From 9d9be0a0cd6f4d5df28dbf463e13d07ec310fb84 Mon Sep 17 00:00:00 2001 From: drbyte <404472+drbyte@users.noreply.github.com> Date: Fri, 31 Jan 2025 01:10:25 +0000 Subject: [PATCH 0934/1013] Update CHANGELOG --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62c8daaac..2771bf0b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.12.0 - 2025-01-31 + +### What's Changed + +* Support Laravel 12 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.11.0...6.12.0 + ## 6.11.0 - 2025-01-30 ### What's Changed @@ -924,6 +932,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` @@ -996,6 +1005,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` From 262b34104ae2c06d4817ff99b72bd83a7947aebe Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 30 Jan 2025 20:55:13 -0500 Subject: [PATCH 0935/1013] phpstan fixes Cherry-picked 0c5789d36315df41e1660dd42fbb0120f0798ec3 Co-authored-by: erikn69 --- src/PermissionServiceProvider.php | 3 +-- src/Traits/HasPermissions.php | 3 +-- src/Traits/HasRoles.php | 4 ---- tests/CommandTest.php | 4 ++-- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 31a029f48..96ae5abc6 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -144,8 +144,7 @@ protected function registerBladeExtensions(BladeCompiler $bladeCompiler): void protected function registerMacroHelpers(): void { - // @phpstan-ignore-next-line - if (! method_exists(Route::class, 'macro')) { // Lumen + if (! method_exists(Route::class, 'macro')) { // @phpstan-ignore-line Lumen return; } diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 8147cac60..cc69d096e 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -348,8 +348,7 @@ public function getAllPermissions(): Collection /** @var Collection $permissions */ $permissions = $this->permissions; - // @phpstan-ignore-next-line - if (method_exists($this, 'roles')) { + if (!is_a($this, Permission::class)) { $permissions = $permissions->merge($this->getPermissionsViaRoles()); } diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 4a332739d..409f1636c 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -127,10 +127,6 @@ private function collectRoles(...$roles): array } $role = $this->getStoredRole($role); - // @phpstan-ignore-next-line - if (! $role instanceof Role) { - return $array; - } if (! in_array($role->getKey(), $array)) { $this->ensureModelSharesGuard($role); diff --git a/tests/CommandTest.php b/tests/CommandTest.php index 4fbb6fa3a..c053416e9 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -200,7 +200,7 @@ public function it_can_respond_to_about_command_with_default() app(\Spatie\Permission\PermissionRegistrar::class)->initializeCache(); Artisan::call('about'); - $output = Artisan::output(); + $output = str_replace("\r\n", "\n", Artisan::output()); $pattern = '/Spatie Permissions[ .\n]*Features Enabled[ .]*Default[ .\n]*Version/'; if (method_exists($this, 'assertMatchesRegularExpression')) { @@ -225,7 +225,7 @@ public function it_can_respond_to_about_command_with_teams() config()->set('permission.teams', true); Artisan::call('about'); - $output = Artisan::output(); + $output = str_replace("\r\n", "\n", Artisan::output()); $pattern = '/Spatie Permissions[ .\n]*Features Enabled[ .]*Teams[ .\n]*Version/'; if (method_exists($this, 'assertMatchesRegularExpression')) { From 9895552dba4b4e98777bc4c28bca66b9473462d2 Mon Sep 17 00:00:00 2001 From: drbyte <404472+drbyte@users.noreply.github.com> Date: Fri, 31 Jan 2025 01:55:39 +0000 Subject: [PATCH 0936/1013] Fix styling --- src/Traits/HasPermissions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index cc69d096e..98214ca4e 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -348,7 +348,7 @@ public function getAllPermissions(): Collection /** @var Collection $permissions */ $permissions = $this->permissions; - if (!is_a($this, Permission::class)) { + if (! is_a($this, Permission::class)) { $permissions = $permissions->merge($this->getPermissionsViaRoles()); } From dd3ccf51f73e728f531886af59691ecc7254bbe8 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Fri, 31 Jan 2025 15:02:09 -0500 Subject: [PATCH 0937/1013] Explicitly call `loadMissing('permissions')` when the relation is needed, and test with `Model::preventLazyLoading()` (#2776) * call loadMissing('permissions') when relation is referenced * Test with `Model::preventLazyLoading()` * Model::preventsLazyLoading() check added --------- Co-authored-by: Chris Brown --- src/Models/Role.php | 3 ++- src/Traits/HasPermissions.php | 3 ++- tests/HasPermissionsTest.php | 34 ++++++++++++++++++++++++++++++++++ tests/HasRolesTest.php | 35 +++++++++++++++++++++++++++++++++++ tests/TestCase.php | 2 ++ 5 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/Models/Role.php b/src/Models/Role.php index 6bd47b75f..5bab4878e 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -188,6 +188,7 @@ public function hasPermissionTo($permission, ?string $guardName = null): bool throw GuardDoesNotMatch::create($permission->guard_name, $guardName ? collect([$guardName]) : $this->getGuardNames()); } - return $this->permissions->contains($permission->getKeyName(), $permission->getKey()); + return $this->loadMissing('permissions')->permissions + ->contains($permission->getKeyName(), $permission->getKey()); } } diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 98214ca4e..15f9e9971 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -323,7 +323,8 @@ public function hasDirectPermission($permission): bool { $permission = $this->filterPermission($permission); - return $this->permissions->contains($permission->getKeyName(), $permission->getKey()); + return $this->loadMissing('permissions')->permissions + ->contains($permission->getKeyName(), $permission->getKey()); } /** diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index e5657536f..4c2d410e4 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -2,6 +2,7 @@ namespace Spatie\Permission\Tests; +use Illuminate\Database\Eloquent\Model; use DB; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Contracts\Role; @@ -765,4 +766,37 @@ public function it_can_reject_permission_based_on_logged_in_user_guard() 'status' => false, ]); } + + /** @test */ + public function it_can_be_given_a_permission_on_role_when_lazy_loading_is_restricted() + { + $this->assertTrue(Model::preventsLazyLoading()); + + try { + $testRole = app(Role::class)->with('permissions')->get()->first(); + + $testRole->givePermissionTo('edit-articles'); + + $this->assertTrue($testRole->hasPermissionTo('edit-articles')); + } catch (Exception $e) { + $this->fail('Lazy loading detected in the givePermissionTo method: ' . $e->getMessage()); + } + } + + /** @test */ + public function it_can_be_given_a_permission_on_user_when_lazy_loading_is_restricted() + { + $this->assertTrue(Model::preventsLazyLoading()); + + try { + User::create(['email' => 'other@user.com']); + $testUser = User::with('permissions')->get()->first(); + + $testUser->givePermissionTo('edit-articles'); + + $this->assertTrue($testUser->hasPermissionTo('edit-articles')); + } catch (Exception $e) { + $this->fail('Lazy loading detected in the givePermissionTo method: ' . $e->getMessage()); + } + } } diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index c5f78169e..c2f8459f0 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -2,7 +2,9 @@ namespace Spatie\Permission\Tests; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\DB; +use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Contracts\Role; use Spatie\Permission\Exceptions\GuardDoesNotMatch; use Spatie\Permission\Exceptions\RoleDoesNotExist; @@ -856,4 +858,37 @@ public function it_does_not_detach_roles_when_user_soft_deleting() $this->assertTrue($user->hasRole('testRole')); } + + /** @test */ + public function it_can_be_given_a_role_on_permission_when_lazy_loading_is_restricted() + { + $this->assertTrue(Model::preventsLazyLoading()); + + try { + $testPermission = app(Permission::class)->with('roles')->get()->first(); + + $testPermission->assignRole('testRole'); + + $this->assertTrue($testPermission->hasRole('testRole')); + } catch (Exception $e) { + $this->fail('Lazy loading detected in the givePermissionTo method: ' . $e->getMessage()); + } + } + + /** @test */ + public function it_can_be_given_a_role_on_user_when_lazy_loading_is_restricted() + { + $this->assertTrue(Model::preventsLazyLoading()); + + try { + User::create(['email' => 'other@user.com']); + $user = User::with('roles')->get()->first(); + $user->assignRole('testRole'); + + $this->assertTrue($user->hasRole('testRole')); + } catch (Exception $e) { + $this->fail('Lazy loading detected in the givePermissionTo method: ' . $e->getMessage()); + } + } + } diff --git a/tests/TestCase.php b/tests/TestCase.php index b6ec94970..1b222ad01 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,6 +2,7 @@ namespace Spatie\Permission\Tests; +use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Schema\Blueprint; use Illuminate\Foundation\Console\AboutCommand; use Illuminate\Http\Request; @@ -110,6 +111,7 @@ protected function getPackageProviders($app) */ protected function getEnvironmentSetUp($app) { + Model::preventLazyLoading(); $app['config']->set('permission.register_permission_check_method', true); $app['config']->set('permission.teams', $this->hasTeams); $app['config']->set('permission.testing', true); // fix sqlite From 9d4fa2987f9b745854a13e1b36ba68e0b6dd4bdb Mon Sep 17 00:00:00 2001 From: drbyte <404472+drbyte@users.noreply.github.com> Date: Fri, 31 Jan 2025 20:02:32 +0000 Subject: [PATCH 0938/1013] Fix styling --- tests/HasPermissionsTest.php | 6 +++--- tests/HasRolesTest.php | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index 4c2d410e4..39d13fcec 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -2,8 +2,8 @@ namespace Spatie\Permission\Tests; -use Illuminate\Database\Eloquent\Model; use DB; +use Illuminate\Database\Eloquent\Model; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Contracts\Role; use Spatie\Permission\Exceptions\GuardDoesNotMatch; @@ -779,7 +779,7 @@ public function it_can_be_given_a_permission_on_role_when_lazy_loading_is_restri $this->assertTrue($testRole->hasPermissionTo('edit-articles')); } catch (Exception $e) { - $this->fail('Lazy loading detected in the givePermissionTo method: ' . $e->getMessage()); + $this->fail('Lazy loading detected in the givePermissionTo method: '.$e->getMessage()); } } @@ -796,7 +796,7 @@ public function it_can_be_given_a_permission_on_user_when_lazy_loading_is_restri $this->assertTrue($testUser->hasPermissionTo('edit-articles')); } catch (Exception $e) { - $this->fail('Lazy loading detected in the givePermissionTo method: ' . $e->getMessage()); + $this->fail('Lazy loading detected in the givePermissionTo method: '.$e->getMessage()); } } } diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index c2f8459f0..4aa7c9073 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -871,7 +871,7 @@ public function it_can_be_given_a_role_on_permission_when_lazy_loading_is_restri $this->assertTrue($testPermission->hasRole('testRole')); } catch (Exception $e) { - $this->fail('Lazy loading detected in the givePermissionTo method: ' . $e->getMessage()); + $this->fail('Lazy loading detected in the givePermissionTo method: '.$e->getMessage()); } } @@ -887,8 +887,7 @@ public function it_can_be_given_a_role_on_user_when_lazy_loading_is_restricted() $this->assertTrue($user->hasRole('testRole')); } catch (Exception $e) { - $this->fail('Lazy loading detected in the givePermissionTo method: ' . $e->getMessage()); + $this->fail('Lazy loading detected in the givePermissionTo method: '.$e->getMessage()); } } - } From bb3ad222d65ec804c219cc52a8b54f5af2e1636a Mon Sep 17 00:00:00 2001 From: Sudhir Mitharwal <6812992+sudkumar@users.noreply.github.com> Date: Wed, 5 Feb 2025 21:12:48 +0530 Subject: [PATCH 0939/1013] [Docs] Add instructions to reinitialize cache for multi-tenancy key settings (#2804) * docs(cache): add instructions for cache store change in multi-tenancy when switching cache prefix/key/store during a request lifecycle, after the service provider has been loaded, the cache store should be reinitialized to make sure that the updated CacheStore is used for upcoming cache hits. --------- Co-authored-by: Chris Brown --- docs/advanced-usage/cache.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/advanced-usage/cache.md b/docs/advanced-usage/cache.md index 6c3a3fe6a..374410af8 100644 --- a/docs/advanced-usage/cache.md +++ b/docs/advanced-usage/cache.md @@ -68,7 +68,13 @@ Laravel Tip: If you are leveraging a caching service such as `redis` or `memcach To prevent other applications from accidentally using/changing your cached data, it is prudent to set your own cache `prefix` in Laravel's `/config/cache.php` to something unique for each application which shares the same caching service. -Most multi-tenant "packages" take care of this for you when switching tenants. +Most multi-tenant "packages" take care of this for you when switching tenants. + +Tip: Most parts of your multitenancy app will relate to a single tenant during a given request lifecycle, so the following step will not be needed: However, in the less-common situation where your app might be switching between multiple tenants during a single request lifecycle (specifically: where changing the cache key/prefix (such as when switching between tenants) or switching the cache store), then after switching tenants or changing the cache configuration you will need to reinitialize the cache of the `PermissionRegistrar` so that the updated `CacheStore` and cache configuration are used. + +```php +app()->make(\Spatie\Permission\PermissionRegistrar::class)->initializeCache(); +``` ### Custom Cache Store From 0356f8de6001aa7e55d0dc1f3dff12ee3c0f69ca Mon Sep 17 00:00:00 2001 From: drbyte <404472+drbyte@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:45:58 +0000 Subject: [PATCH 0940/1013] Update CHANGELOG --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2771bf0b9..21c9539df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.13.0 - 2025-02-05 + +### What's Changed + +* LazyLoading: Explicitly call `loadMissing('permissions')` when the relation is needed, and test with `Model::preventLazyLoading()` by @erikn69 in https://github.com/spatie/laravel-permission/pull/2776 +* [Docs] Add instructions to reinitialize cache for multi-tenancy key settings when updating multiple tenants in a single request cycle, by @sudkumar in https://github.com/spatie/laravel-permission/pull/2804 + +### New Contributors + +* @sudkumar made their first contribution in https://github.com/spatie/laravel-permission/pull/2804 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.12.0...6.13.0 + ## 6.12.0 - 2025-01-31 ### What's Changed @@ -933,6 +946,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` @@ -1006,6 +1020,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` From a07467dcbd015977de9b765ab799c59bb4c90952 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 5 Feb 2025 17:02:05 -0500 Subject: [PATCH 0941/1013] Mention custom cache bootstrapper --- docs/advanced-usage/cache.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-usage/cache.md b/docs/advanced-usage/cache.md index 374410af8..5e76828ec 100644 --- a/docs/advanced-usage/cache.md +++ b/docs/advanced-usage/cache.md @@ -68,7 +68,7 @@ Laravel Tip: If you are leveraging a caching service such as `redis` or `memcach To prevent other applications from accidentally using/changing your cached data, it is prudent to set your own cache `prefix` in Laravel's `/config/cache.php` to something unique for each application which shares the same caching service. -Most multi-tenant "packages" take care of this for you when switching tenants. +Most multi-tenant "packages" take care of this for you when switching tenants. Optionally you might need to change cache boot order by writing a custom [cache boostrapper](https://github.com/spatie/laravel-permission/discussions/2310#discussioncomment-10855389). Tip: Most parts of your multitenancy app will relate to a single tenant during a given request lifecycle, so the following step will not be needed: However, in the less-common situation where your app might be switching between multiple tenants during a single request lifecycle (specifically: where changing the cache key/prefix (such as when switching between tenants) or switching the cache store), then after switching tenants or changing the cache configuration you will need to reinitialize the cache of the `PermissionRegistrar` so that the updated `CacheStore` and cache configuration are used. From aa3548f9e1c4b27827b7a0c3780a9ffd76811df7 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 6 Feb 2025 14:30:23 -0500 Subject: [PATCH 0942/1013] Add note about avoiding $casts property with enums. --- docs/basic-usage/enums.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/basic-usage/enums.md b/docs/basic-usage/enums.md index 493c9380b..23a4167f4 100644 --- a/docs/basic-usage/enums.md +++ b/docs/basic-usage/enums.md @@ -13,6 +13,8 @@ If you are using PHP 8.1+ you can implement Enums as native types. Internally, Enums implicitly implement `\BackedEnum`, which is how this package recognizes that you're passing an Enum. +NOTE: Presently (version 6) this package does not support using `$casts` to specify enums on the `Permission` model. You can still use enums to reference things as shown below, just without declaring it in a `$casts` property. + ## Code Requirements From a6e35eee62ebd10da75df2c7220941797b5880bd Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 12 Feb 2025 20:18:29 -0500 Subject: [PATCH 0943/1013] Formatting --- .../create_permission_tables.php.stub | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/database/migrations/create_permission_tables.php.stub b/database/migrations/create_permission_tables.php.stub index 9c7044b46..70a120f30 100644 --- a/database/migrations/create_permission_tables.php.stub +++ b/database/migrations/create_permission_tables.php.stub @@ -1,8 +1,8 @@ engine('InnoDB'); + Schema::create($tableNames['permissions'], static function (Blueprint $table) { + // $table->engine('InnoDB'); $table->bigIncrements('id'); // permission id $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format) $table->string('guard_name'); // For MyISAM use string('guard_name', 25); @@ -34,8 +34,8 @@ return new class extends Migration $table->unique(['name', 'guard_name']); }); - Schema::create($tableNames['roles'], function (Blueprint $table) use ($teams, $columnNames) { - //$table->engine('InnoDB'); + Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) { + // $table->engine('InnoDB'); $table->bigIncrements('id'); // role id if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable(); @@ -51,7 +51,7 @@ return new class extends Migration } }); - Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) { + Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) { $table->unsignedBigInteger($pivotPermission); $table->string('model_type'); @@ -75,7 +75,7 @@ return new class extends Migration }); - Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) { + Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) { $table->unsignedBigInteger($pivotRole); $table->string('model_type'); @@ -98,7 +98,7 @@ return new class extends Migration } }); - Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) { + Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) { $table->unsignedBigInteger($pivotPermission); $table->unsignedBigInteger($pivotRole); From 85da60957d45c4224837e603d6931e004dc62496 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 12 Feb 2025 20:22:12 -0500 Subject: [PATCH 0944/1013] Add PHPUnit annotations, for future compatibility with PHPUnit 12 Annotations supported since PHPUnit 10 Docblock comments will be ignored in future In a later release of this package (probably `v7.0`), will remove PHPUnit 9 support and all associated doc-comments. --- tests/BladeTest.php | 26 +++++++++ tests/CacheTest.php | 17 ++++++ tests/CommandTest.php | 14 +++++ tests/CustomGateTest.php | 3 + tests/GateTest.php | 10 ++++ tests/HasPermissionsTest.php | 59 +++++++++++++++++++ tests/HasPermissionsWithCustomModelsTest.php | 9 +++ tests/HasRolesTest.php | 60 ++++++++++++++++++++ tests/HasRolesWithCustomModelsTest.php | 6 ++ tests/MultipleGuardsTest.php | 4 ++ tests/PermissionMiddlewareTest.php | 23 ++++++++ tests/PermissionRegistrarTest.php | 8 +++ tests/PermissionTest.php | 8 +++ tests/PolicyTest.php | 2 + tests/RoleMiddlewareTest.php | 22 +++++++ tests/RoleOrPermissionMiddlewareTest.php | 15 +++++ tests/RoleTest.php | 25 ++++++++ tests/RoleWithNestingTest.php | 4 ++ tests/RouteTest.php | 5 ++ tests/TeamHasPermissionsTest.php | 5 ++ tests/TeamHasRolesTest.php | 5 ++ tests/TestCase.php | 4 +- tests/TestModels/Client.php | 4 +- tests/TestModels/Manager.php | 4 +- tests/TestModels/Permission.php | 9 +-- tests/TestModels/Role.php | 25 +++----- tests/TestModels/SoftDeletingUser.php | 2 +- tests/TestModels/TestRolePermissionsEnum.php | 4 ++ tests/TestModels/WildcardPermission.php | 4 +- tests/WildcardHasPermissionsTest.php | 18 ++++++ tests/WildcardMiddlewareTest.php | 8 +++ tests/WildcardRoleTest.php | 10 ++++ tests/WildcardRouteTest.php | 4 ++ 33 files changed, 396 insertions(+), 30 deletions(-) diff --git a/tests/BladeTest.php b/tests/BladeTest.php index 7e19ec81f..aea3ca2f8 100644 --- a/tests/BladeTest.php +++ b/tests/BladeTest.php @@ -3,6 +3,7 @@ namespace Spatie\Permission\Tests; use Illuminate\Support\Facades\Artisan; +use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Contracts\Role; class BladeTest extends TestCase @@ -21,6 +22,7 @@ protected function setUp(): void } /** @test */ + #[Test] public function all_blade_directives_will_evaluate_false_when_there_is_nobody_logged_in() { $permission = 'edit-articles'; @@ -40,6 +42,7 @@ public function all_blade_directives_will_evaluate_false_when_there_is_nobody_lo } /** @test */ + #[Test] public function all_blade_directives_will_evaluate_false_when_somebody_without_roles_or_permissions_is_logged_in() { $permission = 'edit-articles'; @@ -59,6 +62,7 @@ public function all_blade_directives_will_evaluate_false_when_somebody_without_r } /** @test */ + #[Test] public function all_blade_directives_will_evaluate_false_when_somebody_with_another_guard_is_logged_in() { $permission = 'edit-articles'; @@ -79,6 +83,7 @@ public function all_blade_directives_will_evaluate_false_when_somebody_with_anot } /** @test */ + #[Test] public function the_can_directive_can_accept_a_guard_name() { $user = $this->getWriter(); @@ -109,6 +114,7 @@ public function the_can_directive_can_accept_a_guard_name() } /** @test */ + #[Test] public function the_can_directive_will_evaluate_true_when_the_logged_in_user_has_the_permission() { $user = $this->getWriter(); @@ -121,6 +127,7 @@ public function the_can_directive_will_evaluate_true_when_the_logged_in_user_has } /** @test */ + #[Test] public function the_haspermission_directive_will_evaluate_true_when_the_logged_in_user_has_the_permission() { $user = $this->getWriter(); @@ -145,6 +152,7 @@ public function the_haspermission_directive_will_evaluate_true_when_the_logged_i } /** @test */ + #[Test] public function the_role_directive_will_evaluate_true_when_the_logged_in_user_has_the_role() { auth()->setUser($this->getWriter()); @@ -153,6 +161,7 @@ public function the_role_directive_will_evaluate_true_when_the_logged_in_user_ha } /** @test */ + #[Test] public function the_elserole_directive_will_evaluate_true_when_the_logged_in_user_has_the_role() { auth()->setUser($this->getMember()); @@ -161,6 +170,7 @@ public function the_elserole_directive_will_evaluate_true_when_the_logged_in_use } /** @test */ + #[Test] public function the_role_directive_will_evaluate_true_when_the_logged_in_user_has_the_role_for_the_given_guard() { auth('admin')->setUser($this->getSuperAdmin()); @@ -169,6 +179,7 @@ public function the_role_directive_will_evaluate_true_when_the_logged_in_user_ha } /** @test */ + #[Test] public function the_hasrole_directive_will_evaluate_true_when_the_logged_in_user_has_the_role() { auth()->setUser($this->getWriter()); @@ -177,6 +188,7 @@ public function the_hasrole_directive_will_evaluate_true_when_the_logged_in_user } /** @test */ + #[Test] public function the_hasrole_directive_will_evaluate_true_when_the_logged_in_user_has_the_role_for_the_given_guard() { auth('admin')->setUser($this->getSuperAdmin()); @@ -185,6 +197,7 @@ public function the_hasrole_directive_will_evaluate_true_when_the_logged_in_user } /** @test */ + #[Test] public function the_unlessrole_directive_will_evaluate_true_when_the_logged_in_user_does_not_have_the_role() { auth()->setUser($this->getWriter()); @@ -193,6 +206,7 @@ public function the_unlessrole_directive_will_evaluate_true_when_the_logged_in_u } /** @test */ + #[Test] public function the_unlessrole_directive_will_evaluate_true_when_the_logged_in_user_does_not_have_the_role_for_the_given_guard() { auth('admin')->setUser($this->getSuperAdmin()); @@ -202,6 +216,7 @@ public function the_unlessrole_directive_will_evaluate_true_when_the_logged_in_u } /** @test */ + #[Test] public function the_hasanyrole_directive_will_evaluate_false_when_the_logged_in_user_does_not_have_any_of_the_required_roles() { $roles = ['writer', 'intern']; @@ -213,6 +228,7 @@ public function the_hasanyrole_directive_will_evaluate_false_when_the_logged_in_ } /** @test */ + #[Test] public function the_hasanyrole_directive_will_evaluate_true_when_the_logged_in_user_does_have_some_of_the_required_roles() { $roles = ['member', 'writer', 'intern']; @@ -224,6 +240,7 @@ public function the_hasanyrole_directive_will_evaluate_true_when_the_logged_in_u } /** @test */ + #[Test] public function the_hasanyrole_directive_will_evaluate_true_when_the_logged_in_user_does_have_some_of_the_required_roles_for_the_given_guard() { $roles = ['super-admin', 'moderator']; @@ -235,6 +252,7 @@ public function the_hasanyrole_directive_will_evaluate_true_when_the_logged_in_u } /** @test */ + #[Test] public function the_hasanyrole_directive_will_evaluate_true_when_the_logged_in_user_does_have_some_of_the_required_roles_in_pipe() { $guard = 'admin'; @@ -245,6 +263,7 @@ public function the_hasanyrole_directive_will_evaluate_true_when_the_logged_in_u } /** @test */ + #[Test] public function the_hasanyrole_directive_will_evaluate_false_when_the_logged_in_user_doesnt_have_some_of_the_required_roles_in_pipe() { $guard = ''; @@ -255,6 +274,7 @@ public function the_hasanyrole_directive_will_evaluate_false_when_the_logged_in_ } /** @test */ + #[Test] public function the_hasallroles_directive_will_evaluate_false_when_the_logged_in_user_does_not_have_all_required_roles() { $roles = ['member', 'writer']; @@ -266,6 +286,7 @@ public function the_hasallroles_directive_will_evaluate_false_when_the_logged_in } /** @test */ + #[Test] public function the_hasallroles_directive_will_evaluate_true_when_the_logged_in_user_does_have_all_required_roles() { $roles = ['member', 'writer']; @@ -281,6 +302,7 @@ public function the_hasallroles_directive_will_evaluate_true_when_the_logged_in_ } /** @test */ + #[Test] public function the_hasallroles_directive_will_evaluate_true_when_the_logged_in_user_does_have_all_required_roles_for_the_given_guard() { $roles = ['super-admin', 'moderator']; @@ -296,6 +318,7 @@ public function the_hasallroles_directive_will_evaluate_true_when_the_logged_in_ } /** @test */ + #[Test] public function the_hasallroles_directive_will_evaluate_true_when_the_logged_in_user_does_have_all_required_roles_in_pipe() { $guard = 'admin'; @@ -310,6 +333,7 @@ public function the_hasallroles_directive_will_evaluate_true_when_the_logged_in_ } /** @test */ + #[Test] public function the_hasallroles_directive_will_evaluate_false_when_the_logged_in_user_doesnt_have_all_required_roles_in_pipe() { $guard = ''; @@ -323,6 +347,7 @@ public function the_hasallroles_directive_will_evaluate_false_when_the_logged_in } /** @test */ + #[Test] public function the_hasallroles_directive_will_evaluate_true_when_the_logged_in_user_does_have_all_required_roles_in_array() { $guard = 'admin'; @@ -337,6 +362,7 @@ public function the_hasallroles_directive_will_evaluate_true_when_the_logged_in_ } /** @test */ + #[Test] public function the_hasallroles_directive_will_evaluate_false_when_the_logged_in_user_doesnt_have_all_required_roles_in_array() { $guard = ''; diff --git a/tests/CacheTest.php b/tests/CacheTest.php index ba8ca6a1e..be5510fe9 100644 --- a/tests/CacheTest.php +++ b/tests/CacheTest.php @@ -4,6 +4,7 @@ use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; +use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Contracts\Role; use Spatie\Permission\Exceptions\PermissionDoesNotExist; @@ -42,6 +43,7 @@ protected function setUp(): void } /** @test */ + #[Test] public function it_can_cache_the_permissions() { $this->resetQueryCount(); @@ -52,6 +54,7 @@ public function it_can_cache_the_permissions() } /** @test */ + #[Test] public function it_flushes_the_cache_when_creating_a_permission() { app(Permission::class)->create(['name' => 'new']); @@ -64,6 +67,7 @@ public function it_flushes_the_cache_when_creating_a_permission() } /** @test */ + #[Test] public function it_flushes_the_cache_when_updating_a_permission() { $permission = app(Permission::class)->create(['name' => 'new']); @@ -79,6 +83,7 @@ public function it_flushes_the_cache_when_updating_a_permission() } /** @test */ + #[Test] public function it_flushes_the_cache_when_creating_a_role() { app(Role::class)->create(['name' => 'new']); @@ -91,6 +96,7 @@ public function it_flushes_the_cache_when_creating_a_role() } /** @test */ + #[Test] public function it_flushes_the_cache_when_updating_a_role() { $role = app(Role::class)->create(['name' => 'new']); @@ -106,6 +112,7 @@ public function it_flushes_the_cache_when_updating_a_role() } /** @test */ + #[Test] public function removing_a_permission_from_a_user_should_not_flush_the_cache() { $this->testUser->givePermissionTo('edit-articles'); @@ -122,6 +129,7 @@ public function removing_a_permission_from_a_user_should_not_flush_the_cache() } /** @test */ + #[Test] public function removing_a_role_from_a_user_should_not_flush_the_cache() { $this->testUser->assignRole('testRole'); @@ -138,6 +146,7 @@ public function removing_a_role_from_a_user_should_not_flush_the_cache() } /** @test */ + #[Test] public function it_flushes_the_cache_when_removing_a_role_from_a_permission() { $this->testUserPermission->assignRole('testRole'); @@ -154,6 +163,7 @@ public function it_flushes_the_cache_when_removing_a_role_from_a_permission() } /** @test */ + #[Test] public function it_flushes_the_cache_when_assign_a_permission_to_a_role() { $this->testUserRole->givePermissionTo('edit-articles'); @@ -166,6 +176,7 @@ public function it_flushes_the_cache_when_assign_a_permission_to_a_role() } /** @test */ + #[Test] public function user_creation_should_not_flush_the_cache() { $this->registrar->getPermissions(); @@ -181,6 +192,7 @@ public function user_creation_should_not_flush_the_cache() } /** @test */ + #[Test] public function it_flushes_the_cache_when_giving_a_permission_to_a_role() { $this->testUserRole->givePermissionTo($this->testUserPermission); @@ -193,6 +205,7 @@ public function it_flushes_the_cache_when_giving_a_permission_to_a_role() } /** @test */ + #[Test] public function has_permission_to_should_use_the_cache() { $this->testUserRole->givePermissionTo(['edit-articles', 'edit-news', 'Edit News']); @@ -217,6 +230,7 @@ public function has_permission_to_should_use_the_cache() } /** @test */ + #[Test] public function the_cache_should_differentiate_by_guard_name() { $this->expectException(PermissionDoesNotExist::class); @@ -235,6 +249,7 @@ public function the_cache_should_differentiate_by_guard_name() } /** @test */ + #[Test] public function get_all_permissions_should_use_the_cache() { $this->testUserRole->givePermissionTo($expected = ['edit-articles', 'edit-news']); @@ -253,6 +268,7 @@ public function get_all_permissions_should_use_the_cache() } /** @test */ + #[Test] public function get_all_permissions_should_not_over_hydrate_roles() { $this->testUserRole->givePermissionTo(['edit-articles', 'edit-news']); @@ -264,6 +280,7 @@ public function get_all_permissions_should_not_over_hydrate_roles() } /** @test */ + #[Test] public function it_can_reset_the_cache_with_artisan_command() { Artisan::call('permission:create-permission', ['name' => 'new-permission']); diff --git a/tests/CommandTest.php b/tests/CommandTest.php index c053416e9..e1961d812 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -5,12 +5,14 @@ use Composer\InstalledVersions; use Illuminate\Foundation\Console\AboutCommand; use Illuminate\Support\Facades\Artisan; +use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Models\Permission; use Spatie\Permission\Models\Role; class CommandTest extends TestCase { /** @test */ + #[Test] public function it_can_create_a_role() { Artisan::call('permission:create-role', ['name' => 'new-role']); @@ -20,6 +22,7 @@ public function it_can_create_a_role() } /** @test */ + #[Test] public function it_can_create_a_role_with_a_specific_guard() { Artisan::call('permission:create-role', [ @@ -33,6 +36,7 @@ public function it_can_create_a_role_with_a_specific_guard() } /** @test */ + #[Test] public function it_can_create_a_permission() { Artisan::call('permission:create-permission', ['name' => 'new-permission']); @@ -41,6 +45,7 @@ public function it_can_create_a_permission() } /** @test */ + #[Test] public function it_can_create_a_permission_with_a_specific_guard() { Artisan::call('permission:create-permission', [ @@ -54,6 +59,7 @@ public function it_can_create_a_permission_with_a_specific_guard() } /** @test */ + #[Test] public function it_can_create_a_role_and_permissions_at_same_time() { Artisan::call('permission:create-role', [ @@ -68,6 +74,7 @@ public function it_can_create_a_role_and_permissions_at_same_time() } /** @test */ + #[Test] public function it_can_create_a_role_without_duplication() { Artisan::call('permission:create-role', ['name' => 'new-role']); @@ -78,6 +85,7 @@ public function it_can_create_a_role_without_duplication() } /** @test */ + #[Test] public function it_can_create_a_permission_without_duplication() { Artisan::call('permission:create-permission', ['name' => 'new-permission']); @@ -87,6 +95,7 @@ public function it_can_create_a_permission_without_duplication() } /** @test */ + #[Test] public function it_can_show_permission_tables() { Role::where('name', 'testRole2')->delete(); @@ -125,6 +134,7 @@ public function it_can_show_permission_tables() } /** @test */ + #[Test] public function it_can_show_permissions_for_guard() { Artisan::call('permission:show', ['guard' => 'web']); @@ -136,6 +146,7 @@ public function it_can_show_permissions_for_guard() } /** @test */ + #[Test] public function it_can_setup_teams_upgrade() { config()->set('permission.teams', true); @@ -163,6 +174,7 @@ public function it_can_setup_teams_upgrade() } /** @test */ + #[Test] public function it_can_show_roles_by_teams() { config()->set('permission.teams', true); @@ -188,6 +200,7 @@ public function it_can_show_roles_by_teams() } /** @test */ + #[Test] public function it_can_respond_to_about_command_with_default() { if (! class_exists(InstalledVersions::class) || ! class_exists(AboutCommand::class)) { @@ -211,6 +224,7 @@ public function it_can_respond_to_about_command_with_default() } /** @test */ + #[Test] public function it_can_respond_to_about_command_with_teams() { if (! class_exists(InstalledVersions::class) || ! class_exists(AboutCommand::class)) { diff --git a/tests/CustomGateTest.php b/tests/CustomGateTest.php index 2c7e1fe17..498291ee8 100644 --- a/tests/CustomGateTest.php +++ b/tests/CustomGateTest.php @@ -3,6 +3,7 @@ namespace Spatie\Permission\Tests; use Illuminate\Contracts\Auth\Access\Gate; +use PHPUnit\Framework\Attributes\Test; class CustomGateTest extends TestCase { @@ -14,6 +15,7 @@ protected function getEnvironmentSetUp($app) } /** @test */ + #[Test] public function it_doesnt_register_the_method_for_checking_permissions_on_the_gate() { $this->testUser->givePermissionTo('edit-articles'); @@ -23,6 +25,7 @@ public function it_doesnt_register_the_method_for_checking_permissions_on_the_ga } /** @test */ + #[Test] public function it_can_authorize_using_custom_method_for_checking_permissions() { app(Gate::class)->define('edit-articles', function () { diff --git a/tests/GateTest.php b/tests/GateTest.php index 436900b84..6ec0fdcb3 100644 --- a/tests/GateTest.php +++ b/tests/GateTest.php @@ -3,17 +3,21 @@ namespace Spatie\Permission\Tests; use Illuminate\Contracts\Auth\Access\Gate; +use PHPUnit\Framework\Attributes\RequiresPhp; +use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Contracts\Permission; class GateTest extends TestCase { /** @test */ + #[Test] public function it_can_determine_if_a_user_does_not_have_a_permission() { $this->assertFalse($this->testUser->can('edit-articles')); } /** @test */ + #[Test] public function it_allows_other_gate_before_callbacks_to_run_if_a_user_does_not_have_a_permission() { $this->assertFalse($this->testUser->can('edit-articles')); @@ -27,6 +31,7 @@ public function it_allows_other_gate_before_callbacks_to_run_if_a_user_does_not_ } /** @test */ + #[Test] public function it_allows_gate_after_callback_to_grant_denied_privileges() { $this->assertFalse($this->testUser->can('edit-articles')); @@ -39,6 +44,7 @@ public function it_allows_gate_after_callback_to_grant_denied_privileges() } /** @test */ + #[Test] public function it_can_determine_if_a_user_has_a_direct_permission() { $this->testUser->givePermissionTo('edit-articles'); @@ -55,6 +61,8 @@ public function it_can_determine_if_a_user_has_a_direct_permission() * * @requires PHP >= 8.1 */ + #[RequiresPhp('>= 8.1')] + #[Test] public function it_can_determine_if_a_user_has_a_direct_permission_using_enums() { $enum = TestModels\TestRolePermissionsEnum::VIEWARTICLES; @@ -73,6 +81,7 @@ public function it_can_determine_if_a_user_has_a_direct_permission_using_enums() } /** @test */ + #[Test] public function it_can_determine_if_a_user_has_a_permission_through_roles() { $this->testUserRole->givePermissionTo($this->testUserPermission); @@ -89,6 +98,7 @@ public function it_can_determine_if_a_user_has_a_permission_through_roles() } /** @test */ + #[Test] public function it_can_determine_if_a_user_with_a_different_guard_has_a_permission_when_using_roles() { $this->testAdminRole->givePermissionTo($this->testAdminPermission); diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index 39d13fcec..0e2de3aa7 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -4,6 +4,8 @@ use DB; use Illuminate\Database\Eloquent\Model; +use PHPUnit\Framework\Attributes\RequiresPhp; +use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Contracts\Role; use Spatie\Permission\Exceptions\GuardDoesNotMatch; @@ -14,6 +16,7 @@ class HasPermissionsTest extends TestCase { /** @test */ + #[Test] public function it_can_assign_a_permission_to_a_user() { $this->testUser->givePermissionTo($this->testUserPermission); @@ -22,6 +25,7 @@ public function it_can_assign_a_permission_to_a_user() } /** @test */ + #[Test] public function it_can_assign_a_permission_to_a_user_with_a_non_default_guard() { $testUserPermission = app(Permission::class)->create([ @@ -35,6 +39,7 @@ public function it_can_assign_a_permission_to_a_user_with_a_non_default_guard() } /** @test */ + #[Test] public function it_throws_an_exception_when_assigning_a_permission_that_does_not_exist() { $this->expectException(PermissionDoesNotExist::class); @@ -43,6 +48,7 @@ public function it_throws_an_exception_when_assigning_a_permission_that_does_not } /** @test */ + #[Test] public function it_throws_an_exception_when_assigning_a_permission_to_a_user_from_a_different_guard() { $this->expectException(GuardDoesNotMatch::class); @@ -55,6 +61,7 @@ public function it_throws_an_exception_when_assigning_a_permission_to_a_user_fro } /** @test */ + #[Test] public function it_can_revoke_a_permission_from_a_user() { $this->testUser->givePermissionTo($this->testUserPermission); @@ -71,6 +78,8 @@ public function it_can_revoke_a_permission_from_a_user() * * @requires PHP >= 8.1 */ + #[RequiresPhp('>= 8.1')] + #[Test] public function it_can_assign_and_remove_a_permission_using_enums() { $enum = TestModels\TestRolePermissionsEnum::VIEWARTICLES; @@ -95,6 +104,8 @@ public function it_can_assign_and_remove_a_permission_using_enums() * * @requires PHP >= 8.1 */ + #[RequiresPhp('>= 8.1')] + #[Test] public function it_can_scope_users_using_enums() { $enum1 = TestModels\TestRolePermissionsEnum::VIEWARTICLES; @@ -122,6 +133,7 @@ public function it_can_scope_users_using_enums() } /** @test */ + #[Test] public function it_can_scope_users_using_a_string() { User::all()->each(fn ($item) => $item->delete()); @@ -142,6 +154,7 @@ public function it_can_scope_users_using_a_string() } /** @test */ + #[Test] public function it_can_scope_users_using_a_int() { User::all()->each(fn ($item) => $item->delete()); @@ -162,6 +175,7 @@ public function it_can_scope_users_using_a_int() } /** @test */ + #[Test] public function it_can_scope_users_using_an_array() { User::all()->each(fn ($item) => $item->delete()); @@ -183,6 +197,7 @@ public function it_can_scope_users_using_an_array() } /** @test */ + #[Test] public function it_can_scope_users_using_a_collection() { User::all()->each(fn ($item) => $item->delete()); @@ -204,6 +219,7 @@ public function it_can_scope_users_using_a_collection() } /** @test */ + #[Test] public function it_can_scope_users_using_an_object() { User::all()->each(fn ($item) => $item->delete()); @@ -222,6 +238,7 @@ public function it_can_scope_users_using_an_object() } /** @test */ + #[Test] public function it_can_scope_users_without_direct_permissions_only_role() { User::all()->each(fn ($item) => $item->delete()); @@ -241,6 +258,7 @@ public function it_can_scope_users_without_direct_permissions_only_role() } /** @test */ + #[Test] public function it_can_scope_users_with_only_direct_permission() { User::all()->each(fn ($item) => $item->delete()); @@ -258,6 +276,7 @@ public function it_can_scope_users_with_only_direct_permission() } /** @test */ + #[Test] public function it_throws_an_exception_when_calling_hasPermissionTo_with_an_invalid_type() { $user = User::create(['email' => 'user1@test.com']); @@ -268,6 +287,7 @@ public function it_throws_an_exception_when_calling_hasPermissionTo_with_an_inva } /** @test */ + #[Test] public function it_throws_an_exception_when_calling_hasPermissionTo_with_null() { $user = User::create(['email' => 'user1@test.com']); @@ -278,6 +298,7 @@ public function it_throws_an_exception_when_calling_hasPermissionTo_with_null() } /** @test */ + #[Test] public function it_throws_an_exception_when_calling_hasDirectPermission_with_an_invalid_type() { $user = User::create(['email' => 'user1@test.com']); @@ -288,6 +309,7 @@ public function it_throws_an_exception_when_calling_hasDirectPermission_with_an_ } /** @test */ + #[Test] public function it_throws_an_exception_when_calling_hasDirectPermission_with_null() { $user = User::create(['email' => 'user1@test.com']); @@ -298,6 +320,7 @@ public function it_throws_an_exception_when_calling_hasDirectPermission_with_nul } /** @test */ + #[Test] public function it_throws_an_exception_when_trying_to_scope_a_non_existing_permission() { $this->expectException(PermissionDoesNotExist::class); @@ -310,6 +333,7 @@ public function it_throws_an_exception_when_trying_to_scope_a_non_existing_permi } /** @test */ + #[Test] public function it_throws_an_exception_when_trying_to_scope_a_permission_from_another_guard() { $this->expectException(PermissionDoesNotExist::class); @@ -330,6 +354,7 @@ public function it_throws_an_exception_when_trying_to_scope_a_permission_from_an } /** @test */ + #[Test] public function it_doesnt_detach_permissions_when_user_soft_deleting() { $user = SoftDeletingUser::create(['email' => 'test@example.com']); @@ -342,6 +367,7 @@ public function it_doesnt_detach_permissions_when_user_soft_deleting() } /** @test */ + #[Test] public function it_can_give_and_revoke_multiple_permissions() { $this->testUserRole->givePermissionTo(['edit-articles', 'edit-news']); @@ -354,6 +380,7 @@ public function it_can_give_and_revoke_multiple_permissions() } /** @test */ + #[Test] public function it_can_give_and_revoke_permissions_models_array() { $models = [app(Permission::class)::where('name', 'edit-articles')->first(), app(Permission::class)::where('name', 'edit-news')->first()]; @@ -368,6 +395,7 @@ public function it_can_give_and_revoke_permissions_models_array() } /** @test */ + #[Test] public function it_can_give_and_revoke_permissions_models_collection() { $models = app(Permission::class)::whereIn('name', ['edit-articles', 'edit-news'])->get(); @@ -382,12 +410,14 @@ public function it_can_give_and_revoke_permissions_models_collection() } /** @test */ + #[Test] public function it_can_determine_that_the_user_does_not_have_a_permission() { $this->assertFalse($this->testUser->hasPermissionTo('edit-articles')); } /** @test */ + #[Test] public function it_throws_an_exception_when_the_permission_does_not_exist() { $this->expectException(PermissionDoesNotExist::class); @@ -396,6 +426,7 @@ public function it_throws_an_exception_when_the_permission_does_not_exist() } /** @test */ + #[Test] public function it_throws_an_exception_when_the_permission_does_not_exist_for_this_guard() { $this->expectException(PermissionDoesNotExist::class); @@ -404,6 +435,7 @@ public function it_throws_an_exception_when_the_permission_does_not_exist_for_th } /** @test */ + #[Test] public function it_can_reject_a_user_that_does_not_have_any_permissions_at_all() { $user = new User; @@ -412,6 +444,7 @@ public function it_can_reject_a_user_that_does_not_have_any_permissions_at_all() } /** @test */ + #[Test] public function it_can_determine_that_the_user_has_any_of_the_permissions_directly() { $this->assertFalse($this->testUser->hasAnyPermission('edit-articles')); @@ -429,6 +462,7 @@ public function it_can_determine_that_the_user_has_any_of_the_permissions_direct } /** @test */ + #[Test] public function it_can_determine_that_the_user_has_any_of_the_permissions_directly_using_an_array() { $this->assertFalse($this->testUser->hasAnyPermission(['edit-articles'])); @@ -445,6 +479,7 @@ public function it_can_determine_that_the_user_has_any_of_the_permissions_direct } /** @test */ + #[Test] public function it_can_determine_that_the_user_has_any_of_the_permissions_via_role() { $this->testUserRole->givePermissionTo('edit-articles'); @@ -456,6 +491,7 @@ public function it_can_determine_that_the_user_has_any_of_the_permissions_via_ro } /** @test */ + #[Test] public function it_can_determine_that_the_user_has_all_of_the_permissions_directly() { $this->testUser->givePermissionTo('edit-articles', 'edit-news'); @@ -469,6 +505,7 @@ public function it_can_determine_that_the_user_has_all_of_the_permissions_direct } /** @test */ + #[Test] public function it_can_determine_that_the_user_has_all_of_the_permissions_directly_using_an_array() { $this->assertFalse($this->testUser->hasAllPermissions(['edit-articles', 'edit-news'])); @@ -485,6 +522,7 @@ public function it_can_determine_that_the_user_has_all_of_the_permissions_direct } /** @test */ + #[Test] public function it_can_determine_that_the_user_has_all_of_the_permissions_via_role() { $this->testUserRole->givePermissionTo('edit-articles', 'edit-news'); @@ -495,6 +533,7 @@ public function it_can_determine_that_the_user_has_all_of_the_permissions_via_ro } /** @test */ + #[Test] public function it_can_determine_that_user_has_direct_permission() { $this->testUser->givePermissionTo('edit-articles'); @@ -513,6 +552,7 @@ public function it_can_determine_that_user_has_direct_permission() } /** @test */ + #[Test] public function it_can_list_all_the_permissions_via_roles_of_user() { $roleModel = app(Role::class); @@ -528,6 +568,7 @@ public function it_can_list_all_the_permissions_via_roles_of_user() } /** @test */ + #[Test] public function it_can_list_all_the_coupled_permissions_both_directly_and_via_roles() { $this->testUser->givePermissionTo('edit-news'); @@ -542,6 +583,7 @@ public function it_can_list_all_the_coupled_permissions_both_directly_and_via_ro } /** @test */ + #[Test] public function it_can_sync_multiple_permissions() { $this->testUser->givePermissionTo('edit-news'); @@ -556,6 +598,7 @@ public function it_can_sync_multiple_permissions() } /** @test */ + #[Test] public function it_can_avoid_sync_duplicated_permissions() { $this->testUser->syncPermissions('edit-articles', 'edit-blog', 'edit-blog'); @@ -566,6 +609,7 @@ public function it_can_avoid_sync_duplicated_permissions() } /** @test */ + #[Test] public function it_can_sync_multiple_permissions_by_id() { $this->testUser->givePermissionTo('edit-news'); @@ -582,6 +626,7 @@ public function it_can_sync_multiple_permissions_by_id() } /** @test */ + #[Test] public function sync_permission_ignores_null_inputs() { $this->testUser->givePermissionTo('edit-news'); @@ -600,6 +645,7 @@ public function sync_permission_ignores_null_inputs() } /** @test */ + #[Test] public function sync_permission_error_does_not_detach_permissions() { $this->testUser->givePermissionTo('edit-news'); @@ -612,6 +658,7 @@ public function sync_permission_error_does_not_detach_permissions() } /** @test */ + #[Test] public function it_does_not_remove_already_associated_permissions_when_assigning_new_permissions() { $this->testUser->givePermissionTo('edit-news'); @@ -622,6 +669,7 @@ public function it_does_not_remove_already_associated_permissions_when_assigning } /** @test */ + #[Test] public function it_does_not_throw_an_exception_when_assigning_a_permission_that_is_already_assigned() { $this->testUser->givePermissionTo('edit-news'); @@ -632,6 +680,7 @@ public function it_does_not_throw_an_exception_when_assigning_a_permission_that_ } /** @test */ + #[Test] public function it_can_sync_permissions_to_a_model_that_is_not_persisted() { $user = new User(['email' => 'test@user.com']); @@ -647,6 +696,7 @@ public function it_can_sync_permissions_to_a_model_that_is_not_persisted() } /** @test */ + #[Test] public function it_does_not_run_unnecessary_sqls_when_assigning_new_permissions() { $permission2 = app(Permission::class)->where('name', ['edit-news'])->first(); @@ -659,6 +709,7 @@ public function it_does_not_run_unnecessary_sqls_when_assigning_new_permissions( } /** @test */ + #[Test] public function calling_givePermissionTo_before_saving_object_doesnt_interfere_with_other_objects() { $user = new User(['email' => 'test@user.com']); @@ -681,6 +732,7 @@ public function calling_givePermissionTo_before_saving_object_doesnt_interfere_w } /** @test */ + #[Test] public function calling_syncPermissions_before_saving_object_doesnt_interfere_with_other_objects() { $user = new User(['email' => 'test@user.com']); @@ -703,6 +755,7 @@ public function calling_syncPermissions_before_saving_object_doesnt_interfere_wi } /** @test */ + #[Test] public function it_can_retrieve_permission_names() { $this->testUser->givePermissionTo('edit-news', 'edit-articles'); @@ -713,6 +766,7 @@ public function it_can_retrieve_permission_names() } /** @test */ + #[Test] public function it_can_check_many_direct_permissions() { $this->testUser->givePermissionTo(['edit-articles', 'edit-news']); @@ -723,6 +777,7 @@ public function it_can_check_many_direct_permissions() } /** @test */ + #[Test] public function it_can_check_if_there_is_any_of_the_direct_permissions_given() { $this->testUser->givePermissionTo(['edit-articles', 'edit-news']); @@ -732,6 +787,7 @@ public function it_can_check_if_there_is_any_of_the_direct_permissions_given() } /** @test */ + #[Test] public function it_can_check_permission_based_on_logged_in_user_guard() { $this->testUser->givePermissionTo(app(Permission::class)::create([ @@ -746,6 +802,7 @@ public function it_can_check_permission_based_on_logged_in_user_guard() } /** @test */ + #[Test] public function it_can_reject_permission_based_on_logged_in_user_guard() { $unassignedPermission = app(Permission::class)::create([ @@ -768,6 +825,7 @@ public function it_can_reject_permission_based_on_logged_in_user_guard() } /** @test */ + #[Test] public function it_can_be_given_a_permission_on_role_when_lazy_loading_is_restricted() { $this->assertTrue(Model::preventsLazyLoading()); @@ -784,6 +842,7 @@ public function it_can_be_given_a_permission_on_role_when_lazy_loading_is_restri } /** @test */ + #[Test] public function it_can_be_given_a_permission_on_user_when_lazy_loading_is_restricted() { $this->assertTrue(Model::preventsLazyLoading()); diff --git a/tests/HasPermissionsWithCustomModelsTest.php b/tests/HasPermissionsWithCustomModelsTest.php index d26b92286..acbd53fcc 100644 --- a/tests/HasPermissionsWithCustomModelsTest.php +++ b/tests/HasPermissionsWithCustomModelsTest.php @@ -4,6 +4,7 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; +use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\PermissionRegistrar; use Spatie\Permission\Tests\TestModels\Admin; use Spatie\Permission\Tests\TestModels\Permission; @@ -27,12 +28,14 @@ protected function getEnvironmentSetUp($app) } /** @test */ + #[Test] public function it_can_use_custom_model_permission() { $this->assertSame(get_class($this->testUserPermission), Permission::class); } /** @test */ + #[Test] public function it_can_use_custom_fields_from_cache() { DB::connection()->getSchemaBuilder()->table(config('permission.table_names.roles'), function ($table) { @@ -54,6 +57,7 @@ public function it_can_use_custom_fields_from_cache() } /** @test */ + #[Test] public function it_can_scope_users_using_a_int() { // Skipped because custom model uses uuid, @@ -62,6 +66,7 @@ public function it_can_scope_users_using_a_int() } /** @test */ + #[Test] public function it_can_scope_users_using_a_uuid() { $uuid1 = $this->testUserPermission->getKey(); @@ -81,6 +86,7 @@ public function it_can_scope_users_using_a_uuid() } /** @test */ + #[Test] public function it_doesnt_detach_roles_when_soft_deleting() { $this->testUserRole->givePermissionTo($this->testUserPermission); @@ -97,6 +103,7 @@ public function it_doesnt_detach_roles_when_soft_deleting() } /** @test */ + #[Test] public function it_doesnt_detach_users_when_soft_deleting() { $this->testUser->givePermissionTo($this->testUserPermission); @@ -113,6 +120,7 @@ public function it_doesnt_detach_users_when_soft_deleting() } /** @test */ + #[Test] public function it_does_detach_roles_and_users_when_force_deleting() { $permission_id = $this->testUserPermission->getKey(); @@ -133,6 +141,7 @@ public function it_does_detach_roles_and_users_when_force_deleting() } /** @test */ + #[Test] public function it_should_touch_when_assigning_new_permissions() { Carbon::setTestNow('2021-07-19 10:13:14'); diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 4aa7c9073..5777f7bdb 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -4,6 +4,8 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\DB; +use PHPUnit\Framework\Attributes\RequiresPhp; +use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Contracts\Role; use Spatie\Permission\Exceptions\GuardDoesNotMatch; @@ -15,6 +17,7 @@ class HasRolesTest extends TestCase { /** @test */ + #[Test] public function it_can_determine_that_the_user_does_not_have_a_role() { $this->assertFalse($this->testUser->hasRole('testRole')); @@ -45,6 +48,8 @@ public function it_can_determine_that_the_user_does_not_have_a_role() * * @requires PHP >= 8.1 */ + #[RequiresPhp('>= 8.1')] + #[Test] public function it_can_assign_and_remove_a_role_using_enums() { $enum1 = TestModels\TestRolePermissionsEnum::USERMANAGER; @@ -97,6 +102,8 @@ public function it_can_assign_and_remove_a_role_using_enums() * * @requires PHP >= 8.1 */ + #[RequiresPhp('>= 8.1')] + #[Test] public function it_can_scope_a_role_using_enums() { $enum1 = TestModels\TestRolePermissionsEnum::USERMANAGER; @@ -124,6 +131,7 @@ public function it_can_scope_a_role_using_enums() } /** @test */ + #[Test] public function it_can_assign_and_remove_a_role() { $this->assertFalse($this->testUser->hasRole('testRole')); @@ -138,6 +146,7 @@ public function it_can_assign_and_remove_a_role() } /** @test */ + #[Test] public function it_removes_a_role_and_returns_roles() { $this->testUser->assignRole('testRole'); @@ -154,6 +163,7 @@ public function it_removes_a_role_and_returns_roles() } /** @test */ + #[Test] public function it_can_assign_and_remove_a_role_on_a_permission() { $this->testUserPermission->assignRole('testRole'); @@ -166,6 +176,7 @@ public function it_can_assign_and_remove_a_role_on_a_permission() } /** @test */ + #[Test] public function it_can_assign_a_role_using_an_object() { $this->testUser->assignRole($this->testUserRole); @@ -174,6 +185,7 @@ public function it_can_assign_a_role_using_an_object() } /** @test */ + #[Test] public function it_can_assign_a_role_using_an_id() { $this->testUser->assignRole($this->testUserRole->getKey()); @@ -182,6 +194,7 @@ public function it_can_assign_a_role_using_an_id() } /** @test */ + #[Test] public function it_can_assign_multiple_roles_at_once() { $this->testUser->assignRole($this->testUserRole->getKey(), 'testRole2'); @@ -192,6 +205,7 @@ public function it_can_assign_multiple_roles_at_once() } /** @test */ + #[Test] public function it_can_assign_multiple_roles_using_an_array() { $this->testUser->assignRole([$this->testUserRole->getKey(), 'testRole2']); @@ -202,6 +216,7 @@ public function it_can_assign_multiple_roles_using_an_array() } /** @test */ + #[Test] public function it_does_not_remove_already_associated_roles_when_assigning_new_roles() { $this->testUser->assignRole($this->testUserRole->getKey()); @@ -212,6 +227,7 @@ public function it_does_not_remove_already_associated_roles_when_assigning_new_r } /** @test */ + #[Test] public function it_does_not_throw_an_exception_when_assigning_a_role_that_is_already_assigned() { $this->testUser->assignRole($this->testUserRole->getKey()); @@ -222,6 +238,7 @@ public function it_does_not_throw_an_exception_when_assigning_a_role_that_is_alr } /** @test */ + #[Test] public function it_throws_an_exception_when_assigning_a_role_that_does_not_exist() { $this->expectException(RoleDoesNotExist::class); @@ -230,6 +247,7 @@ public function it_throws_an_exception_when_assigning_a_role_that_does_not_exist } /** @test */ + #[Test] public function it_can_only_assign_roles_from_the_correct_guard() { $this->expectException(RoleDoesNotExist::class); @@ -238,6 +256,7 @@ public function it_can_only_assign_roles_from_the_correct_guard() } /** @test */ + #[Test] public function it_throws_an_exception_when_assigning_a_role_from_a_different_guard() { $this->expectException(GuardDoesNotMatch::class); @@ -246,6 +265,7 @@ public function it_throws_an_exception_when_assigning_a_role_from_a_different_gu } /** @test */ + #[Test] public function it_ignores_null_roles_when_syncing() { $this->testUser->assignRole('testRole'); @@ -258,6 +278,7 @@ public function it_ignores_null_roles_when_syncing() } /** @test */ + #[Test] public function it_can_sync_roles_from_a_string() { $this->testUser->assignRole('testRole'); @@ -270,6 +291,7 @@ public function it_can_sync_roles_from_a_string() } /** @test */ + #[Test] public function it_can_sync_roles_from_a_string_on_a_permission() { $this->testUserPermission->assignRole('testRole'); @@ -282,6 +304,7 @@ public function it_can_sync_roles_from_a_string_on_a_permission() } /** @test */ + #[Test] public function it_can_avoid_sync_duplicated_roles() { $this->testUser->syncRoles('testRole', 'testRole', 'testRole2'); @@ -292,6 +315,7 @@ public function it_can_avoid_sync_duplicated_roles() } /** @test */ + #[Test] public function it_can_sync_multiple_roles() { $this->testUser->syncRoles('testRole', 'testRole2'); @@ -302,6 +326,7 @@ public function it_can_sync_multiple_roles() } /** @test */ + #[Test] public function it_can_sync_multiple_roles_from_an_array() { $this->testUser->syncRoles(['testRole', 'testRole2']); @@ -312,6 +337,7 @@ public function it_can_sync_multiple_roles_from_an_array() } /** @test */ + #[Test] public function it_will_remove_all_roles_when_an_empty_array_is_passed_to_sync_roles() { $this->testUser->assignRole('testRole'); @@ -326,6 +352,7 @@ public function it_will_remove_all_roles_when_an_empty_array_is_passed_to_sync_r } /** @test */ + #[Test] public function sync_roles_error_does_not_detach_roles() { $this->testUser->assignRole('testRole'); @@ -338,6 +365,7 @@ public function sync_roles_error_does_not_detach_roles() } /** @test */ + #[Test] public function it_will_sync_roles_to_a_model_that_is_not_persisted() { $user = new User(['email' => 'test@user.com']); @@ -353,6 +381,7 @@ public function it_will_sync_roles_to_a_model_that_is_not_persisted() } /** @test */ + #[Test] public function it_does_not_run_unnecessary_sqls_when_assigning_new_roles() { $role2 = app(Role::class)->where('name', ['testRole2'])->first(); @@ -365,6 +394,7 @@ public function it_does_not_run_unnecessary_sqls_when_assigning_new_roles() } /** @test */ + #[Test] public function calling_syncRoles_before_saving_object_doesnt_interfere_with_other_objects() { $user = new User(['email' => 'test@user.com']); @@ -387,6 +417,7 @@ public function calling_syncRoles_before_saving_object_doesnt_interfere_with_oth } /** @test */ + #[Test] public function calling_assignRole_before_saving_object_doesnt_interfere_with_other_objects() { $user = new User(['email' => 'test@user.com']); @@ -409,6 +440,7 @@ public function calling_assignRole_before_saving_object_doesnt_interfere_with_ot } /** @test */ + #[Test] public function it_throws_an_exception_when_syncing_a_role_from_another_guard() { $this->expectException(RoleDoesNotExist::class); @@ -421,6 +453,7 @@ public function it_throws_an_exception_when_syncing_a_role_from_another_guard() } /** @test */ + #[Test] public function it_deletes_pivot_table_entries_when_deleting_models() { $user = User::create(['email' => 'user@test.com']); @@ -438,6 +471,7 @@ public function it_deletes_pivot_table_entries_when_deleting_models() } /** @test */ + #[Test] public function it_can_scope_users_using_a_string() { $user1 = User::create(['email' => 'user1@test.com']); @@ -451,6 +485,7 @@ public function it_can_scope_users_using_a_string() } /** @test */ + #[Test] public function it_can_withoutscope_users_using_a_string() { User::all()->each(fn ($item) => $item->delete()); @@ -467,6 +502,7 @@ public function it_can_withoutscope_users_using_a_string() } /** @test */ + #[Test] public function it_can_scope_users_using_an_array() { $user1 = User::create(['email' => 'user1@test.com']); @@ -482,6 +518,7 @@ public function it_can_scope_users_using_an_array() } /** @test */ + #[Test] public function it_can_withoutscope_users_using_an_array() { User::all()->each(fn ($item) => $item->delete()); @@ -500,6 +537,7 @@ public function it_can_withoutscope_users_using_an_array() } /** @test */ + #[Test] public function it_can_scope_users_using_an_array_of_ids_and_names() { $user1 = User::create(['email' => 'user1@test.com']); @@ -516,6 +554,7 @@ public function it_can_scope_users_using_an_array_of_ids_and_names() } /** @test */ + #[Test] public function it_can_withoutscope_users_using_an_array_of_ids_and_names() { app(Role::class)->create(['name' => 'testRole3']); @@ -537,6 +576,7 @@ public function it_can_withoutscope_users_using_an_array_of_ids_and_names() } /** @test */ + #[Test] public function it_can_scope_users_using_a_collection() { $user1 = User::create(['email' => 'user1@test.com']); @@ -552,6 +592,7 @@ public function it_can_scope_users_using_a_collection() } /** @test */ + #[Test] public function it_can_withoutscope_users_using_a_collection() { app(Role::class)->create(['name' => 'testRole3']); @@ -572,6 +613,7 @@ public function it_can_withoutscope_users_using_a_collection() } /** @test */ + #[Test] public function it_can_scope_users_using_an_object() { $user1 = User::create(['email' => 'user1@test.com']); @@ -589,6 +631,7 @@ public function it_can_scope_users_using_an_object() } /** @test */ + #[Test] public function it_can_withoutscope_users_using_an_object() { User::all()->each(fn ($item) => $item->delete()); @@ -609,6 +652,7 @@ public function it_can_withoutscope_users_using_an_object() } /** @test */ + #[Test] public function it_can_scope_against_a_specific_guard() { $user1 = User::create(['email' => 'user1@test.com']); @@ -635,6 +679,7 @@ public function it_can_scope_against_a_specific_guard() } /** @test */ + #[Test] public function it_can_withoutscope_against_a_specific_guard() { User::all()->each(fn ($item) => $item->delete()); @@ -665,6 +710,7 @@ public function it_can_withoutscope_against_a_specific_guard() } /** @test */ + #[Test] public function it_throws_an_exception_when_trying_to_scope_a_role_from_another_guard() { $this->expectException(RoleDoesNotExist::class); @@ -677,6 +723,7 @@ public function it_throws_an_exception_when_trying_to_scope_a_role_from_another_ } /** @test */ + #[Test] public function it_throws_an_exception_when_trying_to_call_withoutscope_on_a_role_from_another_guard() { $this->expectException(RoleDoesNotExist::class); @@ -689,6 +736,7 @@ public function it_throws_an_exception_when_trying_to_call_withoutscope_on_a_rol } /** @test */ + #[Test] public function it_throws_an_exception_when_trying_to_scope_a_non_existing_role() { $this->expectException(RoleDoesNotExist::class); @@ -697,6 +745,7 @@ public function it_throws_an_exception_when_trying_to_scope_a_non_existing_role( } /** @test */ + #[Test] public function it_throws_an_exception_when_trying_to_use_withoutscope_on_a_non_existing_role() { $this->expectException(RoleDoesNotExist::class); @@ -705,6 +754,7 @@ public function it_throws_an_exception_when_trying_to_use_withoutscope_on_a_non_ } /** @test */ + #[Test] public function it_can_determine_that_a_user_has_one_of_the_given_roles() { $roleModel = app(Role::class); @@ -733,6 +783,7 @@ public function it_can_determine_that_a_user_has_one_of_the_given_roles() } /** @test */ + #[Test] public function it_can_determine_that_a_user_has_all_of_the_given_roles() { $roleModel = app(Role::class); @@ -762,6 +813,7 @@ public function it_can_determine_that_a_user_has_all_of_the_given_roles() } /** @test */ + #[Test] public function it_can_determine_that_a_user_has_exact_all_of_the_given_roles() { $roleModel = app(Role::class); @@ -801,6 +853,7 @@ public function it_can_determine_that_a_user_has_exact_all_of_the_given_roles() } /** @test */ + #[Test] public function it_can_determine_that_a_user_does_not_have_a_role_from_another_guard() { $this->assertFalse($this->testUser->hasRole('testAdminRole')); @@ -815,6 +868,7 @@ public function it_can_determine_that_a_user_does_not_have_a_role_from_another_g } /** @test */ + #[Test] public function it_can_check_against_any_multiple_roles_using_multiple_arguments() { $this->testUser->assignRole('testRole'); @@ -823,12 +877,14 @@ public function it_can_check_against_any_multiple_roles_using_multiple_arguments } /** @test */ + #[Test] public function it_returns_false_instead_of_an_exception_when_checking_against_any_undefined_roles_using_multiple_arguments() { $this->assertFalse($this->testUser->hasAnyRole('This Role Does Not Even Exist', $this->testAdminRole)); } /** @test */ + #[Test] public function it_throws_an_exception_if_an_unsupported_type_is_passed_to_hasRoles() { $this->expectException(\TypeError::class); @@ -837,6 +893,7 @@ public function it_throws_an_exception_if_an_unsupported_type_is_passed_to_hasRo } /** @test */ + #[Test] public function it_can_retrieve_role_names() { $this->testUser->assignRole('testRole', 'testRole2'); @@ -848,6 +905,7 @@ public function it_can_retrieve_role_names() } /** @test */ + #[Test] public function it_does_not_detach_roles_when_user_soft_deleting() { $user = SoftDeletingUser::create(['email' => 'test@example.com']); @@ -860,6 +918,7 @@ public function it_does_not_detach_roles_when_user_soft_deleting() } /** @test */ + #[Test] public function it_can_be_given_a_role_on_permission_when_lazy_loading_is_restricted() { $this->assertTrue(Model::preventsLazyLoading()); @@ -876,6 +935,7 @@ public function it_can_be_given_a_role_on_permission_when_lazy_loading_is_restri } /** @test */ + #[Test] public function it_can_be_given_a_role_on_user_when_lazy_loading_is_restricted() { $this->assertTrue(Model::preventsLazyLoading()); diff --git a/tests/HasRolesWithCustomModelsTest.php b/tests/HasRolesWithCustomModelsTest.php index b306676af..767ab6752 100644 --- a/tests/HasRolesWithCustomModelsTest.php +++ b/tests/HasRolesWithCustomModelsTest.php @@ -4,6 +4,7 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; +use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Tests\TestModels\Admin; use Spatie\Permission\Tests\TestModels\Role; @@ -25,12 +26,14 @@ protected function getEnvironmentSetUp($app) } /** @test */ + #[Test] public function it_can_use_custom_model_role() { $this->assertSame(get_class($this->testUserRole), Role::class); } /** @test */ + #[Test] public function it_doesnt_detach_permissions_when_soft_deleting() { $this->testUserRole->givePermissionTo($this->testUserPermission); @@ -47,6 +50,7 @@ public function it_doesnt_detach_permissions_when_soft_deleting() } /** @test */ + #[Test] public function it_doesnt_detach_users_when_soft_deleting() { $this->testUser->assignRole($this->testUserRole); @@ -63,6 +67,7 @@ public function it_doesnt_detach_users_when_soft_deleting() } /** @test */ + #[Test] public function it_does_detach_permissions_and_users_when_force_deleting() { $role_id = $this->testUserRole->getKey(); @@ -83,6 +88,7 @@ public function it_does_detach_permissions_and_users_when_force_deleting() } /** @test */ + #[Test] public function it_should_touch_when_assigning_new_roles() { Carbon::setTestNow('2021-07-19 10:13:14'); diff --git a/tests/MultipleGuardsTest.php b/tests/MultipleGuardsTest.php index 64c229a56..cd57639cf 100644 --- a/tests/MultipleGuardsTest.php +++ b/tests/MultipleGuardsTest.php @@ -4,6 +4,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; +use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Tests\TestModels\Manager; @@ -37,6 +38,7 @@ public function setUpRoutes(): void } /** @test */ + #[Test] public function it_can_give_a_permission_to_a_model_that_is_used_by_multiple_guards() { $this->testUser->givePermissionTo(app(Permission::class)::create([ @@ -55,6 +57,7 @@ public function it_can_give_a_permission_to_a_model_that_is_used_by_multiple_gua } /** @test */ + #[Test] public function the_gate_can_grant_permission_to_a_user_by_passing_a_guard_name() { $this->testUser->givePermissionTo(app(Permission::class)::create([ @@ -96,6 +99,7 @@ public function the_gate_can_grant_permission_to_a_user_by_passing_a_guard_name( } /** @test */ + #[Test] public function it_can_honour_guardName_function_on_model_for_overriding_guard_name_property() { $user = Manager::create(['email' => 'manager@test.com']); diff --git a/tests/PermissionMiddlewareTest.php b/tests/PermissionMiddlewareTest.php index 354f1f8b1..4dc233193 100644 --- a/tests/PermissionMiddlewareTest.php +++ b/tests/PermissionMiddlewareTest.php @@ -9,6 +9,7 @@ use Illuminate\Support\Facades\Gate; use InvalidArgumentException; use Laravel\Passport\Passport; +use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Exceptions\UnauthorizedException; use Spatie\Permission\Middleware\PermissionMiddleware; @@ -28,6 +29,7 @@ protected function setUp(): void } /** @test */ + #[Test] public function a_guest_cannot_access_a_route_protected_by_the_permission_middleware() { $this->assertEquals( @@ -37,6 +39,7 @@ public function a_guest_cannot_access_a_route_protected_by_the_permission_middle } /** @test */ + #[Test] public function a_user_cannot_access_a_route_protected_by_the_permission_middleware_of_a_different_guard() { // These permissions are created fresh here in reverse order of guard being applied, so they are not "found first" in the db lookup when matching @@ -75,6 +78,7 @@ public function a_user_cannot_access_a_route_protected_by_the_permission_middlew } /** @test */ + #[Test] public function a_client_cannot_access_a_route_protected_by_the_permission_middleware_of_a_different_guard(): void { if ($this->getLaravelVersion() < 9) { @@ -101,6 +105,7 @@ public function a_client_cannot_access_a_route_protected_by_the_permission_middl } /** @test */ + #[Test] public function a_super_admin_user_can_access_a_route_protected_by_permission_middleware() { Auth::login($this->testUser); @@ -116,6 +121,7 @@ public function a_super_admin_user_can_access_a_route_protected_by_permission_mi } /** @test */ + #[Test] public function a_user_can_access_a_route_protected_by_permission_middleware_if_have_this_permission() { Auth::login($this->testUser); @@ -129,6 +135,7 @@ public function a_user_can_access_a_route_protected_by_permission_middleware_if_ } /** @test */ + #[Test] public function a_client_can_access_a_route_protected_by_permission_middleware_if_have_this_permission(): void { if ($this->getLaravelVersion() < 9) { @@ -146,6 +153,7 @@ public function a_client_can_access_a_route_protected_by_permission_middleware_i } /** @test */ + #[Test] public function a_user_can_access_a_route_protected_by_this_permission_middleware_if_have_one_of_the_permissions() { Auth::login($this->testUser); @@ -164,6 +172,7 @@ public function a_user_can_access_a_route_protected_by_this_permission_middlewar } /** @test */ + #[Test] public function a_client_can_access_a_route_protected_by_this_permission_middleware_if_have_one_of_the_permissions(): void { if ($this->getLaravelVersion() < 9) { @@ -186,6 +195,7 @@ public function a_client_can_access_a_route_protected_by_this_permission_middlew } /** @test */ + #[Test] public function a_user_cannot_access_a_route_protected_by_the_permission_middleware_if_have_not_has_roles_trait() { $userWithoutHasRoles = UserWithoutHasRoles::create(['email' => 'test_not_has_roles@user.com']); @@ -199,6 +209,7 @@ public function a_user_cannot_access_a_route_protected_by_the_permission_middlew } /** @test */ + #[Test] public function a_user_cannot_access_a_route_protected_by_the_permission_middleware_if_have_a_different_permission() { Auth::login($this->testUser); @@ -212,6 +223,7 @@ public function a_user_cannot_access_a_route_protected_by_the_permission_middlew } /** @test */ + #[Test] public function a_client_cannot_access_a_route_protected_by_the_permission_middleware_if_have_a_different_permission(): void { if ($this->getLaravelVersion() < 9) { @@ -229,6 +241,7 @@ public function a_client_cannot_access_a_route_protected_by_the_permission_middl } /** @test */ + #[Test] public function a_user_cannot_access_a_route_protected_by_permission_middleware_if_have_not_permissions() { Auth::login($this->testUser); @@ -240,6 +253,7 @@ public function a_user_cannot_access_a_route_protected_by_permission_middleware_ } /** @test */ + #[Test] public function a_client_cannot_access_a_route_protected_by_permission_middleware_if_have_not_permissions(): void { if ($this->getLaravelVersion() < 9) { @@ -255,6 +269,7 @@ public function a_client_cannot_access_a_route_protected_by_permission_middlewar } /** @test */ + #[Test] public function a_user_can_access_a_route_protected_by_permission_middleware_if_has_permission_via_role() { Auth::login($this->testUser); @@ -274,6 +289,7 @@ public function a_user_can_access_a_route_protected_by_permission_middleware_if_ } /** @test */ + #[Test] public function a_client_can_access_a_route_protected_by_permission_middleware_if_has_permission_via_role(): void { if ($this->getLaravelVersion() < 9) { @@ -297,6 +313,7 @@ public function a_client_can_access_a_route_protected_by_permission_middleware_i } /** @test */ + #[Test] public function the_required_permissions_can_be_fetched_from_the_exception() { Auth::login($this->testUser); @@ -318,6 +335,7 @@ public function the_required_permissions_can_be_fetched_from_the_exception() } /** @test */ + #[Test] public function the_required_permissions_can_be_displayed_in_the_exception() { Auth::login($this->testUser); @@ -337,6 +355,7 @@ public function the_required_permissions_can_be_displayed_in_the_exception() } /** @test */ + #[Test] public function use_not_existing_custom_guard_in_permission() { $class = null; @@ -353,6 +372,7 @@ public function use_not_existing_custom_guard_in_permission() } /** @test */ + #[Test] public function user_can_not_access_permission_with_guard_admin_while_login_using_default_guard() { Auth::login($this->testUser); @@ -366,6 +386,7 @@ public function user_can_not_access_permission_with_guard_admin_while_login_usin } /** @test */ + #[Test] public function client_can_not_access_permission_with_guard_admin_while_login_using_default_guard(): void { if ($this->getLaravelVersion() < 9) { @@ -383,6 +404,7 @@ public function client_can_not_access_permission_with_guard_admin_while_login_us } /** @test */ + #[Test] public function user_can_access_permission_with_guard_admin_while_login_using_admin_guard() { Auth::guard('admin')->login($this->testAdmin); @@ -396,6 +418,7 @@ public function user_can_access_permission_with_guard_admin_while_login_using_ad } /** @test */ + #[Test] public function the_middleware_can_be_created_with_static_using_method() { $this->assertSame( diff --git a/tests/PermissionRegistrarTest.php b/tests/PermissionRegistrarTest.php index 115926d4c..c9df969e4 100644 --- a/tests/PermissionRegistrarTest.php +++ b/tests/PermissionRegistrarTest.php @@ -2,6 +2,7 @@ namespace Spatie\Permission\Tests; +use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Contracts\Permission as PermissionContract; use Spatie\Permission\Contracts\Role as RoleContract; use Spatie\Permission\Models\Permission as SpatiePermission; @@ -13,6 +14,7 @@ class PermissionRegistrarTest extends TestCase { /** @test */ + #[Test] public function it_can_clear_loaded_permissions_collection() { $reflectedClass = new \ReflectionClass(app(PermissionRegistrar::class)); @@ -29,6 +31,7 @@ public function it_can_clear_loaded_permissions_collection() } /** @test */ + #[Test] public function it_can_check_uids() { $uids = [ @@ -72,6 +75,7 @@ public function it_can_check_uids() } /** @test */ + #[Test] public function it_can_get_permission_class() { $this->assertSame(SpatiePermission::class, app(PermissionRegistrar::class)->getPermissionClass()); @@ -79,6 +83,7 @@ public function it_can_get_permission_class() } /** @test */ + #[Test] public function it_can_change_permission_class() { $this->assertSame(SpatiePermission::class, config('permission.models.permission')); @@ -93,6 +98,7 @@ public function it_can_change_permission_class() } /** @test */ + #[Test] public function it_can_get_role_class() { $this->assertSame(SpatieRole::class, app(PermissionRegistrar::class)->getRoleClass()); @@ -100,6 +106,7 @@ public function it_can_get_role_class() } /** @test */ + #[Test] public function it_can_change_role_class() { $this->assertSame(SpatieRole::class, config('permission.models.role')); @@ -114,6 +121,7 @@ public function it_can_change_role_class() } /** @test */ + #[Test] public function it_can_change_team_id() { $team_id = '00000000-0000-0000-0000-000000000000'; diff --git a/tests/PermissionTest.php b/tests/PermissionTest.php index d43645730..553b3650a 100644 --- a/tests/PermissionTest.php +++ b/tests/PermissionTest.php @@ -2,6 +2,7 @@ namespace Spatie\Permission\Tests; +use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Exceptions\PermissionAlreadyExists; use Spatie\Permission\Tests\TestModels\User; @@ -9,6 +10,7 @@ class PermissionTest extends TestCase { /** @test */ + #[Test] public function it_get_user_models_using_with() { $this->testUser->givePermissionTo($this->testUserPermission); @@ -23,6 +25,7 @@ public function it_get_user_models_using_with() } /** @test */ + #[Test] public function it_throws_an_exception_when_the_permission_already_exists() { $this->expectException(PermissionAlreadyExists::class); @@ -32,6 +35,7 @@ public function it_throws_an_exception_when_the_permission_already_exists() } /** @test */ + #[Test] public function it_belongs_to_a_guard() { $permission = app(Permission::class)->create(['name' => 'can-edit', 'guard_name' => 'admin']); @@ -40,6 +44,7 @@ public function it_belongs_to_a_guard() } /** @test */ + #[Test] public function it_belongs_to_the_default_guard_by_default() { $this->assertEquals( @@ -49,6 +54,7 @@ public function it_belongs_to_the_default_guard_by_default() } /** @test */ + #[Test] public function it_has_user_models_of_the_right_class() { $this->testAdmin->givePermissionTo($this->testAdminPermission); @@ -61,6 +67,7 @@ public function it_has_user_models_of_the_right_class() } /** @test */ + #[Test] public function it_is_retrievable_by_id() { $permission_by_id = app(Permission::class)->findById($this->testUserPermission->id); @@ -69,6 +76,7 @@ public function it_is_retrievable_by_id() } /** @test */ + #[Test] public function it_can_delete_hydrated_permissions() { $this->reloadPermissions(); diff --git a/tests/PolicyTest.php b/tests/PolicyTest.php index 7708225b0..78490f997 100644 --- a/tests/PolicyTest.php +++ b/tests/PolicyTest.php @@ -4,11 +4,13 @@ use Illuminate\Contracts\Auth\Access\Authorizable; use Illuminate\Contracts\Auth\Access\Gate; +use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Tests\TestModels\Content; class PolicyTest extends TestCase { /** @test */ + #[Test] public function policy_methods_and_before_intercepts_can_allow_and_deny() { $record1 = Content::create(['content' => 'special admin content']); diff --git a/tests/RoleMiddlewareTest.php b/tests/RoleMiddlewareTest.php index 780277068..532f2061f 100644 --- a/tests/RoleMiddlewareTest.php +++ b/tests/RoleMiddlewareTest.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Config; use InvalidArgumentException; use Laravel\Passport\Passport; +use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Exceptions\UnauthorizedException; use Spatie\Permission\Middleware\RoleMiddleware; use Spatie\Permission\Tests\TestModels\UserWithoutHasRoles; @@ -26,6 +27,7 @@ protected function setUp(): void } /** @test */ + #[Test] public function a_guest_cannot_access_a_route_protected_by_rolemiddleware() { $this->assertEquals( @@ -35,6 +37,7 @@ public function a_guest_cannot_access_a_route_protected_by_rolemiddleware() } /** @test */ + #[Test] public function a_user_cannot_access_a_route_protected_by_role_middleware_of_another_guard() { Auth::login($this->testUser); @@ -48,6 +51,7 @@ public function a_user_cannot_access_a_route_protected_by_role_middleware_of_ano } /** @test */ + #[Test] public function a_client_cannot_access_a_route_protected_by_role_middleware_of_another_guard(): void { if ($this->getLaravelVersion() < 9) { @@ -65,6 +69,7 @@ public function a_client_cannot_access_a_route_protected_by_role_middleware_of_a } /** @test */ + #[Test] public function a_user_can_access_a_route_protected_by_role_middleware_if_have_this_role() { Auth::login($this->testUser); @@ -78,6 +83,7 @@ public function a_user_can_access_a_route_protected_by_role_middleware_if_have_t } /** @test */ + #[Test] public function a_client_can_access_a_route_protected_by_role_middleware_if_have_this_role(): void { if ($this->getLaravelVersion() < 9) { @@ -95,6 +101,7 @@ public function a_client_can_access_a_route_protected_by_role_middleware_if_have } /** @test */ + #[Test] public function a_user_can_access_a_route_protected_by_this_role_middleware_if_have_one_of_the_roles() { Auth::login($this->testUser); @@ -113,6 +120,7 @@ public function a_user_can_access_a_route_protected_by_this_role_middleware_if_h } /** @test */ + #[Test] public function a_client_can_access_a_route_protected_by_this_role_middleware_if_have_one_of_the_roles(): void { if ($this->getLaravelVersion() < 9) { @@ -135,6 +143,7 @@ public function a_client_can_access_a_route_protected_by_this_role_middleware_if } /** @test */ + #[Test] public function a_user_cannot_access_a_route_protected_by_the_role_middleware_if_have_not_has_roles_trait() { $userWithoutHasRoles = UserWithoutHasRoles::create(['email' => 'test_not_has_roles@user.com']); @@ -148,6 +157,7 @@ public function a_user_cannot_access_a_route_protected_by_the_role_middleware_if } /** @test */ + #[Test] public function a_user_cannot_access_a_route_protected_by_the_role_middleware_if_have_a_different_role() { Auth::login($this->testUser); @@ -161,6 +171,7 @@ public function a_user_cannot_access_a_route_protected_by_the_role_middleware_if } /** @test */ + #[Test] public function a_client_cannot_access_a_route_protected_by_the_role_middleware_if_have_a_different_role(): void { if ($this->getLaravelVersion() < 9) { @@ -178,6 +189,7 @@ public function a_client_cannot_access_a_route_protected_by_the_role_middleware_ } /** @test */ + #[Test] public function a_user_cannot_access_a_route_protected_by_role_middleware_if_have_not_roles() { Auth::login($this->testUser); @@ -189,6 +201,7 @@ public function a_user_cannot_access_a_route_protected_by_role_middleware_if_hav } /** @test */ + #[Test] public function a_client_cannot_access_a_route_protected_by_role_middleware_if_have_not_roles(): void { if ($this->getLaravelVersion() < 9) { @@ -204,6 +217,7 @@ public function a_client_cannot_access_a_route_protected_by_role_middleware_if_h } /** @test */ + #[Test] public function a_user_cannot_access_a_route_protected_by_role_middleware_if_role_is_undefined() { Auth::login($this->testUser); @@ -215,6 +229,7 @@ public function a_user_cannot_access_a_route_protected_by_role_middleware_if_rol } /** @test */ + #[Test] public function a_client_cannot_access_a_route_protected_by_role_middleware_if_role_is_undefined(): void { if ($this->getLaravelVersion() < 9) { @@ -230,6 +245,7 @@ public function a_client_cannot_access_a_route_protected_by_role_middleware_if_r } /** @test */ + #[Test] public function the_required_roles_can_be_fetched_from_the_exception() { Auth::login($this->testUser); @@ -251,6 +267,7 @@ public function the_required_roles_can_be_fetched_from_the_exception() } /** @test */ + #[Test] public function the_required_roles_can_be_displayed_in_the_exception() { Auth::login($this->testUser); @@ -270,6 +287,7 @@ public function the_required_roles_can_be_displayed_in_the_exception() } /** @test */ + #[Test] public function use_not_existing_custom_guard_in_role() { $class = null; @@ -286,6 +304,7 @@ public function use_not_existing_custom_guard_in_role() } /** @test */ + #[Test] public function user_can_not_access_role_with_guard_admin_while_login_using_default_guard() { Auth::login($this->testUser); @@ -299,6 +318,7 @@ public function user_can_not_access_role_with_guard_admin_while_login_using_defa } /** @test */ + #[Test] public function client_can_not_access_role_with_guard_admin_while_login_using_default_guard(): void { if ($this->getLaravelVersion() < 9) { @@ -316,6 +336,7 @@ public function client_can_not_access_role_with_guard_admin_while_login_using_de } /** @test */ + #[Test] public function user_can_access_role_with_guard_admin_while_login_using_admin_guard() { Auth::guard('admin')->login($this->testAdmin); @@ -329,6 +350,7 @@ public function user_can_access_role_with_guard_admin_while_login_using_admin_gu } /** @test */ + #[Test] public function the_middleware_can_be_created_with_static_using_method() { $this->assertSame( diff --git a/tests/RoleOrPermissionMiddlewareTest.php b/tests/RoleOrPermissionMiddlewareTest.php index 617c4c5af..8a60e9f4f 100644 --- a/tests/RoleOrPermissionMiddlewareTest.php +++ b/tests/RoleOrPermissionMiddlewareTest.php @@ -9,6 +9,7 @@ use Illuminate\Support\Facades\Gate; use InvalidArgumentException; use Laravel\Passport\Passport; +use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Exceptions\UnauthorizedException; use Spatie\Permission\Middleware\RoleOrPermissionMiddleware; use Spatie\Permission\Tests\TestModels\UserWithoutHasRoles; @@ -27,6 +28,7 @@ protected function setUp(): void } /** @test */ + #[Test] public function a_guest_cannot_access_a_route_protected_by_the_role_or_permission_middleware() { $this->assertEquals( @@ -36,6 +38,7 @@ public function a_guest_cannot_access_a_route_protected_by_the_role_or_permissio } /** @test */ + #[Test] public function a_user_can_access_a_route_protected_by_permission_or_role_middleware_if_has_this_permission_or_role() { Auth::login($this->testUser); @@ -70,6 +73,7 @@ public function a_user_can_access_a_route_protected_by_permission_or_role_middle } /** @test */ + #[Test] public function a_client_can_access_a_route_protected_by_permission_or_role_middleware_if_has_this_permission_or_role(): void { if ($this->getLaravelVersion() < 9) { @@ -108,6 +112,7 @@ public function a_client_can_access_a_route_protected_by_permission_or_role_midd } /** @test */ + #[Test] public function a_super_admin_user_can_access_a_route_protected_by_permission_or_role_middleware() { Auth::login($this->testUser); @@ -123,6 +128,7 @@ public function a_super_admin_user_can_access_a_route_protected_by_permission_or } /** @test */ + #[Test] public function a_user_can_not_access_a_route_protected_by_permission_or_role_middleware_if_have_not_has_roles_trait() { $userWithoutHasRoles = UserWithoutHasRoles::create(['email' => 'test_not_has_roles@user.com']); @@ -136,6 +142,7 @@ public function a_user_can_not_access_a_route_protected_by_permission_or_role_mi } /** @test */ + #[Test] public function a_user_can_not_access_a_route_protected_by_permission_or_role_middleware_if_have_not_this_permission_and_role() { Auth::login($this->testUser); @@ -152,6 +159,7 @@ public function a_user_can_not_access_a_route_protected_by_permission_or_role_mi } /** @test */ + #[Test] public function a_client_can_not_access_a_route_protected_by_permission_or_role_middleware_if_have_not_this_permission_and_role(): void { if ($this->getLaravelVersion() < 9) { @@ -172,6 +180,7 @@ public function a_client_can_not_access_a_route_protected_by_permission_or_role_ } /** @test */ + #[Test] public function use_not_existing_custom_guard_in_role_or_permission() { $class = null; @@ -188,6 +197,7 @@ public function use_not_existing_custom_guard_in_role_or_permission() } /** @test */ + #[Test] public function user_can_not_access_permission_or_role_with_guard_admin_while_login_using_default_guard() { Auth::login($this->testUser); @@ -202,6 +212,7 @@ public function user_can_not_access_permission_or_role_with_guard_admin_while_lo } /** @test */ + #[Test] public function client_can_not_access_permission_or_role_with_guard_admin_while_login_using_default_guard(): void { if ($this->getLaravelVersion() < 9) { @@ -220,6 +231,7 @@ public function client_can_not_access_permission_or_role_with_guard_admin_while_ } /** @test */ + #[Test] public function user_can_access_permission_or_role_with_guard_admin_while_login_using_admin_guard() { Auth::guard('admin')->login($this->testAdmin); @@ -234,6 +246,7 @@ public function user_can_access_permission_or_role_with_guard_admin_while_login_ } /** @test */ + #[Test] public function the_required_permissions_or_roles_can_be_fetched_from_the_exception() { Auth::login($this->testUser); @@ -255,6 +268,7 @@ public function the_required_permissions_or_roles_can_be_fetched_from_the_except } /** @test */ + #[Test] public function the_required_permissions_or_roles_can_be_displayed_in_the_exception() { Auth::login($this->testUser); @@ -275,6 +289,7 @@ public function the_required_permissions_or_roles_can_be_displayed_in_the_except } /** @test */ + #[Test] public function the_middleware_can_be_created_with_static_using_method() { $this->assertSame( diff --git a/tests/RoleTest.php b/tests/RoleTest.php index 68d7c1aee..af88b0af6 100644 --- a/tests/RoleTest.php +++ b/tests/RoleTest.php @@ -2,6 +2,7 @@ namespace Spatie\Permission\Tests; +use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Contracts\Role; use Spatie\Permission\Exceptions\GuardDoesNotMatch; use Spatie\Permission\Exceptions\PermissionDoesNotExist; @@ -25,6 +26,7 @@ protected function setUp(): void } /** @test */ + #[Test] public function it_get_user_models_using_with() { $this->testUser->assignRole($this->testUserRole); @@ -38,6 +40,7 @@ public function it_get_user_models_using_with() } /** @test */ + #[Test] public function it_has_user_models_of_the_right_class() { $this->testAdmin->assignRole($this->testAdminRole); @@ -54,6 +57,7 @@ public function it_has_user_models_of_the_right_class() } /** @test */ + #[Test] public function it_throws_an_exception_when_the_role_already_exists() { $this->expectException(RoleAlreadyExists::class); @@ -63,6 +67,7 @@ public function it_throws_an_exception_when_the_role_already_exists() } /** @test */ + #[Test] public function it_can_be_given_a_permission() { $this->testUserRole->givePermissionTo('edit-articles'); @@ -71,6 +76,7 @@ public function it_can_be_given_a_permission() } /** @test */ + #[Test] public function it_throws_an_exception_when_given_a_permission_that_does_not_exist() { $this->expectException(PermissionDoesNotExist::class); @@ -79,6 +85,7 @@ public function it_throws_an_exception_when_given_a_permission_that_does_not_exi } /** @test */ + #[Test] public function it_throws_an_exception_when_given_a_permission_that_belongs_to_another_guard() { $this->expectException(PermissionDoesNotExist::class); @@ -91,6 +98,7 @@ public function it_throws_an_exception_when_given_a_permission_that_belongs_to_a } /** @test */ + #[Test] public function it_can_be_given_multiple_permissions_using_an_array() { $this->testUserRole->givePermissionTo(['edit-articles', 'edit-news']); @@ -100,6 +108,7 @@ public function it_can_be_given_multiple_permissions_using_an_array() } /** @test */ + #[Test] public function it_can_be_given_multiple_permissions_using_multiple_arguments() { $this->testUserRole->givePermissionTo('edit-articles', 'edit-news'); @@ -109,6 +118,7 @@ public function it_can_be_given_multiple_permissions_using_multiple_arguments() } /** @test */ + #[Test] public function it_can_sync_permissions() { $this->testUserRole->givePermissionTo('edit-articles'); @@ -121,6 +131,7 @@ public function it_can_sync_permissions() } /** @test */ + #[Test] public function it_throws_an_exception_when_syncing_permissions_that_do_not_exist() { $this->testUserRole->givePermissionTo('edit-articles'); @@ -131,6 +142,7 @@ public function it_throws_an_exception_when_syncing_permissions_that_do_not_exis } /** @test */ + #[Test] public function it_throws_an_exception_when_syncing_permissions_that_belong_to_a_different_guard() { $this->testUserRole->givePermissionTo('edit-articles'); @@ -145,6 +157,7 @@ public function it_throws_an_exception_when_syncing_permissions_that_belong_to_a } /** @test */ + #[Test] public function it_will_remove_all_permissions_when_passing_an_empty_array_to_sync_permissions() { $this->testUserRole->givePermissionTo('edit-articles'); @@ -159,6 +172,7 @@ public function it_will_remove_all_permissions_when_passing_an_empty_array_to_sy } /** @test */ + #[Test] public function sync_permission_error_does_not_detach_permissions() { $this->testUserRole->givePermissionTo('edit-news'); @@ -171,6 +185,7 @@ public function sync_permission_error_does_not_detach_permissions() } /** @test */ + #[Test] public function it_can_revoke_a_permission() { $this->testUserRole->givePermissionTo('edit-articles'); @@ -185,6 +200,7 @@ public function it_can_revoke_a_permission() } /** @test */ + #[Test] public function it_can_be_given_a_permission_using_objects() { $this->testUserRole->givePermissionTo($this->testUserPermission); @@ -193,12 +209,14 @@ public function it_can_be_given_a_permission_using_objects() } /** @test */ + #[Test] public function it_returns_false_if_it_does_not_have_the_permission() { $this->assertFalse($this->testUserRole->hasPermissionTo('other-permission')); } /** @test */ + #[Test] public function it_throws_an_exception_if_the_permission_does_not_exist() { $this->expectException(PermissionDoesNotExist::class); @@ -207,6 +225,7 @@ public function it_throws_an_exception_if_the_permission_does_not_exist() } /** @test */ + #[Test] public function it_returns_false_if_it_does_not_have_a_permission_object() { $permission = app(Permission::class)->findByName('other-permission'); @@ -215,6 +234,7 @@ public function it_returns_false_if_it_does_not_have_a_permission_object() } /** @test */ + #[Test] public function it_creates_permission_object_with_findOrCreate_if_it_does_not_have_a_permission_object() { $permission = app(Permission::class)->findOrCreate('another-permission'); @@ -229,6 +249,7 @@ public function it_creates_permission_object_with_findOrCreate_if_it_does_not_ha } /** @test */ + #[Test] public function it_creates_a_role_with_findOrCreate_if_the_named_role_does_not_exist() { $this->expectException(RoleDoesNotExist::class); @@ -243,6 +264,7 @@ public function it_creates_a_role_with_findOrCreate_if_the_named_role_does_not_e } /** @test */ + #[Test] public function it_throws_an_exception_when_a_permission_of_the_wrong_guard_is_passed_in() { $this->expectException(GuardDoesNotMatch::class); @@ -253,6 +275,7 @@ public function it_throws_an_exception_when_a_permission_of_the_wrong_guard_is_p } /** @test */ + #[Test] public function it_belongs_to_a_guard() { $role = app(Role::class)->create(['name' => 'admin', 'guard_name' => 'admin']); @@ -261,6 +284,7 @@ public function it_belongs_to_a_guard() } /** @test */ + #[Test] public function it_belongs_to_the_default_guard_by_default() { $this->assertEquals( @@ -270,6 +294,7 @@ public function it_belongs_to_the_default_guard_by_default() } /** @test */ + #[Test] public function it_can_change_role_class_on_runtime() { $role = app(Role::class)->create(['name' => 'test-role-old']); diff --git a/tests/RoleWithNestingTest.php b/tests/RoleWithNestingTest.php index fb28b8c18..5ddf3cac0 100644 --- a/tests/RoleWithNestingTest.php +++ b/tests/RoleWithNestingTest.php @@ -2,6 +2,8 @@ namespace Spatie\Permission\Tests; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Tests\TestModels\Role; class RoleWithNestingTest extends TestCase @@ -62,6 +64,8 @@ protected function setUpDatabase($app) /** @test * @dataProvider roles_list */ + #[DataProvider('roles_list')] + #[Test] public function it_returns_correct_withCount_of_nested_roles($role_group, $index, $relation, $expectedCount) { $role = $this->$role_group[$index]; diff --git a/tests/RouteTest.php b/tests/RouteTest.php index e4843f0cb..0533ea74c 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -2,9 +2,12 @@ namespace Spatie\Permission\Tests; +use PHPUnit\Framework\Attributes\Test; + class RouteTest extends TestCase { /** @test */ + #[Test] public function test_role_function() { $router = $this->getRouter(); @@ -17,6 +20,7 @@ public function test_role_function() } /** @test */ + #[Test] public function test_permission_function() { $router = $this->getRouter(); @@ -29,6 +33,7 @@ public function test_permission_function() } /** @test */ + #[Test] public function test_role_and_permission_function_together() { $router = $this->getRouter(); diff --git a/tests/TeamHasPermissionsTest.php b/tests/TeamHasPermissionsTest.php index 5c9826b15..1e3ab20af 100644 --- a/tests/TeamHasPermissionsTest.php +++ b/tests/TeamHasPermissionsTest.php @@ -2,6 +2,7 @@ namespace Spatie\Permission\Tests; +use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Tests\TestModels\User; class TeamHasPermissionsTest extends HasPermissionsTest @@ -10,6 +11,7 @@ class TeamHasPermissionsTest extends HasPermissionsTest protected $hasTeams = true; /** @test */ + #[Test] public function it_can_assign_same_and_different_permission_on_same_user_on_different_teams() { setPermissionsTeamId(1); @@ -38,6 +40,7 @@ public function it_can_assign_same_and_different_permission_on_same_user_on_diff } /** @test */ + #[Test] public function it_can_list_all_the_coupled_permissions_both_directly_and_via_roles_on_same_user_on_different_teams() { $this->testUserRole->givePermissionTo('edit-articles'); @@ -68,6 +71,7 @@ public function it_can_list_all_the_coupled_permissions_both_directly_and_via_ro } /** @test */ + #[Test] public function it_can_sync_or_remove_permission_without_detach_on_different_teams() { setPermissionsTeamId(1); @@ -99,6 +103,7 @@ public function it_can_sync_or_remove_permission_without_detach_on_different_tea } /** @test */ + #[Test] public function it_can_scope_users_on_different_teams() { $user1 = User::create(['email' => 'user1@test.com']); diff --git a/tests/TeamHasRolesTest.php b/tests/TeamHasRolesTest.php index 6ad51ec4e..928ed7419 100644 --- a/tests/TeamHasRolesTest.php +++ b/tests/TeamHasRolesTest.php @@ -2,6 +2,7 @@ namespace Spatie\Permission\Tests; +use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Contracts\Role; use Spatie\Permission\Tests\TestModels\User; @@ -11,6 +12,7 @@ class TeamHasRolesTest extends HasRolesTest protected $hasTeams = true; /** @test */ + #[Test] public function it_deletes_pivot_table_entries_when_deleting_models() { $user1 = User::create(['email' => 'user2@test.com']); @@ -37,6 +39,7 @@ public function it_deletes_pivot_table_entries_when_deleting_models() } /** @test */ + #[Test] public function it_can_assign_same_and_different_roles_on_same_user_different_teams() { app(Role::class)->create(['name' => 'testRole3']); // team_test_id = 1 by main class @@ -84,6 +87,7 @@ public function it_can_assign_same_and_different_roles_on_same_user_different_te } /** @test */ + #[Test] public function it_can_sync_or_remove_roles_without_detach_on_different_teams() { app(Role::class)->create(['name' => 'testRole3', 'team_test_id' => 2]); @@ -118,6 +122,7 @@ public function it_can_sync_or_remove_roles_without_detach_on_different_teams() } /** @test */ + #[Test] public function it_can_scope_users_on_different_teams() { User::all()->each(fn ($item) => $item->delete()); diff --git a/tests/TestCase.php b/tests/TestCase.php index 1b222ad01..3a4f1c829 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -23,10 +23,10 @@ abstract class TestCase extends Orchestra { - /** @var \Spatie\Permission\Tests\User */ + /** @var \Spatie\Permission\Tests\TestModels\User */ protected $testUser; - /** @var \Spatie\Permission\Tests\Admin */ + /** @var \Spatie\Permission\Tests\TestModels\Admin */ protected $testAdmin; /** @var \Spatie\Permission\Models\Role */ diff --git a/tests/TestModels/Client.php b/tests/TestModels/Client.php index 267c36122..a0170dd13 100644 --- a/tests/TestModels/Client.php +++ b/tests/TestModels/Client.php @@ -14,8 +14,6 @@ class Client extends BaseClient implements AuthorizableContract /** * Required to make clear that the client requires the api guard - * - * @var string */ - protected $guard_name = 'api'; + protected string $guard_name = 'api'; } diff --git a/tests/TestModels/Manager.php b/tests/TestModels/Manager.php index 3405d924e..c96fecd83 100644 --- a/tests/TestModels/Manager.php +++ b/tests/TestModels/Manager.php @@ -6,11 +6,11 @@ class Manager extends User { // this function is added here to support the unit tests verifying it works // When present, it takes precedence over the $guard_name property. - public function guardName() + public function guardName(): string { return 'jwt'; } // intentionally different property value for the sake of unit tests - protected $guard_name = 'web'; + protected string $guard_name = 'web'; } diff --git a/tests/TestModels/Permission.php b/tests/TestModels/Permission.php index 5751490af..38ae68240 100644 --- a/tests/TestModels/Permission.php +++ b/tests/TestModels/Permission.php @@ -3,6 +3,7 @@ namespace Spatie\Permission\Tests\TestModels; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Str; class Permission extends \Spatie\Permission\Models\Permission { @@ -18,19 +19,19 @@ class Permission extends \Spatie\Permission\Models\Permission protected static function boot() { parent::boot(); - static::creating(function ($model) { + static::creating(static function ($model) { if (empty($model->{$model->getKeyName()})) { - $model->{$model->getKeyName()} = \Str::uuid()->toString(); + $model->{$model->getKeyName()} = Str::uuid()->toString(); } }); } - public function getIncrementing() + public function getIncrementing(): bool { return false; } - public function getKeyType() + public function getKeyType(): string { return 'string'; } diff --git a/tests/TestModels/Role.php b/tests/TestModels/Role.php index 5bd2c55e0..a5d4a2702 100644 --- a/tests/TestModels/Role.php +++ b/tests/TestModels/Role.php @@ -2,7 +2,9 @@ namespace Spatie\Permission\Tests\TestModels; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Str; class Role extends \Spatie\Permission\Models\Role { @@ -17,10 +19,7 @@ class Role extends \Spatie\Permission\Models\Role const HIERARCHY_TABLE = 'roles_hierarchy'; - /** - * @return string|\BackedEnum - */ - public function getNameAttribute() + public function getNameAttribute(): \BackedEnum|string { $name = $this->attributes['name']; @@ -31,10 +30,7 @@ public function getNameAttribute() return $name; } - /** - * @return BelongsToMany - */ - public function parents() + public function parents(): BelongsToMany { return $this->belongsToMany( static::class, @@ -43,10 +39,7 @@ public function parents() 'parent_id'); } - /** - * @return BelongsToMany - */ - public function children() + public function children(): BelongsToMany { return $this->belongsToMany( static::class, @@ -58,19 +51,19 @@ public function children() protected static function boot() { parent::boot(); - static::creating(function ($model) { + static::creating(static function ($model) { if (empty($model->{$model->getKeyName()})) { - $model->{$model->getKeyName()} = \Str::uuid()->toString(); + $model->{$model->getKeyName()} = Str::uuid()->toString(); } }); } - public function getIncrementing() + public function getIncrementing(): bool { return false; } - public function getKeyType() + public function getKeyType(): string { return 'string'; } diff --git a/tests/TestModels/SoftDeletingUser.php b/tests/TestModels/SoftDeletingUser.php index e31278519..a6efba029 100644 --- a/tests/TestModels/SoftDeletingUser.php +++ b/tests/TestModels/SoftDeletingUser.php @@ -8,5 +8,5 @@ class SoftDeletingUser extends User { use SoftDeletes; - protected $guard_name = 'web'; + protected string $guard_name = 'web'; } diff --git a/tests/TestModels/TestRolePermissionsEnum.php b/tests/TestModels/TestRolePermissionsEnum.php index 0f2badd42..7b3ba7485 100644 --- a/tests/TestModels/TestRolePermissionsEnum.php +++ b/tests/TestModels/TestRolePermissionsEnum.php @@ -2,6 +2,8 @@ namespace Spatie\Permission\Tests\TestModels; +use Illuminate\Support\Str; + /** * Enum example * @@ -53,6 +55,8 @@ public function label(): string self::VIEWARTICLES => 'View Articles', self::EDITARTICLES => 'Edit Articles', + + default => Str::words($this->name) }; } } diff --git a/tests/TestModels/WildcardPermission.php b/tests/TestModels/WildcardPermission.php index a6b7a6f10..6be905a24 100644 --- a/tests/TestModels/WildcardPermission.php +++ b/tests/TestModels/WildcardPermission.php @@ -9,9 +9,9 @@ class WildcardPermission extends BaseWildcardPermission /** @var string */ public const WILDCARD_TOKEN = '@'; - /** @var string */ + /** @var non-empty-string */ public const PART_DELIMITER = ':'; - /** @var string */ + /** @var non-empty-string */ public const SUBPART_DELIMITER = ';'; } diff --git a/tests/WildcardHasPermissionsTest.php b/tests/WildcardHasPermissionsTest.php index 8db36c63b..1abbc9e2a 100644 --- a/tests/WildcardHasPermissionsTest.php +++ b/tests/WildcardHasPermissionsTest.php @@ -2,6 +2,8 @@ namespace Spatie\Permission\Tests; +use PHPUnit\Framework\Attributes\RequiresPhp; +use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Exceptions\PermissionDoesNotExist; use Spatie\Permission\Exceptions\WildcardPermissionInvalidArgument; use Spatie\Permission\Exceptions\WildcardPermissionNotProperlyFormatted; @@ -12,6 +14,7 @@ class WildcardHasPermissionsTest extends TestCase { /** @test */ + #[Test] public function it_can_check_wildcard_permission() { app('config')->set('permission.enable_wildcard_permission', true); @@ -32,6 +35,7 @@ public function it_can_check_wildcard_permission() } /** @test */ + #[Test] public function it_can_check_wildcard_permission_for_a_non_default_guard() { app('config')->set('permission.enable_wildcard_permission', true); @@ -52,6 +56,7 @@ public function it_can_check_wildcard_permission_for_a_non_default_guard() } /** @test */ + #[Test] public function it_can_check_wildcard_permission_from_instance_without_explicit_guard_argument() { app('config')->set('permission.enable_wildcard_permission', true); @@ -77,6 +82,8 @@ public function it_can_check_wildcard_permission_from_instance_without_explicit_ * * @requires PHP >= 8.1 */ + #[RequiresPhp('>= 8.1')] + #[Test] public function it_can_assign_wildcard_permissions_using_enums() { app('config')->set('permission.enable_wildcard_permission', true); @@ -114,6 +121,7 @@ public function it_can_assign_wildcard_permissions_using_enums() } /** @test */ + #[Test] public function it_can_check_wildcard_permissions_via_roles() { app('config')->set('permission.enable_wildcard_permission', true); @@ -137,6 +145,7 @@ public function it_can_check_wildcard_permissions_via_roles() } /** @test */ + #[Test] public function it_can_check_custom_wildcard_permission() { app('config')->set('permission.enable_wildcard_permission', true); @@ -160,6 +169,7 @@ public function it_can_check_custom_wildcard_permission() } /** @test */ + #[Test] public function it_can_check_custom_wildcard_permissions_via_roles() { app('config')->set('permission.enable_wildcard_permission', true); @@ -186,6 +196,7 @@ public function it_can_check_custom_wildcard_permissions_via_roles() } /** @test */ + #[Test] public function it_can_check_non_wildcard_permissions() { app('config')->set('permission.enable_wildcard_permission', true); @@ -204,6 +215,7 @@ public function it_can_check_non_wildcard_permissions() } /** @test */ + #[Test] public function it_can_verify_complex_wildcard_permissions() { app('config')->set('permission.enable_wildcard_permission', true); @@ -224,6 +236,7 @@ public function it_can_verify_complex_wildcard_permissions() } /** @test */ + #[Test] public function it_throws_exception_when_wildcard_permission_is_not_properly_formatted() { app('config')->set('permission.enable_wildcard_permission', true); @@ -240,6 +253,7 @@ public function it_throws_exception_when_wildcard_permission_is_not_properly_for } /** @test */ + #[Test] public function it_can_verify_permission_instances_not_assigned_to_user() { app('config')->set('permission.enable_wildcard_permission', true); @@ -258,6 +272,7 @@ public function it_can_verify_permission_instances_not_assigned_to_user() } /** @test */ + #[Test] public function it_can_verify_permission_instances_assigned_to_user() { app('config')->set('permission.enable_wildcard_permission', true); @@ -276,6 +291,7 @@ public function it_can_verify_permission_instances_assigned_to_user() } /** @test */ + #[Test] public function it_can_verify_integers_as_strings() { app('config')->set('permission.enable_wildcard_permission', true); @@ -290,6 +306,7 @@ public function it_can_verify_integers_as_strings() } /** @test */ + #[Test] public function it_throws_exception_when_permission_has_invalid_arguments() { app('config')->set('permission.enable_wildcard_permission', true); @@ -302,6 +319,7 @@ public function it_throws_exception_when_permission_has_invalid_arguments() } /** @test */ + #[Test] public function it_throws_exception_when_permission_id_not_exists() { app('config')->set('permission.enable_wildcard_permission', true); diff --git a/tests/WildcardMiddlewareTest.php b/tests/WildcardMiddlewareTest.php index 596913707..9bb06873b 100644 --- a/tests/WildcardMiddlewareTest.php +++ b/tests/WildcardMiddlewareTest.php @@ -5,6 +5,7 @@ use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; +use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Exceptions\UnauthorizedException; use Spatie\Permission\Middleware\PermissionMiddleware; use Spatie\Permission\Middleware\RoleMiddleware; @@ -33,6 +34,7 @@ protected function setUp(): void } /** @test */ + #[Test] public function a_guest_cannot_access_a_route_protected_by_the_permission_middleware() { $this->assertEquals( @@ -42,6 +44,7 @@ public function a_guest_cannot_access_a_route_protected_by_the_permission_middle } /** @test */ + #[Test] public function a_user_can_access_a_route_protected_by_permission_middleware_if_have_this_permission() { Auth::login($this->testUser); @@ -57,6 +60,7 @@ public function a_user_can_access_a_route_protected_by_permission_middleware_if_ } /** @test */ + #[Test] public function a_user_can_access_a_route_protected_by_this_permission_middleware_if_have_one_of_the_permissions() { Auth::login($this->testUser); @@ -77,6 +81,7 @@ public function a_user_can_access_a_route_protected_by_this_permission_middlewar } /** @test */ + #[Test] public function a_user_cannot_access_a_route_protected_by_the_permission_middleware_if_have_a_different_permission() { Auth::login($this->testUser); @@ -92,6 +97,7 @@ public function a_user_cannot_access_a_route_protected_by_the_permission_middlew } /** @test */ + #[Test] public function a_user_cannot_access_a_route_protected_by_permission_middleware_if_have_not_permissions() { Auth::login($this->testUser); @@ -103,6 +109,7 @@ public function a_user_cannot_access_a_route_protected_by_permission_middleware_ } /** @test */ + #[Test] public function a_user_can_access_a_route_protected_by_permission_or_role_middleware_if_has_this_permission_or_role() { Auth::login($this->testUser); @@ -139,6 +146,7 @@ public function a_user_can_access_a_route_protected_by_permission_or_role_middle } /** @test */ + #[Test] public function the_required_permissions_can_be_fetched_from_the_exception() { Auth::login($this->testUser); diff --git a/tests/WildcardRoleTest.php b/tests/WildcardRoleTest.php index da674c6f6..d0e9212f5 100644 --- a/tests/WildcardRoleTest.php +++ b/tests/WildcardRoleTest.php @@ -2,6 +2,7 @@ namespace Spatie\Permission\Tests; +use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Models\Permission; class WildcardRoleTest extends TestCase @@ -18,6 +19,7 @@ protected function setUp(): void } /** @test */ + #[Test] public function it_can_be_given_a_permission() { Permission::create(['name' => 'posts.*']); @@ -27,6 +29,7 @@ public function it_can_be_given_a_permission() } /** @test */ + #[Test] public function it_can_be_given_multiple_permissions_using_an_array() { Permission::create(['name' => 'posts.*']); @@ -39,6 +42,7 @@ public function it_can_be_given_multiple_permissions_using_an_array() } /** @test */ + #[Test] public function it_can_be_given_multiple_permissions_using_multiple_arguments() { Permission::create(['name' => 'posts.*']); @@ -51,6 +55,7 @@ public function it_can_be_given_multiple_permissions_using_multiple_arguments() } /** @test */ + #[Test] public function it_can_be_given_a_permission_using_objects() { $this->testUserRole->givePermissionTo($this->testUserPermission); @@ -59,18 +64,21 @@ public function it_can_be_given_a_permission_using_objects() } /** @test */ + #[Test] public function it_returns_false_if_it_does_not_have_the_permission() { $this->assertFalse($this->testUserRole->hasPermissionTo('other-permission')); } /** @test */ + #[Test] public function it_returns_false_if_permission_does_not_exists() { $this->assertFalse($this->testUserRole->hasPermissionTo('doesnt-exist')); } /** @test */ + #[Test] public function it_returns_false_if_it_does_not_have_a_permission_object() { $permission = app(Permission::class)->findByName('other-permission'); @@ -79,6 +87,7 @@ public function it_returns_false_if_it_does_not_have_a_permission_object() } /** @test */ + #[Test] public function it_creates_permission_object_with_findOrCreate_if_it_does_not_have_a_permission_object() { $permission = app(Permission::class)->findOrCreate('another-permission'); @@ -93,6 +102,7 @@ public function it_creates_permission_object_with_findOrCreate_if_it_does_not_ha } /** @test */ + #[Test] public function it_returns_false_when_a_permission_of_the_wrong_guard_is_passed_in() { $permission = app(Permission::class)->findByName('wrong-guard-permission', 'admin'); diff --git a/tests/WildcardRouteTest.php b/tests/WildcardRouteTest.php index f56dfcb6c..65e3c437c 100644 --- a/tests/WildcardRouteTest.php +++ b/tests/WildcardRouteTest.php @@ -2,9 +2,12 @@ namespace Spatie\Permission\Tests; +use PHPUnit\Framework\Attributes\Test; + class WildcardRouteTest extends TestCase { /** @test */ + #[Test] public function test_permission_function() { app('config')->set('permission.enable_wildcard_permission', true); @@ -19,6 +22,7 @@ public function test_permission_function() } /** @test */ + #[Test] public function test_role_and_permission_function_together() { app('config')->set('permission.enable_wildcard_permission', true); From 6c7b1de8027cee9efdc0fb1f8b50ef9b04ae9300 Mon Sep 17 00:00:00 2001 From: Jason/CrossPlatform <68124218+crossplatformconsulting@users.noreply.github.com> Date: Thu, 13 Feb 2025 21:56:53 +0200 Subject: [PATCH 0945/1013] LDAP model lookup from Auth Provider (#2750) --------- Co-authored-by: Chris Brown --- src/Guard.php | 45 ++++++++++++++++++++++++++++++++++++++++++++- src/helpers.php | 7 +++---- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/Guard.php b/src/Guard.php index 4f697ce20..55c5d68a3 100644 --- a/src/Guard.php +++ b/src/Guard.php @@ -38,6 +38,25 @@ public static function getNames($model): Collection return self::getConfigAuthGuards($class); } + /** + * Get the model class associated with a given provider. + * + * @param string $provider + * @return string|null + */ + protected static function getProviderModel(string $provider): ?string + { + // Get the provider configuration + $providerConfig = config("auth.providers.{$provider}"); + + // Handle LDAP provider or standard Eloquent provider + if (isset($providerConfig['driver']) && $providerConfig['driver'] === 'ldap') { + return $providerConfig['database']['model'] ?? null; + } + + return $providerConfig['model'] ?? null; + } + /** * Get list of relevant guards for the $class model based on config(auth) settings. * @@ -50,11 +69,35 @@ public static function getNames($model): Collection protected static function getConfigAuthGuards(string $class): Collection { return collect(config('auth.guards')) - ->map(fn ($guard) => isset($guard['provider']) ? config("auth.providers.{$guard['provider']}.model") : null) + ->map(function ($guard) { + if (!isset($guard['provider'])) { + return null; + } + + return static::getProviderModel($guard['provider']); + }) ->filter(fn ($model) => $class === $model) ->keys(); } + /** + * Get the model associated with a given guard name. + * + * @param string $guard + * @return string|null + */ + public static function getModelForGuard(string $guard): ?string + { + // Get the provider configuration for the given guard + $provider = config("auth.guards.{$guard}.provider"); + + if (!$provider) { + return null; + } + + return static::getProviderModel($provider); + } + /** * Lookup a guard name relevant for the $class model and the current user. * diff --git a/src/helpers.php b/src/helpers.php index 55048d753..cf7d38873 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -4,12 +4,11 @@ /** * @return string|null */ - function getModelForGuard(string $guard) + function getModelForGuard(string $guard): ?string { - return collect(config('auth.guards')) - ->map(fn ($guard) => isset($guard['provider']) ? config("auth.providers.{$guard['provider']}.model") : null) - ->get($guard); + return Spatie\Permission\Guard::getModelForGuard($guard); } + } if (! function_exists('setPermissionsTeamId')) { From addf63f14c037afc16298724f2fa69bc010cd00d Mon Sep 17 00:00:00 2001 From: drbyte <404472+drbyte@users.noreply.github.com> Date: Thu, 13 Feb 2025 19:57:18 +0000 Subject: [PATCH 0946/1013] Fix styling --- src/Guard.php | 10 ++-------- src/helpers.php | 3 --- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/Guard.php b/src/Guard.php index 55c5d68a3..3e157be63 100644 --- a/src/Guard.php +++ b/src/Guard.php @@ -40,9 +40,6 @@ public static function getNames($model): Collection /** * Get the model class associated with a given provider. - * - * @param string $provider - * @return string|null */ protected static function getProviderModel(string $provider): ?string { @@ -70,7 +67,7 @@ protected static function getConfigAuthGuards(string $class): Collection { return collect(config('auth.guards')) ->map(function ($guard) { - if (!isset($guard['provider'])) { + if (! isset($guard['provider'])) { return null; } @@ -82,16 +79,13 @@ protected static function getConfigAuthGuards(string $class): Collection /** * Get the model associated with a given guard name. - * - * @param string $guard - * @return string|null */ public static function getModelForGuard(string $guard): ?string { // Get the provider configuration for the given guard $provider = config("auth.guards.{$guard}.provider"); - if (!$provider) { + if (! $provider) { return null; } diff --git a/src/helpers.php b/src/helpers.php index cf7d38873..e7d0e18a4 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -1,9 +1,6 @@ Date: Thu, 13 Feb 2025 20:01:33 +0000 Subject: [PATCH 0947/1013] Update CHANGELOG --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21c9539df..b44056294 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.14.0 - 2025-02-13 + +### What's Changed + +* LDAP model lookup from Auth Provider by @crossplatformconsulting in https://github.com/spatie/laravel-permission/pull/2750 + +### Internals + +* Add PHPUnit annotations, for future compatibility with PHPUnit 12 by @drbyte in https://github.com/spatie/laravel-permission/pull/2806 + +### New Contributors + +* @crossplatformconsulting made their first contribution in https://github.com/spatie/laravel-permission/pull/2750 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.13.0...6.14.0 + ## 6.13.0 - 2025-02-05 ### What's Changed @@ -947,6 +963,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` @@ -1021,6 +1038,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` From d1eab370413b522819315c9883b76b6e004d2eb4 Mon Sep 17 00:00:00 2001 From: Sven Wegner <139229112+sven-wegner@users.noreply.github.com> Date: Sun, 16 Feb 2025 18:56:49 +0100 Subject: [PATCH 0948/1013] 4 events for adding and removing roles or permissions (#2742) * Added Events `PermissionAttached`, `PermissionDetached`, `RoleAttached` and `RoleDetached` --------- Co-authored-by: Sven Wegner Co-authored-by: Chris Brown --- config/permission.php | 11 ++++++ src/Events/PermissionAttached.php | 28 ++++++++++++++ src/Events/PermissionDetached.php | 28 ++++++++++++++ src/Events/RoleAttached.php | 28 ++++++++++++++ src/Events/RoleDetached.php | 28 ++++++++++++++ src/Traits/HasPermissions.php | 14 ++++++- src/Traits/HasRoles.php | 14 ++++++- tests/HasPermissionsTest.php | 62 +++++++++++++++++++++++++++++++ tests/HasRolesTest.php | 42 +++++++++++++++++++++ 9 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 src/Events/PermissionAttached.php create mode 100644 src/Events/PermissionDetached.php create mode 100644 src/Events/RoleAttached.php create mode 100644 src/Events/RoleDetached.php diff --git a/config/permission.php b/config/permission.php index c3b69a5ec..8e84e9d53 100644 --- a/config/permission.php +++ b/config/permission.php @@ -110,6 +110,17 @@ */ 'register_octane_reset_listener' => false, + /* + * Events will fire when a role or permission is assigned/unassigned: + * \Spatie\Permission\Events\RoleAttached + * \Spatie\Permission\Events\RoleDetached + * \Spatie\Permission\Events\PermissionAttached + * \Spatie\Permission\Events\PermissionDetached + * + * To enable, set to true, and then create listeners to watch these events. + */ + 'events_enabled' => false, + /* * Teams Feature. * When set to true the package implements teams using the 'team_foreign_key'. diff --git a/src/Events/PermissionAttached.php b/src/Events/PermissionAttached.php new file mode 100644 index 000000000..c7734f382 --- /dev/null +++ b/src/Events/PermissionAttached.php @@ -0,0 +1,28 @@ +forgetCachedPermissions(); } + if (config('permission.events_enabled')) { + event(new PermissionAttached($this->getModel(), $permissions)); + } + $this->forgetWildcardPermissionIndex(); return $this; @@ -460,12 +466,18 @@ public function syncPermissions(...$permissions) */ public function revokePermissionTo($permission) { - $this->permissions()->detach($this->getStoredPermission($permission)); + $storedPermission = $this->getStoredPermission($permission); + + $this->permissions()->detach($storedPermission); if (is_a($this, Role::class)) { $this->forgetCachedPermissions(); } + if (config('permission.events_enabled')) { + event(new PermissionDetached($this->getModel(), $storedPermission)); + } + $this->forgetWildcardPermissionIndex(); $this->unsetRelation('permissions'); diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 409f1636c..149303a19 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -8,6 +8,8 @@ use Illuminate\Support\Collection; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Contracts\Role; +use Spatie\Permission\Events\RoleAttached; +use Spatie\Permission\Events\RoleDetached; use Spatie\Permission\PermissionRegistrar; trait HasRoles @@ -176,6 +178,10 @@ function ($object) use ($roles, $model, $teamPivot, &$saved) { $this->forgetCachedPermissions(); } + if (config('permission.events_enabled')) { + event(new RoleAttached($this->getModel(), $roles)); + } + return $this; } @@ -186,7 +192,9 @@ function ($object) use ($roles, $model, $teamPivot, &$saved) { */ public function removeRole($role) { - $this->roles()->detach($this->getStoredRole($role)); + $storedRole = $this->getStoredRole($role); + + $this->roles()->detach($storedRole); $this->unsetRelation('roles'); @@ -194,6 +202,10 @@ public function removeRole($role) $this->forgetCachedPermissions(); } + if (config('permission.events_enabled')) { + event(new RoleDetached($this->getModel(), $storedRole)); + } + return $this; } diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index 0e2de3aa7..d4242a553 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -3,11 +3,15 @@ namespace Spatie\Permission\Tests; use DB; +use Illuminate\Support\Facades\Event; use Illuminate\Database\Eloquent\Model; use PHPUnit\Framework\Attributes\RequiresPhp; use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Contracts\Role; +use Spatie\Permission\Events\PermissionAttached; +use Spatie\Permission\Events\PermissionDetached; +use Spatie\Permission\Events\RoleAttached; use Spatie\Permission\Exceptions\GuardDoesNotMatch; use Spatie\Permission\Exceptions\PermissionDoesNotExist; use Spatie\Permission\Tests\TestModels\SoftDeletingUser; @@ -824,6 +828,64 @@ public function it_can_reject_permission_based_on_logged_in_user_guard() ]); } + /** @test */ + #[Test] + public function it_fires_an_event_when_a_permission_is_added() + { + Event::fake(); + app('config')->set('permission.events_enabled', true); + + $this->testUser->givePermissionTo(['edit-articles', 'edit-news']); + + $ids = app(Permission::class)::whereIn('name', ['edit-articles', 'edit-news']) + ->pluck($this->testUserPermission->getKeyName()) + ->toArray(); + + Event::assertDispatched(PermissionAttached::class, function ($event) use ($ids) { + return $event->model instanceof User + && $event->model->hasPermissionTo('edit-news') + && $event->model->hasPermissionTo('edit-articles') + && $ids === $event->permissionsOrIds; + }); + } + + /** @test */ + #[Test] + public function it_does_not_fire_an_event_when_events_are_not_enabled() + { + Event::fake(); + app('config')->set('permission.events_enabled', false); + + $this->testUser->givePermissionTo(['edit-articles', 'edit-news']); + + $ids = app(Permission::class)::whereIn('name', ['edit-articles', 'edit-news']) + ->pluck($this->testUserPermission->getKeyName()) + ->toArray(); + + Event::assertNotDispatched(PermissionAttached::class); + } + + /** @test */ + #[Test] + public function it_fires_an_event_when_a_permission_is_removed() + { + Event::fake(); + app('config')->set('permission.events_enabled', true); + + $permissions = app(Permission::class)::whereIn('name', ['edit-articles', 'edit-news'])->get(); + + $this->testUser->givePermissionTo($permissions); + + $this->testUser->revokePermissionTo($permissions); + + Event::assertDispatched(PermissionDetached::class, function ($event) use ($permissions) { + return $event->model instanceof User + && !$event->model->hasPermissionTo('edit-news') + && !$event->model->hasPermissionTo('edit-articles') + && $event->permissionsOrIds === $permissions; + }); + } + /** @test */ #[Test] public function it_can_be_given_a_permission_on_role_when_lazy_loading_is_restricted() diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 5777f7bdb..2401b8dd7 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -4,10 +4,13 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Event; use PHPUnit\Framework\Attributes\RequiresPhp; use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Contracts\Role; +use Spatie\Permission\Events\RoleAttached; +use Spatie\Permission\Events\RoleDetached; use Spatie\Permission\Exceptions\GuardDoesNotMatch; use Spatie\Permission\Exceptions\RoleDoesNotExist; use Spatie\Permission\Tests\TestModels\Admin; @@ -917,6 +920,45 @@ public function it_does_not_detach_roles_when_user_soft_deleting() $this->assertTrue($user->hasRole('testRole')); } + /** @test */ + #[Test] + public function it_fires_an_event_when_a_role_is_added() + { + Event::fake(); + app('config')->set('permission.events_enabled', true); + + $this->testUser->assignRole(['testRole', 'testRole2']); + + $roleIds = app(Role::class)::whereIn('name', ['testRole', 'testRole2']) + ->pluck($this->testUserRole->getKeyName()) + ->toArray(); + + Event::assertDispatched(RoleAttached::class, function ($event) use ($roleIds) { + return $event->model instanceof User + && $event->model->hasRole('testRole') + && $event->model->hasRole('testRole2') + && $event->rolesOrIds === $roleIds; + }); + } + + /** @test */ + #[Test] + public function it_fires_an_event_when_a_role_is_removed() + { + Event::fake(); + app('config')->set('permission.events_enabled', true); + + $this->testUser->assignRole('testRole'); + + $this->testUser->removeRole('testRole'); + + Event::assertDispatched(RoleDetached::class, function ($event) { + return $event->model instanceof User + && !$event->model->hasRole('testRole') + && $event->rolesOrIds->name === 'testRole'; + }); + } + /** @test */ #[Test] public function it_can_be_given_a_role_on_permission_when_lazy_loading_is_restricted() From 062e08bfaf2b62a81209c489352bc8bfceb3b919 Mon Sep 17 00:00:00 2001 From: drbyte <404472+drbyte@users.noreply.github.com> Date: Sun, 16 Feb 2025 17:57:09 +0000 Subject: [PATCH 0949/1013] Fix styling --- tests/HasPermissionsTest.php | 15 +++++++-------- tests/HasRolesTest.php | 8 ++++---- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index d4242a553..77e909ca8 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -3,15 +3,14 @@ namespace Spatie\Permission\Tests; use DB; -use Illuminate\Support\Facades\Event; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Event; use PHPUnit\Framework\Attributes\RequiresPhp; use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Contracts\Permission; use Spatie\Permission\Contracts\Role; use Spatie\Permission\Events\PermissionAttached; use Spatie\Permission\Events\PermissionDetached; -use Spatie\Permission\Events\RoleAttached; use Spatie\Permission\Exceptions\GuardDoesNotMatch; use Spatie\Permission\Exceptions\PermissionDoesNotExist; use Spatie\Permission\Tests\TestModels\SoftDeletingUser; @@ -834,7 +833,7 @@ public function it_fires_an_event_when_a_permission_is_added() { Event::fake(); app('config')->set('permission.events_enabled', true); - + $this->testUser->givePermissionTo(['edit-articles', 'edit-news']); $ids = app(Permission::class)::whereIn('name', ['edit-articles', 'edit-news']) @@ -855,7 +854,7 @@ public function it_does_not_fire_an_event_when_events_are_not_enabled() { Event::fake(); app('config')->set('permission.events_enabled', false); - + $this->testUser->givePermissionTo(['edit-articles', 'edit-news']); $ids = app(Permission::class)::whereIn('name', ['edit-articles', 'edit-news']) @@ -871,7 +870,7 @@ public function it_fires_an_event_when_a_permission_is_removed() { Event::fake(); app('config')->set('permission.events_enabled', true); - + $permissions = app(Permission::class)::whereIn('name', ['edit-articles', 'edit-news'])->get(); $this->testUser->givePermissionTo($permissions); @@ -880,12 +879,12 @@ public function it_fires_an_event_when_a_permission_is_removed() Event::assertDispatched(PermissionDetached::class, function ($event) use ($permissions) { return $event->model instanceof User - && !$event->model->hasPermissionTo('edit-news') - && !$event->model->hasPermissionTo('edit-articles') + && ! $event->model->hasPermissionTo('edit-news') + && ! $event->model->hasPermissionTo('edit-articles') && $event->permissionsOrIds === $permissions; }); } - + /** @test */ #[Test] public function it_can_be_given_a_permission_on_role_when_lazy_loading_is_restricted() diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 2401b8dd7..31356e8fc 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -926,7 +926,7 @@ public function it_fires_an_event_when_a_role_is_added() { Event::fake(); app('config')->set('permission.events_enabled', true); - + $this->testUser->assignRole(['testRole', 'testRole2']); $roleIds = app(Role::class)::whereIn('name', ['testRole', 'testRole2']) @@ -947,18 +947,18 @@ public function it_fires_an_event_when_a_role_is_removed() { Event::fake(); app('config')->set('permission.events_enabled', true); - + $this->testUser->assignRole('testRole'); $this->testUser->removeRole('testRole'); Event::assertDispatched(RoleDetached::class, function ($event) { return $event->model instanceof User - && !$event->model->hasRole('testRole') + && ! $event->model->hasRole('testRole') && $event->rolesOrIds->name === 'testRole'; }); } - + /** @test */ #[Test] public function it_can_be_given_a_role_on_permission_when_lazy_loading_is_restricted() From 18825c62216fbe4833f9bf4fd5726924f4f477da Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sun, 16 Feb 2025 13:12:40 -0500 Subject: [PATCH 0950/1013] Set a query count var for incrementing in future tests; simplifies future refactoring --- tests/HasPermissionsTest.php | 4 +++- tests/HasRolesTest.php | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index 77e909ca8..49f450359 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -708,7 +708,9 @@ public function it_does_not_run_unnecessary_sqls_when_assigning_new_permissions( $this->testUser->syncPermissions($this->testUserPermission, $permission2); DB::disableQueryLog(); - $this->assertSame(2, count(DB::getQueryLog())); // avoid unnecessary sqls + $necessaryQueriesCount = 2; + + $this->assertCount($necessaryQueriesCount, DB::getQueryLog()); } /** @test */ diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 31356e8fc..40a719343 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -393,7 +393,9 @@ public function it_does_not_run_unnecessary_sqls_when_assigning_new_roles() $this->testUser->syncRoles($this->testUserRole, $role2); DB::disableQueryLog(); - $this->assertSame(2, count(DB::getQueryLog())); // avoid unnecessary sqls + $necessaryQueriesCount = 2; + + $this->assertCount($necessaryQueriesCount, DB::getQueryLog()); } /** @test */ From 7dee857091ae65e98c51875ec89607f87ff03c68 Mon Sep 17 00:00:00 2001 From: Mohammed DS <92916932+mohamedds-12@users.noreply.github.com> Date: Sun, 16 Feb 2025 19:46:57 +0100 Subject: [PATCH 0951/1013] Fixed bug of loading user roles of different teams to current team (#2803) * Fixed calling $this->roles property which could load old roles * Added reloading roles before asigning role if teams feature is enabled --------- Co-authored-by: Chris Brown --- src/Traits/HasRoles.php | 5 +++++ tests/HasRolesTest.php | 6 ++++++ tests/TeamHasRolesTest.php | 5 +++++ 3 files changed, 16 insertions(+) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 149303a19..d10dd77e3 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -154,6 +154,11 @@ public function assignRole(...$roles) [app(PermissionRegistrar::class)->teamsKey => getPermissionsTeamId()] : []; if ($model->exists) { + if (app(PermissionRegistrar::class)->teams) { + // explicit reload in case team has been changed since last load + $this->load('roles'); + } + $currentRoles = $this->roles->map(fn ($role) => $role->getKey())->toArray(); $this->roles()->attach(array_diff($roles, $currentRoles), $teamPivot); diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 40a719343..696fe8525 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -13,6 +13,7 @@ use Spatie\Permission\Events\RoleDetached; use Spatie\Permission\Exceptions\GuardDoesNotMatch; use Spatie\Permission\Exceptions\RoleDoesNotExist; +use Spatie\Permission\PermissionRegistrar; use Spatie\Permission\Tests\TestModels\Admin; use Spatie\Permission\Tests\TestModels\SoftDeletingUser; use Spatie\Permission\Tests\TestModels\User; @@ -394,6 +395,11 @@ public function it_does_not_run_unnecessary_sqls_when_assigning_new_roles() DB::disableQueryLog(); $necessaryQueriesCount = 2; + + // Teams reloads relation, adding an extra query + if (app(PermissionRegistrar::class)->teams) { + $necessaryQueriesCount++; + } $this->assertCount($necessaryQueriesCount, DB::getQueryLog()); } diff --git a/tests/TeamHasRolesTest.php b/tests/TeamHasRolesTest.php index 928ed7419..9f76da49b 100644 --- a/tests/TeamHasRolesTest.php +++ b/tests/TeamHasRolesTest.php @@ -55,6 +55,11 @@ public function it_can_assign_same_and_different_roles_on_same_user_different_te setPermissionsTeamId(1); $this->testUser->assignRole('testRole', 'testRole2'); + // explicit load of roles to assert no mismatch + // when same role assigned in diff teams + // while old team's roles are loaded + $this->testUser->load('roles'); + setPermissionsTeamId(2); $this->testUser->assignRole('testRole', 'testRole3'); From fa5ea338fd5d88dd554dad662bebe91e234f06da Mon Sep 17 00:00:00 2001 From: drbyte <404472+drbyte@users.noreply.github.com> Date: Sun, 16 Feb 2025 18:47:21 +0000 Subject: [PATCH 0952/1013] Fix styling --- tests/HasRolesTest.php | 2 +- tests/TeamHasRolesTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 696fe8525..7f8a137e0 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -395,7 +395,7 @@ public function it_does_not_run_unnecessary_sqls_when_assigning_new_roles() DB::disableQueryLog(); $necessaryQueriesCount = 2; - + // Teams reloads relation, adding an extra query if (app(PermissionRegistrar::class)->teams) { $necessaryQueriesCount++; diff --git a/tests/TeamHasRolesTest.php b/tests/TeamHasRolesTest.php index 9f76da49b..113bdc5aa 100644 --- a/tests/TeamHasRolesTest.php +++ b/tests/TeamHasRolesTest.php @@ -57,7 +57,7 @@ public function it_can_assign_same_and_different_roles_on_same_user_different_te // explicit load of roles to assert no mismatch // when same role assigned in diff teams - // while old team's roles are loaded + // while old team's roles are loaded $this->testUser->load('roles'); setPermissionsTeamId(2); From 86074fcfd127f9fa7bcdf550b7c938e3beb0d65f Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 17 Feb 2025 14:18:01 -0500 Subject: [PATCH 0953/1013] Updates to example app setup --- docs/basic-usage/new-app.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index 7f1ed8361..c08978344 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -19,8 +19,9 @@ laravel new mypermissionsdemo # (Choose Laravel Breeze, choose Blade with Alpine) # (choose your own dark-mode-support choice) # (choose your desired testing framework) -# (say Yes to initialize a Git repo, so that you can track your code changes) +# (If offered, say Yes to initialize a Git repo, so that you can track your code changes) # (Choose SQLite) +# (say Yes to run default database migrations) cd mypermissionsdemo @@ -52,7 +53,7 @@ If you didn't install Laravel Breeze or Jetstream, add Laravel's basic auth scaf ```php composer require laravel/ui --dev php artisan ui bootstrap --auth -# npm install && npm run prod +# npm install && npm run build git add . && git commit -m "Setup auth scaffold" ``` From c528d29aeff7fbf60b4d4f04d69ca73af35c363d Mon Sep 17 00:00:00 2001 From: drbyte <404472+drbyte@users.noreply.github.com> Date: Mon, 17 Feb 2025 19:20:28 +0000 Subject: [PATCH 0954/1013] Update CHANGELOG --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b44056294..a4a056de3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.15.0 - 2025-02-17 + +### What's Changed + +* Added 4 events for adding and removing roles or permissions by @sven-wegner in https://github.com/spatie/laravel-permission/pull/2742 +* Fixed bug of loading user roles of different teams to current team by @mohamedds-12 in https://github.com/spatie/laravel-permission/pull/2803 + +### New Contributors + +* @sven-wegner made their first contribution in https://github.com/spatie/laravel-permission/pull/2742 +* @mohamedds-12 made their first contribution in https://github.com/spatie/laravel-permission/pull/2803 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.14.0...6.15.0 + ## 6.14.0 - 2025-02-13 ### What's Changed @@ -964,6 +978,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` @@ -1039,6 +1054,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` From 43a7ea2d25ba8c128f22da049b3f46532b7f9426 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 17 Feb 2025 14:29:29 -0500 Subject: [PATCH 0955/1013] [Docs] Create events.md --- docs/advanced-usage/events.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 docs/advanced-usage/events.md diff --git a/docs/advanced-usage/events.md b/docs/advanced-usage/events.md new file mode 100644 index 000000000..04682d1ac --- /dev/null +++ b/docs/advanced-usage/events.md @@ -0,0 +1,21 @@ +--- +title: Events +weight: 5 +--- + +By default Events are not enabled, because not all apps need to fire events related to roles and permissions. + +However, you may enable events by setting the `events_enabled => true` in `config/permission.php` + +## Available Events + +The following events are available since `v6.15.0`: + +``` +\Spatie\Permission\Events\RoleAttached::class +\Spatie\Permission\Events\RoleDetached::class +\Spatie\Permission\Events\PermissionAttached::class +\Spatie\Permission\Events\PermissionDetached::class +``` +Note that the events can receive the role or permission details as a model ID or as an Eloquent record, or as an array or collection of ids or records. Be sure to inspect the parameter before acting on it. + From de5893ec02325ba50f1f72e9275fc9c73ea5bdfe Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 20 Feb 2025 22:51:46 -0500 Subject: [PATCH 0956/1013] Bump PHP version --- .github/workflows/test-cache-drivers.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-cache-drivers.yml b/.github/workflows/test-cache-drivers.yml index 403f23db6..2bfd5b28a 100644 --- a/.github/workflows/test-cache-drivers.yml +++ b/.github/workflows/test-cache-drivers.yml @@ -26,7 +26,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.2 + php-version: 8.4 extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv, memcache coverage: none From f9b692dfd06b54b3e206f062f2e874c9783cd868 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 20 Feb 2025 22:53:20 -0500 Subject: [PATCH 0957/1013] Bump PHP version --- .github/workflows/phpstan.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 75eb21d8d..be7882397 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -20,7 +20,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.3 + php-version: 8.4 coverage: none - name: Install composer dependencies From 402cb42aba8a24fec11f6a989b5d815d09d365fc Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Mon, 24 Feb 2025 20:28:05 -0500 Subject: [PATCH 0958/1013] Update example app instruction --- docs/basic-usage/new-app.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md index c08978344..04cf9d212 100644 --- a/docs/basic-usage/new-app.md +++ b/docs/basic-usage/new-app.md @@ -16,16 +16,17 @@ If you're new to Laravel or to any of the concepts mentioned here, you can learn ```sh cd ~/Sites laravel new mypermissionsdemo -# (Choose Laravel Breeze, choose Blade with Alpine) -# (choose your own dark-mode-support choice) -# (choose your desired testing framework) +# (No Starter Kit is needed, but you could go with Livewire or Breeze/Jetstream, with Laravel's Built-In-Auth; or use Bootstrap using laravel/ui described later, below) +# (You might be asked to select a dark-mode-support choice) +# (Choose your desired testing framework: Pest or PHPUnit) # (If offered, say Yes to initialize a Git repo, so that you can track your code changes) -# (Choose SQLite) -# (say Yes to run default database migrations) +# (If offered a database selection, choose SQLite, because it is simplest for test scenarios) +# (If prompted, say Yes to run default database migrations) +# (If prompted, say Yes to run npm install and related commands) cd mypermissionsdemo -# the following git commands are not needed if you Initialized a git repo while "laravel new" was running above: +# The following git commands are not needed if you Initialized a git repo while "laravel new" was running above: git init git add . git commit -m "Fresh Laravel Install" @@ -49,7 +50,8 @@ sed -i '' $'s/use HasApiTokens, HasFactory, Notifiable;/use HasApiTokens, HasFac git add . && git commit -m "Add HasRoles trait" ``` -If you didn't install Laravel Breeze or Jetstream, add Laravel's basic auth scaffolding: +If you didn't install a Starter Kit like Livewire or Breeze or Jetstream, add Laravel's basic auth scaffolding: +This Auth scaffolding will make it simpler to provide login capability for a test/demo user, and test roles/permissions with them. ```php composer require laravel/ui --dev php artisan ui bootstrap --auth From aa08de37c0373a9eb3807980c38b5bff20cfb170 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 26 Feb 2025 18:04:21 -0500 Subject: [PATCH 0959/1013] Relocate compatibility chart to Prerequisites page --- docs/installation-laravel.md | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index c3a6646ce..951690478 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -5,18 +5,7 @@ weight: 4 ## Laravel Version Compatibility -Choose the version of this package that suits your Laravel version. - -Package Version | Laravel Version -----------------|----------- - ^6.0 | 8,9,10,11,12 (PHP 8.0+) - ^5.8 | 7,8,9,10 - ^5.7 | 7,8,9 - ^5.4-^5.6 | 7,8 - 5.0-5.3 | 6,7,8 - ^4 | 6,7,8 - ^3 | 5.8 - +See the "Prerequisites" documentation page for compatibility details. ## Installing From 835ac8b8bcc6a12e5722e2a82fc5dc688c64b5c8 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 26 Feb 2025 18:17:58 -0500 Subject: [PATCH 0960/1013] Relocate version compatibility to Prerequisites page --- docs/prerequisites.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/prerequisites.md b/docs/prerequisites.md index 0b0cf300b..f566be624 100644 --- a/docs/prerequisites.md +++ b/docs/prerequisites.md @@ -3,10 +3,18 @@ title: Prerequisites weight: 3 --- -## Laravel Version - -This package can be used in Laravel 6 or higher. Check the "Installing on Laravel" page for package versions compatible with various Laravel versions. - +## Laravel Version Compatibility + +Laravel Version | Package Version +----------------|----------- + 8,9,10,11,12 | `^6.0` (PHP 8.0+) + 7,8,9,10 | `^5.8` + 7,8,9 | `^5.7` + 7,8 | `^5.4`-`^5.6` + 6,7,8 | `^5.0`-`^5.3` + 6,7,8 | `^4` + 5.8 | `^3` + ## User Model / Contract/Interface This package uses Laravel's Gate layer to provide Authorization capabilities. From 2c9ae7f76c9af581bcefcb81176d3255e2ebffaa Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 26 Feb 2025 18:26:33 -0500 Subject: [PATCH 0961/1013] Clarify relation name limitations --- docs/prerequisites.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/prerequisites.md b/docs/prerequisites.md index f566be624..c4be5cb88 100644 --- a/docs/prerequisites.md +++ b/docs/prerequisites.md @@ -37,23 +37,23 @@ class User extends Authenticatable } ``` -## Must not have a [role] or [roles] property, nor a [roles()] method +## Must not have a [role] or [roles] property/relation, nor a [roles()] method -Your `User` model/object MUST NOT have a `role` or `roles` property (or field in the database by that name), nor a `roles()` method on it. Those will interfere with the properties and methods added by the `HasRoles` trait provided by this package, thus causing unexpected outcomes when this package's methods are used to inspect roles and permissions. +Your `User` model/object MUST NOT have a `role` or `roles` property (or field in the database by that name), nor a `roles()` method on it (nor a `roles` relation). Those will interfere with the properties and methods and relations added by the `HasRoles` trait provided by this package, thus causing unexpected outcomes when this package's methods are used to inspect roles and permissions. -## Must not have a [permission] or [permissions] property, nor a [permissions()] method +## Must not have a [permission] or [permissions] property/relation, nor a [permissions()] method -Your `User` model/object MUST NOT have a `permission` or `permissions` property (or field in the database by that name), nor a `permissions()` method on it. Those will interfere with the properties and methods added by the `HasPermissions` trait provided by this package (which is invoked via the `HasRoles` trait). +Your `User` model/object MUST NOT have a `permission` or `permissions` property (or field in the database by that name), nor a `permissions()` method on it (nor a `permissions` relation). Those will interfere with the properties and methods and relations added by the `HasPermissions` trait provided by this package (which is invoked via the `HasRoles` trait). ## Config file This package publishes a `config/permission.php` file. If you already have a file by that name, you must rename or remove it, as it will conflict with this package. You could optionally merge your own values with those required by this package, as long as the keys that this package expects are present. See the source file for more details. -## Schema Limitation in MySQL +## Database Schema Limitations Potential error message: "1071 Specified key was too long; max key length is 1000 bytes" -MySQL 8.0 limits index key lengths, which might be too short for some compound indexes used by this package. +MySQL 8.0+ limits index key lengths, which might be too short for some compound indexes used by this package. This package publishes a migration which combines multiple columns in a single index. With `utf8mb4` the 4-bytes-per-character requirement of `mb4` means the total length of the columns in the hybrid index can only be `25%` of that maximum index length. - MyISAM tables limit the index to 1000 characters (which is only 250 total chars in `utf8mb4`) @@ -64,7 +64,7 @@ Depending on your MySQL or MariaDB configuration, you may implement one of the f 1. Ideally, configure the database to use InnoDB by default, and use ROW FORMAT of 'Dynamic' by default for all new tables. (See [MySQL](https://dev.mysql.com/doc/refman/8.0/en/innodb-limits.html) and [MariaDB](https://mariadb.com/kb/en/innodb-dynamic-row-format/) docs.) -2. OR if your app doesn't require a longer default, in your AppServiceProvider you can set `Schema::defaultStringLength(125)`. [See the Laravel Docs for instructions](https://laravel.com/docs/migrations#index-lengths-mysql-mariadb). This will have Laravel set all strings to 125 characters by default. +2. OR if your app doesn't require a longer default, in your AppServiceProvider you can set `Schema::defaultStringLength(125)`. [See the Laravel Docs for instructions](https://laravel.com/docs/10.x/migrations#index-lengths-mysql-mariadb). This will have Laravel set all strings to 125 characters by default. 3. OR you could edit the migration and specify a shorter length for 4 fields. Then in your app be sure to manually impose validation limits on any form fields related to these fields. There are 2 instances of this code snippet where you can explicitly set the length.: From 32eed884acf930f4bed86fbcb6a2c1637135448b Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 26 Feb 2025 18:31:35 -0500 Subject: [PATCH 0962/1013] Clarify optional service provider registration --- docs/installation-laravel.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md index 951690478..06b430727 100644 --- a/docs/installation-laravel.md +++ b/docs/installation-laravel.md @@ -17,7 +17,8 @@ See the "Prerequisites" documentation page for compatibility details. composer require spatie/laravel-permission -4. Optional: The **`Spatie\Permission\PermissionServiceProvider::class`** service provider will automatically get registered. Or you may manually add the service provider to the array in your `bootstrap/providers.php` (or `config/app.php` in Laravel 10 or older) file. +4. The Service Provider will automatically be registered; however, if you wish to manually register it, you can manually add the `Spatie\Permission\PermissionServiceProvider::class` service provider to the array in `bootstrap/providers.php` (`config/app.php` in Laravel 10 or older). + 5. **You should publish** [the migration](https://github.com/spatie/laravel-permission/blob/main/database/migrations/create_permission_tables.php.stub) and the [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/main/config/permission.php) with: @@ -33,7 +34,7 @@ See the "Prerequisites" documentation page for compatibility details. - must set `'teams' => true,` - and (optional) you may set `team_foreign_key` name in the config file if you want to use a custom foreign key in your database for teams - - **If you are using MySQL 8**, look at the migration files for notes about MySQL 8 to set/limit the index key length, and edit accordingly. If you get `ERROR: 1071 Specified key was too long` then you need to do this. + - **If you are using MySQL 8+**, look at the migration files for notes about MySQL 8+ to set/limit the index key length, and edit accordingly. If you get `ERROR: 1071 Specified key was too long` then you need to do this. - **If you are using CACHE_STORE=database**, be sure to [install Laravel's cache migration](https://laravel.com/docs/cache#prerequisites-database), else you will encounter cache errors. From 3718fdb618d5daf36d3f61e131e8fe76313413ad Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 26 Feb 2025 18:34:53 -0500 Subject: [PATCH 0963/1013] Lumen not supported --- docs/installation-lumen.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation-lumen.md b/docs/installation-lumen.md index 1cff50275..b5f079e75 100644 --- a/docs/installation-lumen.md +++ b/docs/installation-lumen.md @@ -7,7 +7,7 @@ NOTE: Lumen is **not** officially supported by this package. And Lumen is no lon However, the following are some steps which may help get you started. -Lumen installation instructions can be found in the [Lumen documentation](https://lumen.laravel.com/docs/master). +Lumen installation instructions can be found in the [Lumen documentation](https://lumen.laravel.com/docs). ## Installing From 5faededea5f46e582c40915c41e3dfb210582126 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 26 Feb 2025 18:40:00 -0500 Subject: [PATCH 0964/1013] Update link name --- docs/basic-usage/basic-usage.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/basic-usage/basic-usage.md b/docs/basic-usage/basic-usage.md index bd8f2acb0..76e41e040 100644 --- a/docs/basic-usage/basic-usage.md +++ b/docs/basic-usage/basic-usage.md @@ -55,7 +55,7 @@ $permission->removeRole($role); ``` ## Guard Name -If you're using multiple guards then the `guard_name` attribute must be set as well. Read about it in the [using multiple guards](./multiple-guards) section of the readme. +If you're using multiple guards then the `guard_name` attribute must be set as well. Read about it in the [using multiple guards](./multiple-guards) documentation. ## Get Permissions For A User The `HasRoles` trait adds Eloquent relationships to your models, which can be accessed directly or used as a base query: @@ -108,7 +108,7 @@ $allRolesExceptAandB = Role::whereNotIn('name', ['role A', 'role B'])->get(); ## Counting Users Having A Role One way to count all users who have a certain role is by filtering the collection of all Users with their Roles: ```php -$superAdminCount = User::with('roles')->get()->filter( - fn ($user) => $user->roles->where('name', 'Super Admin')->toArray() +$managersCount = User::with('roles')->get()->filter( + fn ($user) => $user->roles->where('name', 'Manager')->toArray() )->count(); ``` From 5e87096da2a0b86cc46dca4ea6aef224cecc585f Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 26 Feb 2025 18:43:22 -0500 Subject: [PATCH 0965/1013] Better to use roles for giving permissions as a group --- docs/basic-usage/direct-permissions.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/basic-usage/direct-permissions.md b/docs/basic-usage/direct-permissions.md index 944392b0f..e171c8163 100644 --- a/docs/basic-usage/direct-permissions.md +++ b/docs/basic-usage/direct-permissions.md @@ -5,11 +5,11 @@ weight: 2 ## Best Practice -It's better to assign permissions to Roles, and then assign Roles to Users. +INSTEAD OF DIRECT PERMISSIONS, it is better to assign permissions to Roles, and then assign Roles to Users. See the [Roles vs Permissions](../best-practices/roles-vs-permissions) section of the docs for a deeper explanation. -HOWEVER, If you have reason to directly assign individual permissions to specific users (instead of to roles assigned to those users), you can do that as described below: +HOWEVER, If you have reason to directly assign individual permissions to specific users (instead of to roles which are assigned to those users), you can do that as well: ## Direct Permissions to Users @@ -46,7 +46,7 @@ Like all permissions assigned via roles, you can check if a user has a permissio $user->can('edit articles'); ``` -NOTE: The following `hasPermissionTo`, `hasAnyPermission`, `hasAllPermissions` functions do not support Super-Admin functionality. Use `can`, `canAny`, `canAll` instead. +> NOTE: The following `hasPermissionTo`, `hasAnyPermission`, `hasAllPermissions` functions do not support Super-Admin functionality. Use `can`, `canAny`, `canAll` instead. You can check if a user has a permission: From 877e2c73cd939620eabc3972c65a7b9dcbc7ed56 Mon Sep 17 00:00:00 2001 From: Mark Lontoc Date: Sat, 1 Mar 2025 04:29:32 +0800 Subject: [PATCH 0966/1013] Middleware: support enums in role/permission middleware (#2813) * Allow middleware to handle enum based permission or role --------- Co-authored-by: Chris Brown --- src/Middleware/PermissionMiddleware.php | 35 ++++++++++++++--- src/Middleware/RoleMiddleware.php | 30 +++++++++++--- tests/PermissionMiddlewareTest.php | 51 ++++++++++++++++++++++++ tests/RoleMiddlewareTest.php | 52 +++++++++++++++++++++++++ 4 files changed, 158 insertions(+), 10 deletions(-) diff --git a/src/Middleware/PermissionMiddleware.php b/src/Middleware/PermissionMiddleware.php index 3e78f1d60..9a157611f 100644 --- a/src/Middleware/PermissionMiddleware.php +++ b/src/Middleware/PermissionMiddleware.php @@ -28,9 +28,7 @@ public function handle($request, Closure $next, $permission, $guard = null) throw UnauthorizedException::missingTraitHasRoles($user); } - $permissions = is_array($permission) - ? $permission - : explode('|', $permission); + $permissions = explode('|', self::parsePermissionsToString($permission)); if (! $user->canAny($permissions)) { throw UnauthorizedException::forPermissions($permissions); @@ -42,15 +40,42 @@ public function handle($request, Closure $next, $permission, $guard = null) /** * Specify the permission and guard for the middleware. * - * @param array|string $permission + * @param array|string|\BackedEnum $permission * @param string|null $guard * @return string */ public static function using($permission, $guard = null) { - $permissionString = is_string($permission) ? $permission : implode('|', $permission); + // Convert Enum to its value if an Enum is passed + if ($permission instanceof \BackedEnum) { + $permission = $permission->value; + } + + $permissionString = self::parsePermissionsToString($permission); + $args = is_null($guard) ? $permissionString : "$permissionString,$guard"; return static::class.':'.$args; } + + /** + * Convert array or string of permissions to string representation. + * + * @return string + */ + protected static function parsePermissionsToString(array|string|\BackedEnum $permission) + { + // Convert Enum to its value if an Enum is passed + if ($permission instanceof \BackedEnum) { + $permission = $permission->value; + } + + if (is_array($permission)) { + $permission = array_map(fn ($r) => $r instanceof \BackedEnum ? $r->value : $r, $permission); + + return implode('|', $permission); + } + + return (string) $permission; + } } diff --git a/src/Middleware/RoleMiddleware.php b/src/Middleware/RoleMiddleware.php index 010363f6e..edc02ff90 100644 --- a/src/Middleware/RoleMiddleware.php +++ b/src/Middleware/RoleMiddleware.php @@ -28,9 +28,7 @@ public function handle($request, Closure $next, $role, $guard = null) throw UnauthorizedException::missingTraitHasRoles($user); } - $roles = is_array($role) - ? $role - : explode('|', $role); + $roles = explode('|', self::parseRolesToString($role)); if (! $user->hasAnyRole($roles)) { throw UnauthorizedException::forRoles($roles); @@ -42,15 +40,37 @@ public function handle($request, Closure $next, $role, $guard = null) /** * Specify the role and guard for the middleware. * - * @param array|string $role + * @param array|string|\BackedEnum $role * @param string|null $guard * @return string */ public static function using($role, $guard = null) { - $roleString = is_string($role) ? $role : implode('|', $role); + $roleString = self::parseRolesToString($role); + $args = is_null($guard) ? $roleString : "$roleString,$guard"; return static::class.':'.$args; } + + /** + * Convert array or string of roles to string representation. + * + * @return string + */ + protected static function parseRolesToString(array|string|\BackedEnum $role) + { + // Convert Enum to its value if an Enum is passed + if ($role instanceof \BackedEnum) { + $role = $role->value; + } + + if (is_array($role)) { + $role = array_map(fn ($r) => $r instanceof \BackedEnum ? $r->value : $r, $role); + + return implode('|', $role); + } + + return (string) $role; + } } diff --git a/tests/PermissionMiddlewareTest.php b/tests/PermissionMiddlewareTest.php index 4dc233193..ecd88894c 100644 --- a/tests/PermissionMiddlewareTest.php +++ b/tests/PermissionMiddlewareTest.php @@ -434,4 +434,55 @@ public function the_middleware_can_be_created_with_static_using_method() PermissionMiddleware::using(['edit-articles', 'edit-news']) ); } + + /** + * @test + * + * @requires PHP >= 8.1 + */ + #[RequiresPhp('>= 8.1')] + #[Test] + public function the_middleware_can_handle_enum_based_permissions_with_static_using_method() + { + $this->assertSame( + 'Spatie\Permission\Middleware\PermissionMiddleware:view articles', + PermissionMiddleware::using(TestModels\TestRolePermissionsEnum::VIEWARTICLES) + ); + $this->assertEquals( + 'Spatie\Permission\Middleware\PermissionMiddleware:view articles,my-guard', + PermissionMiddleware::using(TestModels\TestRolePermissionsEnum::VIEWARTICLES, 'my-guard') + ); + $this->assertEquals( + 'Spatie\Permission\Middleware\PermissionMiddleware:view articles|edit articles', + PermissionMiddleware::using([TestModels\TestRolePermissionsEnum::VIEWARTICLES, TestModels\TestRolePermissionsEnum::EDITARTICLES]) + ); + } + + /** + * @test + * + * @requires PHP >= 8.1 + */ + #[RequiresPhp('>= 8.1')] + #[Test] + public function the_middleware_can_handle_enum_based_permissions_with_handle_method() + { + app(Permission::class)->create(['name' => TestModels\TestRolePermissionsEnum::VIEWARTICLES->value]); + app(Permission::class)->create(['name' => TestModels\TestRolePermissionsEnum::EDITARTICLES->value]); + + Auth::login($this->testUser); + $this->testUser->givePermissionTo(TestModels\TestRolePermissionsEnum::VIEWARTICLES); + + $this->assertEquals( + 200, + $this->runMiddleware($this->permissionMiddleware, TestModels\TestRolePermissionsEnum::VIEWARTICLES) + ); + + $this->testUser->givePermissionTo(TestModels\TestRolePermissionsEnum::EDITARTICLES); + + $this->assertEquals( + 200, + $this->runMiddleware($this->permissionMiddleware, [TestModels\TestRolePermissionsEnum::VIEWARTICLES, TestModels\TestRolePermissionsEnum::EDITARTICLES]) + ); + } } diff --git a/tests/RoleMiddlewareTest.php b/tests/RoleMiddlewareTest.php index 532f2061f..6cf01e2dd 100644 --- a/tests/RoleMiddlewareTest.php +++ b/tests/RoleMiddlewareTest.php @@ -9,6 +9,7 @@ use InvalidArgumentException; use Laravel\Passport\Passport; use PHPUnit\Framework\Attributes\Test; +use Spatie\Permission\Contracts\Role; use Spatie\Permission\Exceptions\UnauthorizedException; use Spatie\Permission\Middleware\RoleMiddleware; use Spatie\Permission\Tests\TestModels\UserWithoutHasRoles; @@ -366,4 +367,55 @@ public function the_middleware_can_be_created_with_static_using_method() RoleMiddleware::using(['testAdminRole', 'anotherRole']) ); } + + /** + * @test + * + * @requires PHP >= 8.1 + */ + #[RequiresPhp('>= 8.1')] + #[Test] + public function the_middleware_can_handle_enum_based_roles_with_static_using_method() + { + $this->assertSame( + 'Spatie\Permission\Middleware\RoleMiddleware:writer', + RoleMiddleware::using(TestModels\TestRolePermissionsEnum::WRITER) + ); + $this->assertEquals( + 'Spatie\Permission\Middleware\RoleMiddleware:writer,my-guard', + RoleMiddleware::using(TestModels\TestRolePermissionsEnum::WRITER, 'my-guard') + ); + $this->assertEquals( + 'Spatie\Permission\Middleware\RoleMiddleware:writer|editor', + RoleMiddleware::using([TestModels\TestRolePermissionsEnum::WRITER, TestModels\TestRolePermissionsEnum::EDITOR]) + ); + } + + /** + * @test + * + * @requires PHP >= 8.1 + */ + #[RequiresPhp('>= 8.1')] + #[Test] + public function the_middleware_can_handle_enum_based_roles_with_handle_method() + { + app(Role::class)->create(['name' => TestModels\TestRolePermissionsEnum::WRITER->value]); + app(Role::class)->create(['name' => TestModels\TestRolePermissionsEnum::EDITOR->value]); + + Auth::login($this->testUser); + $this->testUser->assignRole(TestModels\TestRolePermissionsEnum::WRITER); + + $this->assertEquals( + 200, + $this->runMiddleware($this->roleMiddleware, TestModels\TestRolePermissionsEnum::WRITER) + ); + + $this->testUser->assignRole(TestModels\TestRolePermissionsEnum::EDITOR); + + $this->assertEquals( + 200, + $this->runMiddleware($this->roleMiddleware, [TestModels\TestRolePermissionsEnum::WRITER, TestModels\TestRolePermissionsEnum::EDITOR]) + ); + } } From 4fa03c06509e037a4d42c131d0f181e3e4bbd483 Mon Sep 17 00:00:00 2001 From: drbyte <404472+drbyte@users.noreply.github.com> Date: Fri, 28 Feb 2025 20:29:57 +0000 Subject: [PATCH 0967/1013] Fix styling --- tests/PermissionMiddlewareTest.php | 4 +-- tests/RoleMiddlewareTest.php | 54 +++++++++++++++--------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/tests/PermissionMiddlewareTest.php b/tests/PermissionMiddlewareTest.php index ecd88894c..2aa233307 100644 --- a/tests/PermissionMiddlewareTest.php +++ b/tests/PermissionMiddlewareTest.php @@ -472,14 +472,14 @@ public function the_middleware_can_handle_enum_based_permissions_with_handle_met Auth::login($this->testUser); $this->testUser->givePermissionTo(TestModels\TestRolePermissionsEnum::VIEWARTICLES); - + $this->assertEquals( 200, $this->runMiddleware($this->permissionMiddleware, TestModels\TestRolePermissionsEnum::VIEWARTICLES) ); $this->testUser->givePermissionTo(TestModels\TestRolePermissionsEnum::EDITARTICLES); - + $this->assertEquals( 200, $this->runMiddleware($this->permissionMiddleware, [TestModels\TestRolePermissionsEnum::VIEWARTICLES, TestModels\TestRolePermissionsEnum::EDITARTICLES]) diff --git a/tests/RoleMiddlewareTest.php b/tests/RoleMiddlewareTest.php index 6cf01e2dd..96adea0a9 100644 --- a/tests/RoleMiddlewareTest.php +++ b/tests/RoleMiddlewareTest.php @@ -391,31 +391,31 @@ public function the_middleware_can_handle_enum_based_roles_with_static_using_met ); } - /** - * @test - * - * @requires PHP >= 8.1 - */ - #[RequiresPhp('>= 8.1')] - #[Test] - public function the_middleware_can_handle_enum_based_roles_with_handle_method() - { - app(Role::class)->create(['name' => TestModels\TestRolePermissionsEnum::WRITER->value]); - app(Role::class)->create(['name' => TestModels\TestRolePermissionsEnum::EDITOR->value]); - - Auth::login($this->testUser); - $this->testUser->assignRole(TestModels\TestRolePermissionsEnum::WRITER); - - $this->assertEquals( - 200, - $this->runMiddleware($this->roleMiddleware, TestModels\TestRolePermissionsEnum::WRITER) - ); - - $this->testUser->assignRole(TestModels\TestRolePermissionsEnum::EDITOR); - - $this->assertEquals( - 200, - $this->runMiddleware($this->roleMiddleware, [TestModels\TestRolePermissionsEnum::WRITER, TestModels\TestRolePermissionsEnum::EDITOR]) - ); - } + /** + * @test + * + * @requires PHP >= 8.1 + */ + #[RequiresPhp('>= 8.1')] + #[Test] + public function the_middleware_can_handle_enum_based_roles_with_handle_method() + { + app(Role::class)->create(['name' => TestModels\TestRolePermissionsEnum::WRITER->value]); + app(Role::class)->create(['name' => TestModels\TestRolePermissionsEnum::EDITOR->value]); + + Auth::login($this->testUser); + $this->testUser->assignRole(TestModels\TestRolePermissionsEnum::WRITER); + + $this->assertEquals( + 200, + $this->runMiddleware($this->roleMiddleware, TestModels\TestRolePermissionsEnum::WRITER) + ); + + $this->testUser->assignRole(TestModels\TestRolePermissionsEnum::EDITOR); + + $this->assertEquals( + 200, + $this->runMiddleware($this->roleMiddleware, [TestModels\TestRolePermissionsEnum::WRITER, TestModels\TestRolePermissionsEnum::EDITOR]) + ); + } } From 05dec51715d03fe178b10140bc9676cdabf42158 Mon Sep 17 00:00:00 2001 From: drbyte <404472+drbyte@users.noreply.github.com> Date: Fri, 28 Feb 2025 21:22:53 +0000 Subject: [PATCH 0968/1013] Update CHANGELOG --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4a056de3..a721eb9e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.16.0 - 2025-02-28 + +### What's Changed + +* Middleware: support enums in role/permission middleware by @marklawntalk in https://github.com/spatie/laravel-permission/pull/2813 + +### New Contributors + +* @marklawntalk made their first contribution in https://github.com/spatie/laravel-permission/pull/2813 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.15.0...6.16.0 + ## 6.15.0 - 2025-02-17 ### What's Changed @@ -979,6 +991,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` @@ -1055,6 +1068,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` From 9158ee379bbd5717b94c97beb9793c61ac13b7bd Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 28 Feb 2025 17:42:14 -0500 Subject: [PATCH 0969/1013] formatting --- tests/TestModels/TestRolePermissionsEnum.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestModels/TestRolePermissionsEnum.php b/tests/TestModels/TestRolePermissionsEnum.php index 7b3ba7485..61fec1a12 100644 --- a/tests/TestModels/TestRolePermissionsEnum.php +++ b/tests/TestModels/TestRolePermissionsEnum.php @@ -56,7 +56,7 @@ public function label(): string self::VIEWARTICLES => 'View Articles', self::EDITARTICLES => 'Edit Articles', - default => Str::words($this->name) + default => Str::words($this->value), }; } } From 02687346fe01242038a5ef9346696a42eda81250 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 28 Feb 2025 17:42:54 -0500 Subject: [PATCH 0970/1013] Tests refactoring --- tests/TestCase.php | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/tests/TestCase.php b/tests/TestCase.php index 3a4f1c829..073e7c8fb 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -70,6 +70,9 @@ protected function setUp(): void // Note: this also flushes the cache from within the migration $this->setUpDatabase($this->app); + + $this->setUpBaseTestPermissions($this->app); + if ($this->hasTeams) { setPermissionsTeamId(1); } @@ -92,9 +95,8 @@ protected function tearDown(): void /** * @param \Illuminate\Foundation\Application $app - * @return array */ - protected function getPackageProviders($app) + protected function getPackageProviders($app): array { return $this->getLaravelVersion() < 9 ? [ PermissionServiceProvider::class, @@ -196,13 +198,25 @@ protected function setUpDatabase($app) $this->testUser = User::create(['email' => 'test@user.com']); $this->testAdmin = Admin::create(['email' => 'admin@user.com']); + } + + /** + * Set up initial roles and permissions used in many tests + * + * @param \Illuminate\Foundation\Application $app + */ + protected function setUpBaseTestPermissions($app): void + { $this->testUserRole = $app[Role::class]->create(['name' => 'testRole']); $app[Role::class]->create(['name' => 'testRole2']); $this->testAdminRole = $app[Role::class]->create(['name' => 'testAdminRole', 'guard_name' => 'admin']); $this->testUserPermission = $app[Permission::class]->create(['name' => 'edit-articles']); $app[Permission::class]->create(['name' => 'edit-news']); $app[Permission::class]->create(['name' => 'edit-blog']); - $this->testAdminPermission = $app[Permission::class]->create(['name' => 'admin-permission', 'guard_name' => 'admin']); + $this->testAdminPermission = $app[Permission::class]->create([ + 'name' => 'admin-permission', + 'guard_name' => 'admin', + ]); $app[Permission::class]->create(['name' => 'Edit News']); } From d15a731b4b0db37a644f8721c13dfdf8ed04f1c2 Mon Sep 17 00:00:00 2001 From: Ali Qasemzadeh Date: Sun, 2 Mar 2025 18:16:49 +0330 Subject: [PATCH 0971/1013] Add JetAdmin link to UI options documentation Updated the advanced usage UI options documentation to include JetAdmin, a Laravel Livewire starter kit for --- docs/advanced-usage/ui-options.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/advanced-usage/ui-options.md b/docs/advanced-usage/ui-options.md index 17f70b85f..ebfb552ef 100644 --- a/docs/advanced-usage/ui-options.md +++ b/docs/advanced-usage/ui-options.md @@ -29,3 +29,5 @@ If you decide you need a UI, even if it's not for creating/editing role/permissi - [LiveWire Base Admin Panel](https://github.com/aliqasemzadeh/bap) User management by [AliQasemzadeh](https://github.com/aliqasemzadeh) + +- [JetAdmin](https://github.com/aliqasemzadeh/jetadmin) JetAdmin use laravel livewire starter kit and manage permissions. [AliQasemzadeh](https://github.com/aliqasemzadeh) From 759f15c9ec8d170d88409b110539815d8d702053 Mon Sep 17 00:00:00 2001 From: so1e <31845646+Yi-pixel@users.noreply.github.com> Date: Sat, 15 Mar 2025 13:42:34 +0800 Subject: [PATCH 0972/1013] Route macro functions add backed enum support --- src/PermissionServiceProvider.php | 10 ++++- tests/RouteTest.php | 64 +++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php index 96ae5abc6..3aa63def0 100644 --- a/src/PermissionServiceProvider.php +++ b/src/PermissionServiceProvider.php @@ -149,13 +149,19 @@ protected function registerMacroHelpers(): void } Route::macro('role', function ($roles = []) { + $roles = Arr::wrap($roles); + $roles = array_map(fn ($role) => $role instanceof \BackedEnum ? $role->value : $role, $roles); + /** @var Route $this */ - return $this->middleware('role:'.implode('|', Arr::wrap($roles))); + return $this->middleware('role:'.implode('|', $roles)); }); Route::macro('permission', function ($permissions = []) { + $permissions = Arr::wrap($permissions); + $permissions = array_map(fn ($permission) => $permission instanceof \BackedEnum ? $permission->value : $permission, $permissions); + /** @var Route $this */ - return $this->middleware('permission:'.implode('|', Arr::wrap($permissions))); + return $this->middleware('permission:'.implode('|', $permissions)); }); } diff --git a/tests/RouteTest.php b/tests/RouteTest.php index 0533ea74c..7b213eee8 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -2,7 +2,9 @@ namespace Spatie\Permission\Tests; +use PHPUnit\Framework\Attributes\RequiresPhp; use PHPUnit\Framework\Attributes\Test; +use Spatie\Permission\Tests\TestModels\TestRolePermissionsEnum; class RouteTest extends TestCase { @@ -51,4 +53,66 @@ public function test_role_and_permission_function_together() $this->getLastRouteMiddlewareFromRouter($router) ); } + + /** + * @test + * + * @requires PHP 8.1.0 + */ + #[RequiresPhp('>= 8.1.0')] + #[Test] + public function test_role_function_with_backed_enum() + { + $router = $this->getRouter(); + + $router->get('role-test.enum', $this->getRouteResponse()) + ->name('role.test.enum') + ->role(TestRolePermissionsEnum::USERMANAGER); + + $this->assertEquals(['role:'.TestRolePermissionsEnum::USERMANAGER->value], $this->getLastRouteMiddlewareFromRouter($router)); + } + + /** + * @test + * + * @requires PHP 8.1.0 + */ + #[RequiresPhp('>= 8.1.0')] + #[Test] + public function test_permission_function_with_backed_enum() + { + $router = $this->getRouter(); + + $router->get('permission-test.enum', $this->getRouteResponse()) + ->name('permission.test.enum') + ->permission(TestRolePermissionsEnum::WRITER); + + $expected = ['permission:'.TestRolePermissionsEnum::WRITER->value]; + $this->assertEquals($expected, $this->getLastRouteMiddlewareFromRouter($router)); + } + + /** + * @test + * + * @requires PHP 8.1.0 + */ + #[RequiresPhp('>= 8.1.0')] + #[Test] + public function test_role_and_permission_function_together_with_backed_enum() + { + $router = $this->getRouter(); + + $router->get('roles-permissions-test.enum', $this->getRouteResponse()) + ->name('roles-permissions.test.enum') + ->role([TestRolePermissionsEnum::USERMANAGER, TestRolePermissionsEnum::ADMIN]) + ->permission([TestRolePermissionsEnum::WRITER, TestRolePermissionsEnum::EDITOR]); + + $this->assertEquals( + [ + 'role:'.TestRolePermissionsEnum::USERMANAGER->value.'|'.TestRolePermissionsEnum::ADMIN->value, + 'permission:'.TestRolePermissionsEnum::WRITER->value.'|'.TestRolePermissionsEnum::EDITOR->value, + ], + $this->getLastRouteMiddlewareFromRouter($router) + ); + } } From f5b59d92282845f0a8c9e025130097097370925a Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 4 Apr 2025 17:12:23 -0400 Subject: [PATCH 0973/1013] Mention Octane Fixes #2827 --- docs/advanced-usage/cache.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/advanced-usage/cache.md b/docs/advanced-usage/cache.md index 5e76828ec..c2dd260c2 100644 --- a/docs/advanced-usage/cache.md +++ b/docs/advanced-usage/cache.md @@ -46,6 +46,9 @@ php artisan permission:cache-reset ``` (This command is effectively an alias for `artisan cache:forget spatie.permission.cache` but respects the package config as well.) +## Octane cache reset +In many cases Octane will not need additional cache resets; however, if you find that cache results are stale or crossing over between requests, you can force a cache flush upon every Octane reset cycle by editing the `/config/permission.php` and setting `register_octane_reset_listener` to true. + ## Cache Configuration Settings This package allows you to customize cache-related operations via its config file. In most cases the defaults are fine; however, in a multitenancy situation you may wish to do some cache-prefix overrides when switching tenants. See below for more details. From 02ada8f638b643713fa2fb543384738e27346ddb Mon Sep 17 00:00:00 2001 From: Jimi Robaer Date: Tue, 8 Apr 2025 17:06:14 +0200 Subject: [PATCH 0974/1013] Update README.md --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1a5e00473..a8a54d3fd 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,18 @@ -

Social Card of Laravel Permission

+
## Documentation, Installation, and Usage Instructions @@ -77,7 +85,7 @@ can be found [in this repo on GitHub](https://github.com/laracasts/laravel-5-rol Special thanks to [Alex Vanderbist](https://github.com/AlexVanderbist) who greatly helped with `v2`, and to [Chris Brown](https://github.com/drbyte) for his longtime support helping us maintain the package. -And a special thanks to [Caneco](https://twitter.com/caneco) for the logo ✨ +Special thanks to [Caneco](https://twitter.com/caneco) for the original logo. ## Alternatives From 9bdd93375f5e296c3a1d19bbf87c3711be2e6c0a Mon Sep 17 00:00:00 2001 From: drbyte <404472+drbyte@users.noreply.github.com> Date: Wed, 9 Apr 2025 22:03:12 +0000 Subject: [PATCH 0975/1013] Update CHANGELOG --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a721eb9e2..3778ef834 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.17.0 - 2025-04-09 + +### What's Changed + +* Route macro functions: add backed enum support by @Yi-pixel in https://github.com/spatie/laravel-permission/pull/2823 + +### New Contributors + +* @Yi-pixel made their first contribution in https://github.com/spatie/laravel-permission/pull/2823 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.16.0...6.17.0 + ## 6.16.0 - 2025-02-28 ### What's Changed @@ -992,6 +1004,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` @@ -1069,6 +1082,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` From 13873682fdc6ee91c4d4408f57a3ce881f0a4e5e Mon Sep 17 00:00:00 2001 From: Caio Adriano <66336349+ccaioadriano@users.noreply.github.com> Date: Wed, 9 Apr 2025 19:07:29 -0300 Subject: [PATCH 0976/1013] Refactor exception throwing in migration file to use throw_if (#2819) Refactor: refactoring exception throwing --- database/migrations/create_permission_tables.php.stub | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/database/migrations/create_permission_tables.php.stub b/database/migrations/create_permission_tables.php.stub index 70a120f30..ce4d9d2d4 100644 --- a/database/migrations/create_permission_tables.php.stub +++ b/database/migrations/create_permission_tables.php.stub @@ -17,12 +17,8 @@ return new class extends Migration $pivotRole = $columnNames['role_pivot_key'] ?? 'role_id'; $pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id'; - if (empty($tableNames)) { - throw new \Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.'); - } - if ($teams && empty($columnNames['team_foreign_key'] ?? null)) { - throw new \Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.'); - } + throw_if(empty($tableNames), new Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.')); + throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), new Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.')); Schema::create($tableNames['permissions'], static function (Blueprint $table) { // $table->engine('InnoDB'); From cc264a1d959e70742301156a1b06bbbcd262357e Mon Sep 17 00:00:00 2001 From: Ali Qasemzadeh Date: Thu, 10 Apr 2025 01:40:36 +0330 Subject: [PATCH 0977/1013] Add JetAdmin link to UI options documentation (#2814) Updated the advanced usage UI options documentation to include JetAdmin, a Laravel Livewire starter kit for --- docs/advanced-usage/ui-options.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/advanced-usage/ui-options.md b/docs/advanced-usage/ui-options.md index 17f70b85f..ebfb552ef 100644 --- a/docs/advanced-usage/ui-options.md +++ b/docs/advanced-usage/ui-options.md @@ -29,3 +29,5 @@ If you decide you need a UI, even if it's not for creating/editing role/permissi - [LiveWire Base Admin Panel](https://github.com/aliqasemzadeh/bap) User management by [AliQasemzadeh](https://github.com/aliqasemzadeh) + +- [JetAdmin](https://github.com/aliqasemzadeh/jetadmin) JetAdmin use laravel livewire starter kit and manage permissions. [AliQasemzadeh](https://github.com/aliqasemzadeh) From df3997986f5d08eb734d3ee1c85725c7a79b5b26 Mon Sep 17 00:00:00 2001 From: Jerren Saunders Date: Mon, 21 Apr 2025 15:59:24 -0400 Subject: [PATCH 0978/1013] Fix: `wildcard_permission` example includes `permission.` prefix In the `permission.php` template file, the example in the comments for how to override the `WildcardPermission` class incorrectly uses `permission.wildcard_permission` as the key instead of just `wildcard_permission`. --- config/permission.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/permission.php b/config/permission.php index 8e84e9d53..f39f6b5bf 100644 --- a/config/permission.php +++ b/config/permission.php @@ -172,7 +172,7 @@ * The class to use for interpreting wildcard permissions. * If you need to modify delimiters, override the class and specify its name here. */ - // 'permission.wildcard_permission' => Spatie\Permission\WildcardPermission::class, + // 'wildcard_permission' => Spatie\Permission\WildcardPermission::class, /* Cache-specific settings */ From bfb824aa8bf03b3bd8b77ef4b227496f03dbad77 Mon Sep 17 00:00:00 2001 From: Jimi Robaer Date: Tue, 22 Apr 2025 09:32:40 +0200 Subject: [PATCH 0979/1013] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a8a54d3fd..12bed721f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ - Logo for laravel-permission + Logo for laravel-permission From ca77ab3c9c3da231df0f6c6a8e5d44d8813cc9fe Mon Sep 17 00:00:00 2001 From: Ken van der Eerden <15888558+Ken-vdE@users.noreply.github.com> Date: Wed, 23 Apr 2025 16:47:25 +0200 Subject: [PATCH 0980/1013] Update multiple-guards.md Fix, otherwise `Guard::getNames(User::class)` does not work. --- docs/basic-usage/multiple-guards.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/basic-usage/multiple-guards.md b/docs/basic-usage/multiple-guards.md index d9360d6cf..062e49fad 100644 --- a/docs/basic-usage/multiple-guards.md +++ b/docs/basic-usage/multiple-guards.md @@ -20,7 +20,8 @@ Note that this package requires you to register a permission name (same for role If your app structure does NOT differentiate between guards when it comes to roles/permissions, (ie: if ALL your roles/permissions are the SAME for ALL guards), you can override the `getDefaultGuardName` function by adding it to your User model, and specifying your desired `$guard_name`. Then you only need to create roles/permissions for that single `$guard_name`, not duplicating them. The example here sets it to `web`, but use whatever your application's default is: ```php - protected function getDefaultGuardName(): string { return 'web'; } + protected string $guard_name = 'web'; + protected function getDefaultGuardName(): string { return $this->guard_name; } ```` From 39bb908380d903cf7f89d68ef6c45529e2bf37a9 Mon Sep 17 00:00:00 2001 From: Ken van der Eerden <15888558+Ken-vdE@users.noreply.github.com> Date: Wed, 23 Apr 2025 17:07:05 +0200 Subject: [PATCH 0981/1013] Update Role.php --- src/Models/Role.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Models/Role.php b/src/Models/Role.php index 5bab4878e..7aa729dc1 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -27,7 +27,7 @@ class Role extends Model implements RoleContract public function __construct(array $attributes = []) { - $attributes['guard_name'] = $attributes['guard_name'] ?? config('auth.defaults.guard'); + $attributes['guard_name'] ??= Guard::getDefaultName(static::class); parent::__construct($attributes); @@ -42,7 +42,7 @@ public function __construct(array $attributes = []) */ public static function create(array $attributes = []) { - $attributes['guard_name'] = $attributes['guard_name'] ?? Guard::getDefaultName(static::class); + $attributes['guard_name'] ??= Guard::getDefaultName(static::class); $params = ['name' => $attributes['name'], 'guard_name' => $attributes['guard_name']]; if (app(PermissionRegistrar::class)->teams) { @@ -97,7 +97,7 @@ public function users(): BelongsToMany */ public static function findByName(string $name, ?string $guardName = null): RoleContract { - $guardName = $guardName ?? Guard::getDefaultName(static::class); + $guardName ??= Guard::getDefaultName(static::class); $role = static::findByParam(['name' => $name, 'guard_name' => $guardName]); @@ -115,7 +115,7 @@ public static function findByName(string $name, ?string $guardName = null): Role */ public static function findById(int|string $id, ?string $guardName = null): RoleContract { - $guardName = $guardName ?? Guard::getDefaultName(static::class); + $guardName ??= Guard::getDefaultName(static::class); $role = static::findByParam([(new static)->getKeyName() => $id, 'guard_name' => $guardName]); @@ -133,7 +133,7 @@ public static function findById(int|string $id, ?string $guardName = null): Role */ public static function findOrCreate(string $name, ?string $guardName = null): RoleContract { - $guardName = $guardName ?? Guard::getDefaultName(static::class); + $guardName ??= Guard::getDefaultName(static::class); $role = static::findByParam(['name' => $name, 'guard_name' => $guardName]); From 6f2f96fb2e9d9a888d449ae399484e92272beb3d Mon Sep 17 00:00:00 2001 From: Ken van der Eerden <15888558+Ken-vdE@users.noreply.github.com> Date: Wed, 23 Apr 2025 17:09:10 +0200 Subject: [PATCH 0982/1013] Update Permission.php --- src/Models/Permission.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Models/Permission.php b/src/Models/Permission.php index fd54b8ca0..490a6c23c 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -26,7 +26,7 @@ class Permission extends Model implements PermissionContract public function __construct(array $attributes = []) { - $attributes['guard_name'] = $attributes['guard_name'] ?? config('auth.defaults.guard'); + $attributes['guard_name'] ??= Guard::getDefaultName(static::class); parent::__construct($attributes); @@ -41,7 +41,7 @@ public function __construct(array $attributes = []) */ public static function create(array $attributes = []) { - $attributes['guard_name'] = $attributes['guard_name'] ?? Guard::getDefaultName(static::class); + $attributes['guard_name'] ??= Guard::getDefaultName(static::class); $permission = static::getPermission(['name' => $attributes['name'], 'guard_name' => $attributes['guard_name']]); @@ -88,7 +88,7 @@ public function users(): BelongsToMany */ public static function findByName(string $name, ?string $guardName = null): PermissionContract { - $guardName = $guardName ?? Guard::getDefaultName(static::class); + $guardName ??= Guard::getDefaultName(static::class); $permission = static::getPermission(['name' => $name, 'guard_name' => $guardName]); if (! $permission) { throw PermissionDoesNotExist::create($name, $guardName); @@ -106,7 +106,7 @@ public static function findByName(string $name, ?string $guardName = null): Perm */ public static function findById(int|string $id, ?string $guardName = null): PermissionContract { - $guardName = $guardName ?? Guard::getDefaultName(static::class); + $guardName ??= Guard::getDefaultName(static::class); $permission = static::getPermission([(new static)->getKeyName() => $id, 'guard_name' => $guardName]); if (! $permission) { @@ -123,7 +123,7 @@ public static function findById(int|string $id, ?string $guardName = null): Perm */ public static function findOrCreate(string $name, ?string $guardName = null): PermissionContract { - $guardName = $guardName ?? Guard::getDefaultName(static::class); + $guardName ??= Guard::getDefaultName(static::class); $permission = static::getPermission(['name' => $name, 'guard_name' => $guardName]); if (! $permission) { From 502ec932de77278bb30315e377905d5812199ea3 Mon Sep 17 00:00:00 2001 From: Jordan Welch Date: Fri, 9 May 2025 07:13:50 -0500 Subject: [PATCH 0983/1013] Remove `collectPermissions` that is not being assigned --- src/Traits/HasPermissions.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index ff7339b19..1a8071659 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -450,7 +450,6 @@ public function forgetWildcardPermissionIndex(): void public function syncPermissions(...$permissions) { if ($this->getModel()->exists) { - $this->collectPermissions($permissions); $this->permissions()->detach(); $this->setRelation('permissions', collect()); } From 556f25131d809fcccaa483fccf3527037060576b Mon Sep 17 00:00:00 2001 From: coreyhn Date: Tue, 13 May 2025 12:23:36 -0600 Subject: [PATCH 0984/1013] [Docs] Remove extra period (#2841) --- docs/advanced-usage/seeding.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-usage/seeding.md b/docs/advanced-usage/seeding.md index 30dc21bf2..51c56e8d2 100644 --- a/docs/advanced-usage/seeding.md +++ b/docs/advanced-usage/seeding.md @@ -7,7 +7,7 @@ weight: 2 You may discover that it is best to flush this package's cache **BEFORE seeding, to avoid cache conflict errors**. -And if you use the `WithoutModelEvents` trait in your seeders, flush it **AFTER creating any roles/permissions as well, before assigning or granting them.**. +And if you use the `WithoutModelEvents` trait in your seeders, flush it **AFTER creating any roles/permissions as well, before assigning or granting them**. ```php // reset cached roles and permissions From 7d53c90ed56030afb163549f14810f8b45f9b562 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Tue, 13 May 2025 16:03:55 -0500 Subject: [PATCH 0985/1013] Fix #2843 --- config/permission.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/permission.php b/config/permission.php index 8e84e9d53..f39f6b5bf 100644 --- a/config/permission.php +++ b/config/permission.php @@ -172,7 +172,7 @@ * The class to use for interpreting wildcard permissions. * If you need to modify delimiters, override the class and specify its name here. */ - // 'permission.wildcard_permission' => Spatie\Permission\WildcardPermission::class, + // 'wildcard_permission' => Spatie\Permission\WildcardPermission::class, /* Cache-specific settings */ From 68f9b48ea9cbb1599dfbd9efafdb221fc8cb7c0e Mon Sep 17 00:00:00 2001 From: drbyte <404472+drbyte@users.noreply.github.com> Date: Wed, 14 May 2025 03:34:44 +0000 Subject: [PATCH 0986/1013] Update CHANGELOG --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3778ef834..45e6dcb5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,28 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.18.0 - 2025-05-14 + +### What's Changed + +* refactor exception throwing in migration file to use throw_if() by @ccaioadriano in https://github.com/spatie/laravel-permission/pull/2819 +* Fix: Example in config comment includes `permission.` prefix on `wildcard_permission` key by @jerrens in https://github.com/spatie/laravel-permission/pull/2835 +* Fix commented config key typo by @erikn69 in https://github.com/spatie/laravel-permission/pull/2844 +* Remove `collectPermissions` that is not being assigned by @JHWelch in https://github.com/spatie/laravel-permission/pull/2840 +* [Docs] Update multiple-guards.md by @Ken-vdE in https://github.com/spatie/laravel-permission/pull/2836 +* [Docs] Remove extra period by @coreyhn in https://github.com/spatie/laravel-permission/pull/2841 +* Add JetAdmin as UI Option. by @aliqasemzadeh in https://github.com/spatie/laravel-permission/pull/2814 + +### New Contributors + +* @ccaioadriano made their first contribution in https://github.com/spatie/laravel-permission/pull/2819 +* @coreyhn made their first contribution in https://github.com/spatie/laravel-permission/pull/2841 +* @jerrens made their first contribution in https://github.com/spatie/laravel-permission/pull/2835 +* @Ken-vdE made their first contribution in https://github.com/spatie/laravel-permission/pull/2836 +* @JHWelch made their first contribution in https://github.com/spatie/laravel-permission/pull/2840 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.17.0...6.18.0 + ## 6.17.0 - 2025-04-09 ### What's Changed @@ -1005,6 +1027,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` @@ -1083,6 +1106,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` From 78630c7f8a343106d10dfad076e50534ee52b990 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Tue, 27 May 2025 08:55:53 -0500 Subject: [PATCH 0987/1013] Revert "Remove `collectPermissions`, test added" --- src/Traits/HasPermissions.php | 1 + tests/HasPermissionsTest.php | 17 +++++++++++++++++ tests/HasRolesTest.php | 17 +++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index 1a8071659..ff7339b19 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -450,6 +450,7 @@ public function forgetWildcardPermissionIndex(): void public function syncPermissions(...$permissions) { if ($this->getModel()->exists) { + $this->collectPermissions($permissions); $this->permissions()->detach(); $this->setRelation('permissions', collect()); } diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index 49f450359..e3b3e3474 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -611,6 +611,23 @@ public function it_can_avoid_sync_duplicated_permissions() $this->assertTrue($this->testUser->hasDirectPermission('edit-blog')); } + /** @test */ + #[Test] + public function it_can_avoid_detach_on_permission_that_does_not_exist_sync() + { + $this->testUser->syncPermissions('edit-articles'); + + try { + $this->testUser->syncPermissions('permission-does-not-exist'); + $this->fail('Expected PermissionDoesNotExist exception was not thrown.'); + } catch (PermissionDoesNotExist $e){ + // + } + + $this->assertTrue($this->testUser->hasDirectPermission('edit-articles')); + $this->assertFalse($this->testUser->checkPermissionTo('permission-does-not-exist')); + } + /** @test */ #[Test] public function it_can_sync_multiple_permissions_by_id() diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 7f8a137e0..59941f78d 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -318,6 +318,23 @@ public function it_can_avoid_sync_duplicated_roles() $this->assertTrue($this->testUser->hasRole('testRole2')); } + /** @test */ + #[Test] + public function it_can_avoid_detach_on_role_that_does_not_exist_sync() + { + $this->testUser->syncRoles('testRole'); + + try { + $this->testUser->syncRoles('role-does-not-exist'); + $this->fail('Expected RoleDoesNotExist exception was not thrown.'); + } catch (RoleDoesNotExist $e){ + // + } + + $this->assertTrue($this->testUser->hasRole('testRole')); + $this->assertFalse($this->testUser->hasRole('role-does-not-exist')); + } + /** @test */ #[Test] public function it_can_sync_multiple_roles() From e64507622153f0afa2734f6bc87c050c2c8d7fd5 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Tue, 27 May 2025 10:04:28 -0500 Subject: [PATCH 0988/1013] Make phpstan happy --- phpstan.neon.dist | 3 +++ 1 file changed, 3 insertions(+) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 022878407..e6a4b0f89 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -17,3 +17,6 @@ parameters: # wildcard permissions: - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Model::getWildcardClass#' - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Model::getAllPermissions#' + # contract checks: + - '#Call to function is_a\(\) with (.*) and ''Spatie\\\\Permission\\\\Contracts\\\\Permission'' will always evaluate to true\.$#' + - '#Call to function is_a\(\) with (.*) and ''Spatie\\\\Permission\\\\Contracts\\\\Role'' will always evaluate to true\.$#' From 15b15e10c812791d1bbe1f5502a2773bbd54892b Mon Sep 17 00:00:00 2001 From: drbyte <404472+drbyte@users.noreply.github.com> Date: Wed, 28 May 2025 02:51:34 +0000 Subject: [PATCH 0989/1013] Fix styling --- tests/HasPermissionsTest.php | 2 +- tests/HasRolesTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index e3b3e3474..95ad3a473 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -620,7 +620,7 @@ public function it_can_avoid_detach_on_permission_that_does_not_exist_sync() try { $this->testUser->syncPermissions('permission-does-not-exist'); $this->fail('Expected PermissionDoesNotExist exception was not thrown.'); - } catch (PermissionDoesNotExist $e){ + } catch (PermissionDoesNotExist $e) { // } diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 59941f78d..2ead3982e 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -327,7 +327,7 @@ public function it_can_avoid_detach_on_role_that_does_not_exist_sync() try { $this->testUser->syncRoles('role-does-not-exist'); $this->fail('Expected RoleDoesNotExist exception was not thrown.'); - } catch (RoleDoesNotExist $e){ + } catch (RoleDoesNotExist $e) { // } From c5c63c145c2e0f34896c66765aede8a6019e1d5a Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Tue, 27 May 2025 23:16:44 -0400 Subject: [PATCH 0990/1013] remove apc --- tests/TestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestCase.php b/tests/TestCase.php index 073e7c8fb..45831ce15 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -148,7 +148,7 @@ protected function getEnvironmentSetUp($app) // FOR MANUAL TESTING OF ALTERNATE CACHE STORES: // $app['config']->set('cache.default', 'array'); // Laravel supports: array, database, file - // requires extensions: apc, memcached, redis, dynamodb, octane + // requires extensions: memcached, redis, dynamodb, octane } /** From 51a919e5acde63620088b8ad48084aaa846d9fca Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 28 May 2025 12:36:25 -0400 Subject: [PATCH 0991/1013] ampersand --- docs/basic-usage/teams-permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/teams-permissions.md b/docs/basic-usage/teams-permissions.md index ad220a1ac..684770ded 100644 --- a/docs/basic-usage/teams-permissions.md +++ b/docs/basic-usage/teams-permissions.md @@ -99,7 +99,7 @@ Role::create(['name' => 'reader', 'team_id' => 1]); Role::create(['name' => 'reviewer']); ``` -## Roles/Permissions Assignment & Removal +## Roles/Permissions Assignment and Removal The role/permission assignment and removal for teams are the same as without teams, but they take the global `team_id` which is set on login. From f241ca71546e0b8f82284e172908ec6d59bb29ef Mon Sep 17 00:00:00 2001 From: drbyte <404472+drbyte@users.noreply.github.com> Date: Sat, 31 May 2025 23:39:13 +0000 Subject: [PATCH 0992/1013] Update CHANGELOG --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45e6dcb5a..56bd868eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.19.0 - 2025-05-31 + +### What's Changed + +* Revert "Remove `collectPermissions` that is not being assigned" by @erikn69 in https://github.com/spatie/laravel-permission/pull/2851 +* Fix guard_name not used to set default attribute in Role and Permission model by @Ken-vdE in https://github.com/spatie/laravel-permission/pull/2837 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.18.0...6.19.0 + ## 6.18.0 - 2025-05-14 ### What's Changed @@ -1028,6 +1037,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` @@ -1107,6 +1117,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` From 92d02871f0e955991de03a373a9f094edcdc1e3f Mon Sep 17 00:00:00 2001 From: nAa666 Date: Tue, 3 Jun 2025 10:26:43 +0300 Subject: [PATCH 0993/1013] Add translations support for exception messages --- src/Exceptions/GuardDoesNotMatch.php | 5 ++++- src/Exceptions/PermissionAlreadyExists.php | 5 ++++- src/Exceptions/PermissionDoesNotExist.php | 10 ++++++++-- src/Exceptions/RoleAlreadyExists.php | 5 ++++- src/Exceptions/RoleDoesNotExist.php | 10 ++++++++-- src/Exceptions/UnauthorizedException.php | 18 ++++++++++-------- .../WildcardPermissionInvalidArgument.php | 2 +- ...WildcardPermissionNotImplementsContract.php | 2 +- .../WildcardPermissionNotProperlyFormatted.php | 4 +++- 9 files changed, 43 insertions(+), 18 deletions(-) diff --git a/src/Exceptions/GuardDoesNotMatch.php b/src/Exceptions/GuardDoesNotMatch.php index 6506bb658..7c57b3155 100644 --- a/src/Exceptions/GuardDoesNotMatch.php +++ b/src/Exceptions/GuardDoesNotMatch.php @@ -9,6 +9,9 @@ class GuardDoesNotMatch extends InvalidArgumentException { public static function create(string $givenGuard, Collection $expectedGuards) { - return new static("The given role or permission should use guard `{$expectedGuards->implode(', ')}` instead of `{$givenGuard}`."); + return new static(__('The given role or permission should use guard `:expected` instead of `:given`.', [ + 'expected' => $expectedGuards->implode(', '), + 'given' => $givenGuard, + ])); } } diff --git a/src/Exceptions/PermissionAlreadyExists.php b/src/Exceptions/PermissionAlreadyExists.php index 2748c9d1d..2270db148 100644 --- a/src/Exceptions/PermissionAlreadyExists.php +++ b/src/Exceptions/PermissionAlreadyExists.php @@ -8,6 +8,9 @@ class PermissionAlreadyExists extends InvalidArgumentException { public static function create(string $permissionName, string $guardName) { - return new static("A `{$permissionName}` permission already exists for guard `{$guardName}`."); + return new static(__('A `:permission` permission already exists for guard `:guard`.', [ + 'permission' => $permissionName, + 'guard' => $guardName, + ])); } } diff --git a/src/Exceptions/PermissionDoesNotExist.php b/src/Exceptions/PermissionDoesNotExist.php index c04f5eaf0..853bab10e 100644 --- a/src/Exceptions/PermissionDoesNotExist.php +++ b/src/Exceptions/PermissionDoesNotExist.php @@ -8,7 +8,10 @@ class PermissionDoesNotExist extends InvalidArgumentException { public static function create(string $permissionName, ?string $guardName) { - return new static("There is no permission named `{$permissionName}` for guard `{$guardName}`."); + return new static(__('There is no permission named `:permission` for guard `:guard`.', [ + 'permission' => $permissionName, + 'guard' => $guardName, + ])); } /** @@ -17,6 +20,9 @@ public static function create(string $permissionName, ?string $guardName) */ public static function withId($permissionId, ?string $guardName) { - return new static("There is no [permission] with ID `{$permissionId}` for guard `{$guardName}`."); + return new static(__('There is no [permission] with ID `:id` for guard `:guard`.', [ + 'id' => $permissionId, + 'guard' => $guardName, + ])); } } diff --git a/src/Exceptions/RoleAlreadyExists.php b/src/Exceptions/RoleAlreadyExists.php index 939c55bac..141be8d26 100644 --- a/src/Exceptions/RoleAlreadyExists.php +++ b/src/Exceptions/RoleAlreadyExists.php @@ -8,6 +8,9 @@ class RoleAlreadyExists extends InvalidArgumentException { public static function create(string $roleName, string $guardName) { - return new static("A role `{$roleName}` already exists for guard `{$guardName}`."); + return new static(__('A role `:role` already exists for guard `:guard`.', [ + 'role' => $roleName, + 'guard' => $guardName, + ])); } } diff --git a/src/Exceptions/RoleDoesNotExist.php b/src/Exceptions/RoleDoesNotExist.php index a4871c665..641b4d37f 100644 --- a/src/Exceptions/RoleDoesNotExist.php +++ b/src/Exceptions/RoleDoesNotExist.php @@ -8,7 +8,10 @@ class RoleDoesNotExist extends InvalidArgumentException { public static function named(string $roleName, ?string $guardName) { - return new static("There is no role named `{$roleName}` for guard `{$guardName}`."); + return new static(__('There is no role named `:role` for guard `:guard`.', [ + 'role' => $roleName, + 'guard' => $guardName, + ])); } /** @@ -17,6 +20,9 @@ public static function named(string $roleName, ?string $guardName) */ public static function withId($roleId, ?string $guardName) { - return new static("There is no role with ID `{$roleId}` for guard `{$guardName}`."); + return new static(__('There is no role with ID `:id` for guard `:guard`.', [ + 'id' => $roleId, + 'guard' => $guardName, + ])); } } diff --git a/src/Exceptions/UnauthorizedException.php b/src/Exceptions/UnauthorizedException.php index 249898a75..be5d47402 100644 --- a/src/Exceptions/UnauthorizedException.php +++ b/src/Exceptions/UnauthorizedException.php @@ -13,10 +13,10 @@ class UnauthorizedException extends HttpException public static function forRoles(array $roles): self { - $message = 'User does not have the right roles.'; + $message = __('User does not have the right roles.'); if (config('permission.display_role_in_exception')) { - $message .= ' Necessary roles are '.implode(', ', $roles); + $message .= ' ' . __('Necessary roles are :roles', ['roles' => implode(', ', $roles)]); } $exception = new static(403, $message, null, []); @@ -27,10 +27,10 @@ public static function forRoles(array $roles): self public static function forPermissions(array $permissions): self { - $message = 'User does not have the right permissions.'; + $message = __('User does not have the right permissions.'); if (config('permission.display_permission_in_exception')) { - $message .= ' Necessary permissions are '.implode(', ', $permissions); + $message .= ' ' . __('Necessary permissions are :permissions', ['permissions' => implode(', ', $permissions)]); } $exception = new static(403, $message, null, []); @@ -41,10 +41,10 @@ public static function forPermissions(array $permissions): self public static function forRolesOrPermissions(array $rolesOrPermissions): self { - $message = 'User does not have any of the necessary access rights.'; + $message = __('User does not have any of the necessary access rights.'); if (config('permission.display_permission_in_exception') && config('permission.display_role_in_exception')) { - $message .= ' Necessary roles or permissions are '.implode(', ', $rolesOrPermissions); + $message .= ' ' . __('Necessary roles or permissions are :values', ['values' => implode(', ', $rolesOrPermissions)]); } $exception = new static(403, $message, null, []); @@ -57,12 +57,14 @@ public static function missingTraitHasRoles(Authorizable $user): self { $class = get_class($user); - return new static(403, "Authorizable class `{$class}` must use Spatie\Permission\Traits\HasRoles trait.", null, []); + return new static(403, __('Authorizable class `:class` must use Spatie\\Permission\\Traits\\HasRoles trait.', [ + 'class' => $class, + ]), null, []); } public static function notLoggedIn(): self { - return new static(403, 'User is not logged in.', null, []); + return new static(403, __('User is not logged in.'), null, []); } public function getRequiredRoles(): array diff --git a/src/Exceptions/WildcardPermissionInvalidArgument.php b/src/Exceptions/WildcardPermissionInvalidArgument.php index 44f092401..2c0a802ee 100644 --- a/src/Exceptions/WildcardPermissionInvalidArgument.php +++ b/src/Exceptions/WildcardPermissionInvalidArgument.php @@ -8,6 +8,6 @@ class WildcardPermissionInvalidArgument extends InvalidArgumentException { public static function create() { - return new static('Wildcard permission must be string, permission id or permission instance'); + return new static(__('Wildcard permission must be string, permission id or permission instance')); } } diff --git a/src/Exceptions/WildcardPermissionNotImplementsContract.php b/src/Exceptions/WildcardPermissionNotImplementsContract.php index 7b81c73c9..52deaa456 100644 --- a/src/Exceptions/WildcardPermissionNotImplementsContract.php +++ b/src/Exceptions/WildcardPermissionNotImplementsContract.php @@ -8,6 +8,6 @@ class WildcardPermissionNotImplementsContract extends InvalidArgumentException { public static function create() { - return new static('Wildcard permission class must implements Spatie\Permission\Contracts\Wildcard contract'); + return new static(__('Wildcard permission class must implement Spatie\\Permission\\Contracts\\Wildcard contract')); } } diff --git a/src/Exceptions/WildcardPermissionNotProperlyFormatted.php b/src/Exceptions/WildcardPermissionNotProperlyFormatted.php index 721157e6c..54b1e2915 100644 --- a/src/Exceptions/WildcardPermissionNotProperlyFormatted.php +++ b/src/Exceptions/WildcardPermissionNotProperlyFormatted.php @@ -8,6 +8,8 @@ class WildcardPermissionNotProperlyFormatted extends InvalidArgumentException { public static function create(string $permission) { - return new static("Wildcard permission `{$permission}` is not properly formatted."); + return new static(__('Wildcard permission `:permission` is not properly formatted.', [ + 'permission' => $permission, + ])); } } From db1184b113f37ba6639bd7fdefa1872d9b38a4e9 Mon Sep 17 00:00:00 2001 From: drbyte <404472+drbyte@users.noreply.github.com> Date: Tue, 3 Jun 2025 21:46:35 +0000 Subject: [PATCH 0994/1013] Fix styling --- src/Exceptions/UnauthorizedException.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Exceptions/UnauthorizedException.php b/src/Exceptions/UnauthorizedException.php index be5d47402..b9e9fe902 100644 --- a/src/Exceptions/UnauthorizedException.php +++ b/src/Exceptions/UnauthorizedException.php @@ -16,7 +16,7 @@ public static function forRoles(array $roles): self $message = __('User does not have the right roles.'); if (config('permission.display_role_in_exception')) { - $message .= ' ' . __('Necessary roles are :roles', ['roles' => implode(', ', $roles)]); + $message .= ' '.__('Necessary roles are :roles', ['roles' => implode(', ', $roles)]); } $exception = new static(403, $message, null, []); @@ -30,7 +30,7 @@ public static function forPermissions(array $permissions): self $message = __('User does not have the right permissions.'); if (config('permission.display_permission_in_exception')) { - $message .= ' ' . __('Necessary permissions are :permissions', ['permissions' => implode(', ', $permissions)]); + $message .= ' '.__('Necessary permissions are :permissions', ['permissions' => implode(', ', $permissions)]); } $exception = new static(403, $message, null, []); @@ -44,7 +44,7 @@ public static function forRolesOrPermissions(array $rolesOrPermissions): self $message = __('User does not have any of the necessary access rights.'); if (config('permission.display_permission_in_exception') && config('permission.display_role_in_exception')) { - $message .= ' ' . __('Necessary roles or permissions are :values', ['values' => implode(', ', $rolesOrPermissions)]); + $message .= ' '.__('Necessary roles or permissions are :values', ['values' => implode(', ', $rolesOrPermissions)]); } $exception = new static(403, $message, null, []); From 31c05679102c73f3b0d05790d2400182745a5615 Mon Sep 17 00:00:00 2001 From: Jimi Robaer Date: Thu, 5 Jun 2025 09:33:07 +0200 Subject: [PATCH 0995/1013] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 12bed721f..cf4e7e7f8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ - Logo for laravel-permission + Logo for laravel-permission From 5b26459ea34d79de6a9b021e7cb3775d69cc2c5b Mon Sep 17 00:00:00 2001 From: drbyte <404472+drbyte@users.noreply.github.com> Date: Sat, 14 Jun 2025 01:22:04 +0000 Subject: [PATCH 0996/1013] Update CHANGELOG --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56bd868eb..0005da4eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.20.0 - 2025-06-14 + +### What's Changed + +* Add translations support for exception messages by @nAa6666 in https://github.com/spatie/laravel-permission/pull/2852 + +### New Contributors + +* @nAa6666 made their first contribution in https://github.com/spatie/laravel-permission/pull/2852 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.19.0...6.20.0 + ## 6.19.0 - 2025-05-31 ### What's Changed @@ -1038,6 +1050,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` @@ -1118,6 +1131,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` From a5e2ab61e77f605129d35f72353eac92b91bb0d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 05:20:45 +0000 Subject: [PATCH 0997/1013] Bump stefanzweifel/git-auto-commit-action from 5 to 6 Bumps [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action) from 5 to 6. - [Release notes](https://github.com/stefanzweifel/git-auto-commit-action/releases) - [Changelog](https://github.com/stefanzweifel/git-auto-commit-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/stefanzweifel/git-auto-commit-action/compare/v5...v6) --- updated-dependencies: - dependency-name: stefanzweifel/git-auto-commit-action dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/fix-php-code-style-issues.yml | 2 +- .github/workflows/update-changelog.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/fix-php-code-style-issues.yml b/.github/workflows/fix-php-code-style-issues.yml index 60dc90d6f..376238340 100644 --- a/.github/workflows/fix-php-code-style-issues.yml +++ b/.github/workflows/fix-php-code-style-issues.yml @@ -19,6 +19,6 @@ jobs: uses: aglipanci/laravel-pint-action@v2 - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v5 + uses: stefanzweifel/git-auto-commit-action@v6 with: commit_message: Fix styling diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index 0cdea2336..de5865b94 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -21,7 +21,7 @@ jobs: release-notes: ${{ github.event.release.body }} - name: Commit updated CHANGELOG - uses: stefanzweifel/git-auto-commit-action@v5 + uses: stefanzweifel/git-auto-commit-action@v6 with: branch: main commit_message: Update CHANGELOG From e71f6f32535c86166fcf511d87464c35c8cf50e3 Mon Sep 17 00:00:00 2001 From: TobMoeller Date: Mon, 7 Jul 2025 00:09:16 +0200 Subject: [PATCH 0998/1013] add option to remove multiple roles in one function call --- src/Events/RoleDetached.php | 4 ++-- src/Traits/HasRoles.php | 11 +++++----- tests/HasRolesTest.php | 41 +++++++++++++++++++++++++++++-------- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/src/Events/RoleDetached.php b/src/Events/RoleDetached.php index 287995fe6..9a32518ab 100644 --- a/src/Events/RoleDetached.php +++ b/src/Events/RoleDetached.php @@ -18,8 +18,8 @@ class RoleDetached use SerializesModels; /** - * Internally the HasRoles trait passes $rolesOrIds as a single Eloquent record - * Theoretically one could register the event to other places with an array etc + * Internally the HasRoles trait passes an array of role ids (eg: int's or uuid's) + * Theoretically one could register the event to other places passing other types * So a Listener should inspect the type of $rolesOrIds received before using. * * @param array|int[]|string[]|Role|Role[]|Collection $rolesOrIds diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index d10dd77e3..38d2f02c0 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -193,13 +193,14 @@ function ($object) use ($roles, $model, $teamPivot, &$saved) { /** * Revoke the given role from the model. * - * @param string|int|Role|\BackedEnum $role + * @param string|int|array|Role|Collection|\BackedEnum ...$role + * @return $this */ - public function removeRole($role) + public function removeRole(...$role) { - $storedRole = $this->getStoredRole($role); + $roles = $this->collectRoles($role); - $this->roles()->detach($storedRole); + $this->roles()->detach($roles); $this->unsetRelation('roles'); @@ -208,7 +209,7 @@ public function removeRole($role) } if (config('permission.events_enabled')) { - event(new RoleDetached($this->getModel(), $storedRole)); + event(new RoleDetached($this->getModel(), $roles)); } return $this; diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 2ead3982e..734394706 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -181,42 +181,62 @@ public function it_can_assign_and_remove_a_role_on_a_permission() /** @test */ #[Test] - public function it_can_assign_a_role_using_an_object() + public function it_can_assign_and_remove_a_role_using_an_object() { $this->testUser->assignRole($this->testUserRole); $this->assertTrue($this->testUser->hasRole($this->testUserRole)); + + $this->testUser->removeRole($this->testUserRole); + + $this->assertFalse($this->testUser->hasRole($this->testUserRole)); } /** @test */ #[Test] - public function it_can_assign_a_role_using_an_id() + public function it_can_assign_and_remove_a_role_using_an_id() { $this->testUser->assignRole($this->testUserRole->getKey()); $this->assertTrue($this->testUser->hasRole($this->testUserRole)); + + $this->testUser->removeRole($this->testUserRole->getKey()); + + $this->assertFalse($this->testUser->hasRole($this->testUserRole)); } /** @test */ #[Test] - public function it_can_assign_multiple_roles_at_once() + public function it_can_assign_and_remove_multiple_roles_at_once() { $this->testUser->assignRole($this->testUserRole->getKey(), 'testRole2'); $this->assertTrue($this->testUser->hasRole('testRole')); $this->assertTrue($this->testUser->hasRole('testRole2')); + + $this->testUser->removeRole($this->testUserRole->getKey(), 'testRole2'); + + $this->assertFalse($this->testUser->hasRole('testRole')); + + $this->assertFalse($this->testUser->hasRole('testRole2')); } /** @test */ #[Test] - public function it_can_assign_multiple_roles_using_an_array() + public function it_can_assign_and_remove_multiple_roles_using_an_array() { $this->testUser->assignRole([$this->testUserRole->getKey(), 'testRole2']); $this->assertTrue($this->testUser->hasRole('testRole')); $this->assertTrue($this->testUser->hasRole('testRole2')); + + $this->testUser->removeRole([$this->testUserRole->getKey(), 'testRole2']); + + $this->assertFalse($this->testUser->hasRole('testRole')); + + $this->assertFalse($this->testUser->hasRole('testRole2')); } /** @test */ @@ -973,14 +993,19 @@ public function it_fires_an_event_when_a_role_is_removed() Event::fake(); app('config')->set('permission.events_enabled', true); - $this->testUser->assignRole('testRole'); + $this->testUser->assignRole('testRole', 'testRole2'); - $this->testUser->removeRole('testRole'); + $this->testUser->removeRole('testRole', 'testRole2'); - Event::assertDispatched(RoleDetached::class, function ($event) { + $roleIds = app(Role::class)::whereIn('name', ['testRole', 'testRole2']) + ->pluck($this->testUserRole->getKeyName()) + ->toArray(); + + Event::assertDispatched(RoleDetached::class, function ($event) use ($roleIds) { return $event->model instanceof User && ! $event->model->hasRole('testRole') - && $event->rolesOrIds->name === 'testRole'; + && ! $event->model->hasRole('testRole2') + && $event->rolesOrIds === $roleIds; }); } From 52a2970793d6788b000261b6fbf284d0523bf6b2 Mon Sep 17 00:00:00 2001 From: dualklip Date: Thu, 17 Jul 2025 15:25:45 +0200 Subject: [PATCH 0999/1013] Correct middleware order for documentation example in `teams-permissions.md`. --- docs/basic-usage/teams-permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/teams-permissions.md b/docs/basic-usage/teams-permissions.md index 684770ded..868ae4281 100644 --- a/docs/basic-usage/teams-permissions.md +++ b/docs/basic-usage/teams-permissions.md @@ -77,8 +77,8 @@ class AppServiceProvider extends ServiceProvider $kernel = app()->make(Kernel::class); $kernel->addToMiddlewarePriorityBefore( - SubstituteBindings::class, YourCustomMiddlewareClass::class, + SubstituteBindings::class, ); } } From da53256252f71a1a42051382142ee930b07999f9 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 23 Jul 2025 07:37:53 -0400 Subject: [PATCH 1000/1013] Update direct-permissions.md --- docs/basic-usage/direct-permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/direct-permissions.md b/docs/basic-usage/direct-permissions.md index e171c8163..6b4987ec5 100644 --- a/docs/basic-usage/direct-permissions.md +++ b/docs/basic-usage/direct-permissions.md @@ -46,7 +46,7 @@ Like all permissions assigned via roles, you can check if a user has a permissio $user->can('edit articles'); ``` -> NOTE: The following `hasPermissionTo`, `hasAnyPermission`, `hasAllPermissions` functions do not support Super-Admin functionality. Use `can`, `canAny`, `canAll` instead. +> NOTE: The following `hasPermissionTo`, `hasAnyPermission`, `hasAllPermissions` functions do not support Super-Admin functionality. Use `can`, `canAny` instead. You can check if a user has a permission: From a028147780a2832aa7b3a6d6b522a61c87ea51fd Mon Sep 17 00:00:00 2001 From: drbyte <404472+drbyte@users.noreply.github.com> Date: Wed, 23 Jul 2025 16:13:20 +0000 Subject: [PATCH 1001/1013] Update CHANGELOG --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0005da4eb..b76054f36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.21.0 - 2025-07-23 + +### What's Changed + +* Allow removing multiple roles with the `removeRole` method by @TobMoeller in https://github.com/spatie/laravel-permission/pull/2859 +* [Docs] Correct middleware order for documentation example in `teams-permissions.md` by @dualklip in https://github.com/spatie/laravel-permission/pull/2863 + +### New Contributors + +* @dualklip made their first contribution in https://github.com/spatie/laravel-permission/pull/2863 +* @TobMoeller made their first contribution in https://github.com/spatie/laravel-permission/pull/2859 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.20.0...6.21.0 + ## 6.20.0 - 2025-06-14 ### What's Changed @@ -1051,6 +1065,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` @@ -1132,6 +1147,7 @@ The following changes are not "breaking", but worth making the updates to your a + ``` From 7bd6e44f99809bfcb73db3ee7d019ccc6e2a2f34 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Thu, 31 Jul 2025 16:05:07 -0400 Subject: [PATCH 1002/1013] Add livewire middleware persistence doc link --- docs/basic-usage/teams-permissions.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/basic-usage/teams-permissions.md b/docs/basic-usage/teams-permissions.md index 868ae4281..15095a7ab 100644 --- a/docs/basic-usage/teams-permissions.md +++ b/docs/basic-usage/teams-permissions.md @@ -83,6 +83,9 @@ class AppServiceProvider extends ServiceProvider } } ``` +### Using LiveWire? + +You may need to register your team middleware as Persisted in Livewire. See [Livewire docs: Configuring Persistent Middleware](https://livewire.laravel.com/docs/security#configuring-persistent-middleware) ## Roles Creating From ae68164eba5f09119d165f367052c026e6bd1734 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 1 Aug 2025 03:12:39 -0400 Subject: [PATCH 1003/1013] Update tests badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cf4e7e7f8..413063953 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

Associate users with permissions and roles

[![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-permission.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-permission) -[![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/spatie/laravel-permission/run-tests-L8.yml?branch=main&label=Tests)](https://github.com/spatie/laravel-permission/actions?query=workflow%3ATests+branch%3Amain) +[![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/spatie/laravel-permission/run-tests.yml?branch=main&label=Tests)](https://github.com/spatie/laravel-permission/actions?query=workflow%3ATests+branch%3Amain) [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-permission.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-permission) From a4316d6e01377bf29ac4c382eafa3f0492bcabd0 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 8 Aug 2025 18:14:38 -0400 Subject: [PATCH 1004/1013] Add roave/security-advisories --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index c04c5bff9..c83d07b2e 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,8 @@ "laravel/passport": "^11.0|^12.0", "laravel/pint": "^1.0", "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0|^10.0", - "phpunit/phpunit": "^9.4|^10.1|^11.5" + "phpunit/phpunit": "^9.4|^10.1|^11.5", + "roave/security-advisories": "dev-latest" }, "minimum-stability": "dev", "prefer-stable": true, From 0b9284c00640febe7a7d01471c5a145f543b65f0 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Fri, 8 Aug 2025 21:05:22 -0400 Subject: [PATCH 1005/1013] Revert "Add roave/security-advisories" This reverts commit a4316d6e01377bf29ac4c382eafa3f0492bcabd0. --- composer.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/composer.json b/composer.json index c83d07b2e..c04c5bff9 100644 --- a/composer.json +++ b/composer.json @@ -32,8 +32,7 @@ "laravel/passport": "^11.0|^12.0", "laravel/pint": "^1.0", "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0|^10.0", - "phpunit/phpunit": "^9.4|^10.1|^11.5", - "roave/security-advisories": "dev-latest" + "phpunit/phpunit": "^9.4|^10.1|^11.5" }, "minimum-stability": "dev", "prefer-stable": true, From c3edf29c4d0fb40b12e3f65e0ffa16026ce757c1 Mon Sep 17 00:00:00 2001 From: josedaian Date: Sun, 17 Aug 2025 11:22:21 -0300 Subject: [PATCH 1006/1013] feat: dispatch RoleDetached on syncRoles when events are enabled --- src/Traits/HasRoles.php | 11 +++++++++-- tests/HasRolesTest.php | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index 38d2f02c0..40839e94c 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -225,8 +225,15 @@ public function syncRoles(...$roles) { if ($this->getModel()->exists) { $this->collectRoles($roles); - $this->roles()->detach(); - $this->setRelation('roles', collect()); + if (config('permission.events_enabled')) { + $currentRoles = $this->roles()->get(); + if ($currentRoles->isNotEmpty()) { + $this->removeRole($currentRoles); + } + } else { + $this->roles()->detach(); + $this->setRelation('roles', collect()); + } } return $this->assignRole($roles); diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 734394706..cf49e3dd6 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -1042,4 +1042,43 @@ public function it_can_be_given_a_role_on_user_when_lazy_loading_is_restricted() $this->fail('Lazy loading detected in the givePermissionTo method: '.$e->getMessage()); } } + + /** @test */ + #[Test] + public function it_fires_detach_event_when_syncing_roles() + { + Event::fake([RoleDetached::class, RoleAttached::class]); + app('config')->set('permission.events_enabled', true); + + $this->testUser->assignRole('testRole', 'testRole2'); + + app(Role::class)->create(['name' => 'testRole3']); + + $this->testUser->syncRoles('testRole3'); + + $this->assertFalse($this->testUser->hasRole('testRole')); + $this->assertFalse($this->testUser->hasRole('testRole2')); + $this->assertTrue($this->testUser->hasRole('testRole3')); + + $removedRoleIds = app(Role::class)::whereIn('name', ['testRole', 'testRole2']) + ->pluck($this->testUserRole->getKeyName()) + ->toArray(); + + Event::assertDispatched(RoleDetached::class, function ($event) use ($removedRoleIds) { + return $event->model instanceof User + && ! $event->model->hasRole('testRole') + && ! $event->model->hasRole('testRole2') + && $event->rolesOrIds === $removedRoleIds; + }); + + $attachedRoleIds = app(Role::class)::whereIn('name', ['testRole3']) + ->pluck($this->testUserRole->getKeyName()) + ->toArray(); + + Event::assertDispatched(RoleAttached::class, function ($event) use ($attachedRoleIds) { + return $event->model instanceof User + && $event->model->hasRole('testRole3') + && $event->rolesOrIds === $attachedRoleIds; + }); + } } From 041c43beab5cf6783a07e302d178c5f2c7bcc287 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 07:29:50 +0000 Subject: [PATCH 1007/1013] Bump actions/checkout from 4 to 5 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/fix-php-code-style-issues.yml | 2 +- .github/workflows/phpstan.yml | 2 +- .github/workflows/run-tests.yml | 2 +- .github/workflows/test-cache-drivers.yml | 2 +- .github/workflows/update-changelog.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/fix-php-code-style-issues.yml b/.github/workflows/fix-php-code-style-issues.yml index 376238340..26b2a4f84 100644 --- a/.github/workflows/fix-php-code-style-issues.yml +++ b/.github/workflows/fix-php-code-style-issues.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: ${{ github.head_ref }} diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index be7882397..17bbcdf3f 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -15,7 +15,7 @@ jobs: name: phpstan runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 3f47884ba..869f05809 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -43,7 +43,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/test-cache-drivers.yml b/.github/workflows/test-cache-drivers.yml index 2bfd5b28a..3b9398c9c 100644 --- a/.github/workflows/test-cache-drivers.yml +++ b/.github/workflows/test-cache-drivers.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index de5865b94..da74ce831 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -10,7 +10,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: main From f6ea7b3e1c40e8724425c61cd6585ae28c862cd6 Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Sat, 23 Aug 2025 11:23:05 -0400 Subject: [PATCH 1008/1013] Indicate 11+ --- docs/basic-usage/middleware.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index 259e9214f..e9173ec59 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -22,7 +22,7 @@ This package comes with `RoleMiddleware`, `PermissionMiddleware` and `RoleOrPerm You can register their aliases for easy reference elsewhere in your app: -In Laravel 11 open `/bootstrap/app.php` and register them there: +In Laravel 11+ open `/bootstrap/app.php` and register them there: ```php ->withMiddleware(function (Middleware $middleware) { From ba8c37a1f590d526f2cc6a133e698e972892039b Mon Sep 17 00:00:00 2001 From: Alex Vanderbist Date: Mon, 8 Sep 2025 08:51:35 +0200 Subject: [PATCH 1009/1013] Update issue template --- .github/ISSUE_TEMPLATE/config.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 5940c1979..8c6da4eb4 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ -blank_issues_enabled: false +blank_issues_enabled: true contact_links: - - name: Ask a Question - url: https://github.com/spatie/laravel-permission/discussions/new?category=q-a - about: Ask the community for help - name: Feature Request url: https://github.com/spatie/laravel-permission/discussions/new?category=ideas about: Share ideas for new features + - name: Ask a Question + url: https://github.com/spatie/laravel-permission/discussions/new?category=q-a + about: Ask the community for help From 115f5267b45f7654487de26b6d383de21c179d1a Mon Sep 17 00:00:00 2001 From: erikn69 Date: Wed, 1 Oct 2025 16:31:27 -0500 Subject: [PATCH 1010/1013] Test PHP 8.5 --- .github/workflows/run-tests.yml | 8 +++++++- tests/TestCase.php | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 869f05809..2dd07ca39 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - php: [8.4, 8.3, 8.2, 8.1, 8.0] + php: [8.5, 8.4, 8.3, 8.2, 8.1, 8.0] laravel: ["^12.0", "^11.0", "^10.0", "^9.0", "^8.12"] dependency-version: [prefer-lowest, prefer-stable] include: @@ -34,10 +34,16 @@ jobs: php: 8.0 - laravel: "^10.0" php: 8.0 + - laravel: "^10.0" + php: 8.5 + - laravel: "^9.0" + php: 8.5 - laravel: "^8.12" php: 8.3 - laravel: "^8.12" php: 8.4 + - laravel: "^8.12" + php: 8.5 name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} diff --git a/tests/TestCase.php b/tests/TestCase.php index 45831ce15..ebd66f3cc 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -230,7 +230,7 @@ protected function setUpPassport($app): void $app['config']->set('auth.guards.api', ['driver' => 'passport', 'provider' => 'users']); // mimic passport:install (must load migrations using our own call to loadMigrationsFrom() else rollbacks won't occur, and migrations will be left in skeleton directory - $this->artisan('passport:keys'); + //$this->artisan('passport:keys'); $this->loadMigrationsFrom(__DIR__.'/../vendor/laravel/passport/database/migrations/'); $provider = in_array('users', array_keys(config('auth.providers'))) ? 'users' : null; $this->artisan('passport:client', ['--personal' => true, '--name' => config('app.name').' Personal Access Client']); From 8a795bab595607d8c525eb04615ae88bbcf26616 Mon Sep 17 00:00:00 2001 From: drbyte <404472+drbyte@users.noreply.github.com> Date: Thu, 2 Oct 2025 19:19:17 +0000 Subject: [PATCH 1011/1013] Fix styling --- tests/TestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestCase.php b/tests/TestCase.php index ebd66f3cc..cc243faa1 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -230,7 +230,7 @@ protected function setUpPassport($app): void $app['config']->set('auth.guards.api', ['driver' => 'passport', 'provider' => 'users']); // mimic passport:install (must load migrations using our own call to loadMigrationsFrom() else rollbacks won't occur, and migrations will be left in skeleton directory - //$this->artisan('passport:keys'); + // $this->artisan('passport:keys'); $this->loadMigrationsFrom(__DIR__.'/../vendor/laravel/passport/database/migrations/'); $provider = in_array('users', array_keys(config('auth.providers'))) ? 'users' : null; $this->artisan('passport:client', ['--personal' => true, '--name' => config('app.name').' Personal Access Client']); From 69062a9a51bf1620584e50c8d1c99434b25b8a1e Mon Sep 17 00:00:00 2001 From: Ali Qasemzadeh Date: Sun, 12 Oct 2025 08:31:53 +0330 Subject: [PATCH 1012/1013] Add QuickPanel link to UI options documentation --- docs/advanced-usage/ui-options.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/advanced-usage/ui-options.md b/docs/advanced-usage/ui-options.md index ebfb552ef..4e1d5d41d 100644 --- a/docs/advanced-usage/ui-options.md +++ b/docs/advanced-usage/ui-options.md @@ -31,3 +31,5 @@ If you decide you need a UI, even if it's not for creating/editing role/permissi - [LiveWire Base Admin Panel](https://github.com/aliqasemzadeh/bap) User management by [AliQasemzadeh](https://github.com/aliqasemzadeh) - [JetAdmin](https://github.com/aliqasemzadeh/jetadmin) JetAdmin use laravel livewire starter kit and manage permissions. [AliQasemzadeh](https://github.com/aliqasemzadeh) + +- [QuickPanel](https://github.com/aliqasemzadeh/quickpanel) Quick Panel (TALL Flowbite Starter Kit). [AliQasemzadeh](https://github.com/aliqasemzadeh) From 691ae1050d8cf4e8eee3de294563110529ab2c7c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 05:08:33 +0000 Subject: [PATCH 1013/1013] Bump stefanzweifel/git-auto-commit-action from 6 to 7 Bumps [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action) from 6 to 7. - [Release notes](https://github.com/stefanzweifel/git-auto-commit-action/releases) - [Changelog](https://github.com/stefanzweifel/git-auto-commit-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/stefanzweifel/git-auto-commit-action/compare/v6...v7) --- updated-dependencies: - dependency-name: stefanzweifel/git-auto-commit-action dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/fix-php-code-style-issues.yml | 2 +- .github/workflows/update-changelog.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/fix-php-code-style-issues.yml b/.github/workflows/fix-php-code-style-issues.yml index 26b2a4f84..e3be7f171 100644 --- a/.github/workflows/fix-php-code-style-issues.yml +++ b/.github/workflows/fix-php-code-style-issues.yml @@ -19,6 +19,6 @@ jobs: uses: aglipanci/laravel-pint-action@v2 - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v6 + uses: stefanzweifel/git-auto-commit-action@v7 with: commit_message: Fix styling diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index da74ce831..cb7fa9f8b 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -21,7 +21,7 @@ jobs: release-notes: ${{ github.event.release.body }} - name: Commit updated CHANGELOG - uses: stefanzweifel/git-auto-commit-action@v6 + uses: stefanzweifel/git-auto-commit-action@v7 with: branch: main commit_message: Update CHANGELOG
+ + + + Logo for laravel-permission + + -# Associate users with permissions and roles +

Associate users with permissions and roles

[![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-permission.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-permission) [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/spatie/laravel-permission/run-tests-L8.yml?branch=main&label=Tests)](https://github.com/spatie/laravel-permission/actions?query=workflow%3ATests+branch%3Amain) [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-permission.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-permission) + +

v&*Z-8v4#S0B!9Tu*10Mzc!S_nk)+;^XYl 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%0Dk9yD_OEA<_R!I7U}PgKEXnomy0Ea7 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 Relc(9 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%0A+_nsJLZ@uibAd z4rYMKKL;T>CmV|-xAmb)fJ~>!ae%S~(yg@Acxm;iCEnh*R>$?3+NaASRA=hfEQnb~ zhx7;8^^SsdnL{mZtdc0mG#Q(@vH6)RV)C#i>-}0U;OT{e2jFpGEvrzth|+HeFNTs_ zj@Coiq08-sSB1iui>_yN^<KWpCT2`?d#lm=y~I*xH-e&S1&=bezxpCSrHD^|@n;u@E?Cpq^PXDfHR6eNqC#)t@V|gx_HwbNSlvV`T3$ z_{Dc`;_P{a45r{`U-XlI@wpnjUDvKBc4@ib_!#cIxTK|uh;L6%v{1U8Mr2lRXzT~3 z0;xZwy9Z&;$1|Na7A1FUpY8_XGiSPyx-e2MOJ3!Kv_Dt~cs^V%*}(W{$(?9QmruTO zg9`c==i)UOe17hdKmRG4)YGK0X{WV|h#y_w$KT*mzWk7Ke6m1tZJ}Q%b4mBC(msI`y~-0O z6ZQ)aM_J(mV>yfteD$bI!Oh)Ei4Bko{xpkDNI780G^>srJtdn{aFVWub(RZ@tOUkU z{NbHKL|`lt%V{Q@;D&7sfGOjYvi&pCgX5j<)$rXmrLj+Br%l))bgVUk+>;_ znN2D!7Ut4D5v9;o3WJK5JU#@Yb5Cn zneh`j@#RZbPZz1}#!x#f$eJ9Uzcz+g#`^Wy$luuxI6C@=?>dq#PxnErctq zM;iN;7A6Ol2MqA^j=}e8^iSi54*}W?F=34NoG^8v3yguF?VdJL9FMt}o{QUZEHgaZ zP?ArTbp?o?TPP(iX0a|LgpMihkq>P6vSr$Fr~Y+Al`6Hw1azJlw*akOV1v;SUc||q zNpO7B>^<2ZvMPM>OaVJ@#SKJE1*yzc8*x^ohs5#Y6$2BZlL~G9Ef5!KNz-yNAi=iS3j4rL~Qb4l+T;E)>lku*;4Sn5@irW-Oj*Ayy?GYr{+&~5c0B1w9>aE1s)KQtSeb42H-IQ<#8%eLpQ8!4{^>I z%i5om=S7@9cQ2T=f5EWudc*(J^ur^YYo%wVx7u0%#=rok2k9R^5SipDCksGc$Vx$o*AmW0bphdc`ICf zz0Wa6S_~QJ|;dq;CZfTSIPRPp5qM77Hk-6S^|e2-I=RN+anH7 zu|UF8oyt@}YT(tvC$%=V)j*EVI(6^kcnNdz?}_7j=_8t~_W>TTHo^c?IG7X_v$~m^IpQ+CRdR+p ziXk((fc0g4=Q&Ob!*P3dG-U@)J`?yH=s4M<2As=x#0>6AOm%9$lW$%*x0(K`Pnzc9 zxcM4jBJo+_F#Ms@R9_CvIqsn5RVlwFhQJqh!OGv5>%%8{4iDBK&#mF)!#H%HL=t{S zBH<%WAY6kz^-m^f4nN65`HEjL|H>~7x!6f5i1qVadmBKSDu3mtyxLB@*zjAtCs#ah z%8`hHUJZ z?bcG|0}t+shuoCZl-A5tlT{ug#T6ntgK(treLD*XpLc_07fRc~WbKW$8w`gLpD@K? z*IKcYF+y3Ytejw}e6F<5<4A)=U3)zTYfBWlU=)z=9zJpao>I9>zIRw2xK~ZE4h18j zfhQ}V5K9pe>LgpEL!CgQ?L{wLr5#k6QeR|b=6(&L@Bsf6a!Ppy3 zRP$OjhTbe^_MuOjiytMH;!J>}A{d1W{f4l=j8W%yM*e&49};0&`fTtt3lrR6u6c*C zhX$(WxXpgt=*LVl*~*qGKaS%FO*3YjL{~pMfzvY>llC3Vrnb$l(8b7Pv^kUlu7xEb zUzB+?rwj~f9eTRL5H**Z&{!RF+VXwZaZi^dhE4&e*}B*Eg(1L*pba{JF>aw&6dLZK z?5=?t7@!`~_L!+FsW?FibS5+{CXN~-%r>wtna;y)mqD%kR#(F!a=wxCG{g>YHQ-yH6F6wh33!e9Ffeww8MC>e*^^Vf$C{!s#_i!QRwDU?YdK zwbOXAclEf(>?f=bhHP0D5#C(!{rpJ6$I(Hb`*nP$mzP(zn^L_2lf%PhBrgTL5K3J^ zP%#;SWFiAu&({)h+YMN&k3pdnWLGCl;Z>adFm$g&pAd;BbuaS4pq^^0pX}y|B1tf+ubdjYKk2HNla} zeZfa0t9eOC77J%@YCI@UOZ(sejH4^QzkRDr+Dt*w3TkIcY^qL{rA6i*2zmQTG=dpU zOzBc19~Uq+lNfY5&#Df03Uq?!DCs}8V{tU)>X5SKtVCp4q54RGD=SnquwJyE8K#I* z;jv&63J#lUS$JA@qeo9Q-SePUWAKF6_x-l}OC1qbSI=o9!pB?C*Y2oEATE7R>(~ z;_-O0SfKt7PV~m-1CG<{q~aOQ&Bevq&T~XRH1MNZ#mlp#Ov}4UPl>0>LYIM09@<*_ z^klW*bvHNx^j5v6V-C`7^(%sq7ANN8$^#+N=WA+^e&cALCS|jeC}ispV?VTVbPP;I zF9-RE)%n0eG`qAi&P|M1Rrbhf$u^+rcL@Hk5jh3)Ybhm9_Ak~4$_dILOv|<%7kq!mqX6C8Ah-M17fK=| zJ4K2~>Blt}Q|~*K%V7oWvD%zu*whZhPOIL%de+n~ulvmks95+G?25jR^d7CH=m=g} z{DCD6DT;L@(Fl(0|1n9@74GBfX@L_19oPa+FkjYV51<-!1dUz;c~O$|)c>Qo)eY9d zC+(kEH0hQnZ1br}L<)sbirsem!53|=%{+$FPeA*aq5U^+FAWQk(U%0IxDA+GkT4~clI9<}{NL~Gs|b_wq!10&9e^J1EQL$_(QkQj86u4BRMV-unv z3Cx$(T{e&ZeWSb$c>DHkzi$$%H1#uiSFsd()X^0L$F8yRSI!rsQ0{sqd$E+zrZnk5 zIGpgE7x@yJ3MO=LM#4^Jyc`RmgOY9QoEoopnRi{u71RbsY8?wF&V6;H=}_d)-~wW- z%d21-bd_ZBNcim1tea{a5_4w*sT+-Rw)^Yrm)F%9 zXk~c}>H4VET-B69ieR~bK*47yVLct#QX!eA$5@Jx@&4X8_1<_%P?lNLoPLX0VUm>> z9||`nX;aD>zw9AShcyJ45k7P0^97%Mq{+oJ?opZ@;fSgFr6Q|UfI_c0KMt^YHKf$c z>zuvBFU2`I8LkGhj~hz3Bx_&CUp?XT%rlfJw;3K`RcD`+V3azi!K>R!79b06ni zLAMc1>%SGG6sv{lNh(>g+AqE}gd4xj>`V)HktkN#%SK*Rl{0pEV3kfOn?Nc{EiZo84W`vTf=g~BX(}5eN4A$ z;{$5wqUaQu_7aYgtwkPV9jt>xAX83q^TD^%O*1{P-ru3!3OlyQ2d3m+Z!WlnNi33M zf9;aqiL>OZF9Yc5D5(uzqjWmK{AtLRFHbGO#juugQWPc z?+xkCq3m}CK42ZHh3B}XCMu``J!6KD^Cni4} zu8RV})kh|cPFAo14~9u zSh3S&FwYISc%A4=nBvZd6zrAH1CF!NpUPj|@Wbz2hul-Srz)Q9^7umyC5WC_O;h`) z$^+vflG}=>0gnewCf>e7}d$gK{140|yUw3_+_e~5KMN1h*z!HsEMtT`Q%Nfu$djxVs^*P%um99=i zm!Uw+L$M&s8cHkqD_=u6%q~`Wwt(%z5JgP|2Rq;741$^AhwbV`zh9`s_xbfb+-5FK zU!a|7so{fpVinI$4M~k-b2OYB2wTGw?QpUN`l_tL%aakFHi4H{?+=wH`W5%?ToWQ* z-I|0a!J`KMGmP7bH9A>RXYE-^P`JG6IaExyJPn+3_e*Pd*mZNU%ouSiFxo}B8pQ_{ zvTaRegNNl-mZjjsogntHSY0_zK?PZU5YP^6)Ar#LDnpDcEKW-T-l`?;OHKsC(*{<0 zxhQeyA^;Y@dd$12chP!)NXk6(Gc#x_u3DuMH{fis`i0})sbA^gnburuqCPK;dYJB9 z51eA0A`Q-Elc&*pBE1KNod4-6Lp3~20+K^aeyyP4+8L=ZQKQl966KmEl5uG@YSVI& zuYs+2xx#f}F-N}E|8F9CAG+AYF`03;Ff~!itveM8Ovjuzn@$WjhJjfdU;)0Ea|m@L zaJ75SxM;yiPOOAfP{P%pRIA3b4epfvSnLx(>w6_Oz+(%mv|x<#!OxgHKiy7_rU)va zIg8TkQ&=-yYg>+^p3GLPyY^$t9=nXn^l`?Jt$IHWcT_U2zqq_BGYA=DowWtjtnK!R0~x)@?D>m*><}L3%?{ zb$Kqi7AulY{23_rSzem>OO1i02WZnw;j;0oqM4YiPim2=haE9>Ixg&O3B?LnUSr6= zS@0qclx-M*jst>2wsv}9RgVaBybhrJh@P)DYlWWI6bF1OlJ|=B;7Ymgr2u@*dHe zI(2=q#0XhM>w07Q=B{IcvLzN4?m6(_8ddvlVWg0fm{&xoPwhdlxfF>IE{P(fPGi4LBoVna4wAzTE zRHk@qcXbA30N?aBc=|g(An-x8n;$cr0fDdJ`kBR_rX22VjG0^tU(?zZCzU6N+-EqG z_E0{tjZKNu;d9d-S@5;1_NhHik2p0TQ?aOENKd^s52#2MO|*cWxj&(f(IJQjBSEnk zg%41-hZo_3yNQ=8<@5K%wwCYIC4R1(g>#POjGoQkxA43mj?uH^SvA-vW5g5;;(vQC z3?m0Z_;WcH;$o?*aw-Rz7cUn1=^g_h%?%m@(+P6f6Q0(D?6d^B?r#= ziP3`S()tWir;(|7@X`KuaSCBrf2J{WS#$1xr>9h_1t_|uQfVPQis|I!P=G2($-KD_ z3+vOf-P6Tc7=cds8EyOJFd~Pt7w) zj4D4117U~MbjhWpl3A-NNL&{v7P^?n+<8f&3nTn0aBZjl@-&Ya44ae*F zW90H&EIPdEr5_z`N8IsNNuOlH?1W1bw@&ucRP z1iSd`O}DJnV=LX-10^EG$g^QJkhf)fIE;faKrV;^uJdi>pB+M6+={EYeE|6paY!b* z>mrCE5+W@JqY+M$Aii0{WiywQPWfo$eXbZW5{=8MavSs%-I&&5V)IJMa5~uZiGx2_MKKvmNH;H^R?!CfUiR20KsOR6pqQEUJ)ABO8pyKnuLQkjQGk z^WSYl!ipjW?EH-}a%mnoTYVB~yXqD{AHDc-Lew8Fh&X#mOiMl=XcM_5ikvoVot(=M zY;Y>+&`kdT=h`rC4J2I1&x%pY8D;>1L+<_|#{*Aes;`2z$RzoTkpo^QV&bV}Vkpi6 z*>e*Jr~8Xn{tk0_pMSvA6Dgi-y094A-cWl|V^0Lt(KH`Aab=0(c~3$7V0vdYqc_$udK})KVLrfz(RrgIm-4r#xUFv1sSksL zEccAd=&t$N{bD>1(XNV%DW(jQ#x=^cM)1ts_*5(S;6jCq(9C5`1kacVhMZtZAi<9) z7@9_!cBssHw;wRNvqeSAF~g%E@o2~DkZiw3zrcjC@(RMs)Zr`ilNEa6VUuQtnc*v_ zj|1I3SrFt|xU6hrZ9n|zJZ;x?Yg&*j(cxc>mx_cXlIy2EY*|RarVD$rV~qy%c{0f$!U%s&UWW?9yvp z`Ts?~m(?>vpZAuqLQ@y^2L}En_Z-~J(;c7Yd9Uu!w#AQ14fSSQMnWPyKARGM^ti>x z0%$qgSI$dH{}`enF&L@C)EfJ|9YZJq?EzbDIowJAFU}c$K zV=^yo{2&a8^pqtxXYmOb*eTzIF!tmbD;MWj7JEZCsyP1Y{l23{n)HuTdSgqEjGyKw zu2Xc~)U-Mzm%*?NgIre1RHkKnyUVT53}FI2*jEqD5Aa}b}RR? zWuN#UytGM3Tl@@?%~*_IcHZ|ocz=7pzw0IC5${HNPP&8l#zCXE7urMKhs?7iAdFsW zOPRMh&dKSiD823qW-pvF09)Wf(Kxk4X=$MoK1&>G=`z&Sxd&KrGNHofs0-L)I1$S} zIQ0yH^x+D-sqMOuW-{Za5vp@P_$l@yegH0AG)M4p&j(-q7Y{Df^bsK8@zBhavMwiu z8)PidM@OTt=Z$@?Lcmgr@H=sFAAV7&SjY1=4^9!YaQUBKaqYhLQ)lA_D9$3^;T5OA zrXYwSOduJ|ktf-+1j|1Tk4pC~Gi&`sU+VtEa*Y*csCf&mS6l`jmLIh7!p(rGlr)j^=hROzqy7o4@V)6bBk?gfe3wWw@`${QqmwnMn>Q~=QPAIr|T-n@F)m0pGzV0$M z#0UR2Vlx^}ol(^gmKx4TLB(a*qr=53erb{&q}~(_M$00WQ2hCr)Wwo>4tda5$ zVk?sO?M9E3AESS||9{lI+jb;5vLyzC2eVrL|C^(E&>AgGk>M}}?79sO4>Ggrj5O7K z`eY@^V1zpyFtdGADR5Viha(zJda9#ae!2!G#C@ep9l%Q`SRb}@oAA5?`?QOx9_N7x z(*cG)fK`7qFr9$HIHz|QiTUv>JH?c%hI73&oElIOT4(o#Y)=UWIHX;TIbQ^q#^VYI za6_$T>@Ca}k&k^8?IaP;Y-zE3<U}Vg07m&?H)ccSb(t0^L`f zilD879oYzzuHRE`-VwR;KKoE+P79CaYxV)V1fXa)D4rjxBSu002tAxlwDG!yNpC6g zxwBN$ITyB?Z&(22sbaz)ur6dro{t6{A5$bbbYe=}7yhK`_@`8L>RDq`jca;`2xV!u zj5FPf*z5>igxepTjGQ3!SI@*Xu(+9=~QLA$`1h z!uCAX!9rM(6U}?|>l)`U@0fvss-S+#$L+~kwJO9u?6Ce4PF4T#_IY}`Vd+`ls#pm#$@UUvac!LHC+(_(=Zjry7%YbJ8D#P`}5c zE^6u(lce9FGgh8ZfnC>c!33!6B6xu6^0h3Vi z`u1w_YVOLr7x~@L#NSw%ob#M?4;Z5*XJTiHK^r^ICFda{W!zA(8v|4R zrmN{&?>4@-EM5DmsU*66sOIm1(tjWa%I*TK-|LwZo)5!&k1Z@q zEcr|W95T}bZU1%T!N8(f zc-#zTGt1$&6dZ)Kt+r+m-)#_=-4eVhwlMK3c}6-T@3=5vI|@{|I!geP=$4id>c%mD zO2+UpXv}dk!p`~o7|csz)RO^28+#tr+O$mLvHXTtA!QF}*GR+pO0(eldeSc&HL#~{ z_Exs03rLt@ZUyhPJ~sU%4afoGsRbR0Z~@M^HjB7as-C{4!4YB#$&(WGgh7%EE+Zw! zOKLUbG(6nlBem~;E#l*dtWhozc)FFayHmm#VPWdNA7{$t>%lzHxJ9b0=>XAdaYYdR zL*cy{pEvAe*-hQVTYjWedf}=WeJ{K&qSoKpt3!AL11Ll=Ahdh!6Q9|iclD*}gQ6hd!_eiQuKuz!t+p_6lD&ep9t_1ET?>6@;mpJv(|Nia)U9 z2yD^n(gy3>&<{MU8+50{pMX5EhZl_KQsBz_^F>1SNiQolKD>`hr?FCa5r;3)>r02a zE0Yb6rnY!S+dsmGZQV~6v40()S(qpF$ktPO2m-x1-P2%RiQsPM=@L!S%o6bs7TSU} zqK}A^c|1N|SA9Bm4V{L(o+~+s`qUhL(Bt*pU!R{*zRYi!Qkl6-`$1eRj@yE4E>T0P znkfc#+s2p;#T7FK2Q1<#Z5}HX5bjO24Q>XdNPbtEQ@-ZSL?Ah`9T z@iz&Qd}Zt|V(v&(yJ3>i-K($H=SQN#AKwUwj-yr)xiUMeHCz7W;=P&WKE2uW9Aj8B zF3|!5d`)(&ODf{tjDbZ*k^HtY^D0#2M*Nl-yaje+0);%pqGFF(Oi|*5Dh81=w&M%T zN@J$#5S6kOyXbR=PVypP&?lxnyFs&BFluO)0kl+`>2s`=7V6@tRuN(PsLgYa(zHl4 z3m_i!dkEX;@<|w=XY^7tFsf>Nc@Mw98HlRe& zv2!KUHn)h=T&Zs`W9aNF^BB9M3*E7;^bcJZgDicnY$*9#^FtwpOD$aI5l*hAl-i&~ zmKEI>l<*-|aiUV0x`9Fi*%JtJ78C8Qck_A-T^Q)aK=$GygL8N1AZGJiW>MAl#tP*g zU=FNlx^(I6HimrHIF)YSn_Dq@F(a0==nWwt^$_G34`HZ%3PZt4OIwBfE|HKteb35{ zdg#Pg5V5BOWW6dWsn2xDximD@TD(~6Ts-#TfHD@oS*y#ORS9{*R)k2oFu>s5iIUY9 zdnHT&c(GA%%@cXHhjw~k6h{Q7Pyd{B?Fd?YI3YbgKEx66sfjw%i1mC@3;u#7@N`n@ z#aa#}zldwI5}p=hWF%a+pew_1c-?~t(RD@`l-4C^*e8qsvWcoU(~YwDuZi1BIf#>n ze7=>VD}f*La@o;%G)svj)7Z(aCkW*$zWM>0x!Mp7=RNe3-~$AD3fyeJ;L*dF&e8?w zkB=k$IYH;?uoIJ~i8Ibqy^)UlDS4POG$l2Zd##X`l$ByQEZz*_)4dneHV&X;idZq;9OXpm2e?*(?7s`%&U*O+jeUE$}~=i}qo@$sR_6C)in za6Yx^f`Gn3S&fRpi#D+-JJ+eZ%y(wChn?)0iS&9(PvUMDO74)revi4M43o6Jp3R}J zFW;gJO{Ui|{pth%CJoj4_1CFkj7H2ihi9UPzT0m0p0UAp?qjJ4NGLq8zrm@SW)iUx z>#T2T9B1;Ar1UDS~FNFJVwo)v))sk~l!`{J_h?h7+ zqMu*g4epypRA?*tW;nnr|2rj z8+9Tq10oB-O1>TFE6?4L@lLStN`$;&P%AEzeQ6`-MPXo{csay_9f>B4dELIRQjXtX ztdVBjc@!X=W8Q(laj*ay7omuDK&`oCCYXhq;ej|Kvkg$C`17JkJzxfVPSX$Vm9_|( z;b9tnOZ3uHLB{wx7Tk>)`ePElhRXE>iPG#4%}kQ6_EpICz!n$MNP9^Io+JA2|Kl@Mm67jZWPLJ}Sz%4Z=%7Dh)h*0JLaGOj=x}tQ=$&fhip=?u z>fkVgNJ_d?^BQ!am7k?N(a|8;qGZKO_M&G;j_3_Sq6M4b{MU21S=@4mWXzhOI$`cF zF>bHALdZEnaAb3fgpRw29nh^Yk-DdRkmDc-RfrTjj4=rR+T!vWuho0wOXKH(ov8&6 zezW+9;M6bGsIn}SZD{jmwU>ZliH%j{@*2}XrYSmrACd^CamH$K`6^aamewQ`!8w*A zp5n;SY5`t5pGmzlgzV6H$7>r9}{-?{QQ&$t6QQO^U;sIEZvNi zrBAquHWU#X=|i16PFrGa-1}$VhsWbXK!Yb94BB?KAmSUo_qvY{{evD(ljLT7wL?`B z7wK6eLkJ!eUcT--quQ!ry);^n41i)`q0d4`7ynv76L&Ty_(bvt- z_}AkD>|Pk4i(Ux?_A7=2scUdx$GpIn4N0_8a@{SbkE7v~wf7Wn5?Q6=`Sl#WfByBC zByh?dH@(?B3;XMPa#ZJXvI#N$n(IgWmJWFzuYTk8gtkZj_4?{lkt>yswDwwcuQeq- z;p-y28kO_w^J|Xg9(i+@VGtBR6&)6Tqew_=z|XuoVJe>|iBTX-Q=2zwin@L>{qyUx zyspD^oP70rzIb%Ky_%<)W&ic13&0|jMDlNEEkP$^2E3JXBG(oRtmpgaJ*ETX!DBA00HGBMLY>XkC^F4pQ9&Ez(tgHVtH}6`o*%Leictnpl zZD7R~NL97NH6USf5N1NImMIdGI!eyVp_%oPUvdVqIrzF6if5dIh4BlRe6Uz^?&&G1 z;0PFBiXr16r`4-B4&DAvcf{Yddyxw0bFt*W@a@!=t2`}FYATH$4yYZse;bq_lR*0;t|GBsy44w&$7&D^^eh1Jw5wE0DzW+R8a}dkB#@> z?&1Y@&&G9ddlz!4n`#nvB56E^zo540VdAm|&gFE^%hlYk+CsTPM{yJ315m~(Zn6?I zX4d*K)oQ^d1{`&To}s6V>@7N5>HWf+C20R0IH}loyD@SPVn70y2Q^VHXfhde!utW~RmIM;=%3;t3a`Z9|*W+K~ zysn1bFmC4sU0Mc^SGFH-@|R$~4zvu#o~)4$O%B=v!W$#RArJ&-u`y)V#?1dnMBb^p zhr&A=`ph)S<{ab8F${)*h*fQtGDlafA}p)|N|ylZK}&=qDAxqfp*ueW6FEqeUxeY+ zVh%D~_f?>$-O=soWz?%vWlm@GND=)WW)KLjI9$*;rHzv6Hq5_PnW6Iy>mXdZxZbmq zF~r-*7hlTaQvhG$DMZal&Qlnwfr`^HsJ@C}ETsXL5Qi0{#Zm^WH7>@(ca#$NHVA0B z+Wj`h+Ov;^q+KDc6A}&-e%F*BRxdOW0R7l>g zA+9p1Q=~`XH3Qkzcy^L!?0}(jHYXF#GUn)=Dw#Mh11$tL-We(04g2}qig95@3Xm)N z?&FYmK1f5=Jf*@_XBeZW5rNfHxA2yu*XrImp!`Qf%1({Odeb}@xC|oY;T$R;l!;Dv z!R}noubwa{thOd!cV#>#%wK^CY3ubd@5P`gRj8h1lX5D;m8>Eo; z&ZFNnSS3~X+b)Us03*?wI`E#y$yVjJV*2&JI_dAf1lQx=W{A?_*H9^nmm9z(A&?`G z3|e<+@i`4(asJX^YQN9Es%hOI!Y@HSeB%RjuY?V9CO+>F>&*?bX;XCIoy&@OGHsaA zb>KoC;X@6Fq+WB-8n0QRPALp2jL@HR;TI-=GZb5>u6r}iz~7T3=0BF@GdGsLa6BrKOSeQ&vC$pny6asQ=Mfuf7u zn@5E94~m-Vf>&kYUJ-XEl3AyNpOzsPK{zaY>%R`9!(w zlnx0D;TlPub@KPlzzx`hxuKXHg^pai%wL9k#CPSS&6(L5H5S_XHFvEfNxDCpH2|~t z7~&dlv`@&65##mFnN=Xo6;g6MEz}@>6BAFj(dRgiFkTrPd(xy8==JG>;(I}}h@LoV zCYLW}{(BuOlER59%N7-pJ2DK(ZTqRe1%knb0He;9AR+TP9(6lXR+aNKsmBgZ)eptLml8a5H^Y{p# zgYlfCR?^u##k!5Sk;O6sC1J_TZ}wE;`UK#{C#O_S9H`=WM<9r*kwRHtprQ9wb=^d` z0){ADycCjY$_GGV`_q1KH+1;6L*P=@QM(l>1~(pm5&tG!#>K1YT^Coy(Y5RX4D+@d zpwe}6#^Z7Xz*7Iy!t!@+%Z`xy<9x24V~;Cr^G358-^W)w^6#2YE8hr#+>J_barO1I zm_qzppQ7UB!__2rGc~JWzg#u9SEO{SHuE@P2&N2^5nAcbYeESXXEH`iJzeoKSG10w zc+<|`xG-gzAF!Cst(*xGr~EM^p{cZc7w+ptnam5q>_QBnw)5)zdOf!=O)4(?5GG3+ ze{&Q(?2fK;6g>T7NW>$rL*eTT+b~!Xe2JTVH^aK)dOzH(g%@x$K*CxcQi+w)O4p74 zsLqe00W(T!_hEeaOKf*l&Urwtwbd@hu}8;YbMa2R#TjNLneCptQBI4E%ZjRn6nvP6 z`s)Kc2M@-LL;2ahd8G3yZ9FX7e*eQMLwPtjcb@3@*OG!So~8r9w*mCprG;y6875LR zU&1I;*?pN|`xq}2cb1%fj&+em^QMYQHOQf4Xm|AAFORhX7H}4N$|A#m>*CL8RTji1 zR-~EG_3`TwX+^YhqHs3EKBC>XtuWcitxIm$rzxZooe}F4f7_{2ou*u?>#BYBlaM%DG>gM%bS-@$iieL*FD`%QvF%V|!K1^%G#*bOuO0Kr>- zFr{TnMC}hS#-9O__Y?mm3kWK6dfO0Eal14lyt zyExamFt$d;frl?RO0lgd8s;*2AgZ~Oxd39~-yoov>eOw+7X?0^nzF#q7X8jNdKbiz zZYgqUoQ$T(Y`{>f&NjGl0rUA<0`9O3f2vlJ z=gsCr)m-n!0&cN%FX*vNh76_Lneb<&g%d}vX9AeV(UoWDpq5myuP5%HqX}{))9R-O zz^Ls4Q4Y}uOs*-Ne}!Dya#Y!j-Z`{Gq^fR4cOeSvR|`w=xz;epg~KVhnih6ae;9jR zwY;K}RH<&(VT?wdqkF!{fG+494B(qZG4!l8XH#e0gQ=Kwot{eFoT9ZUs~g6Wl~zI6 z0v=`N%qa*)oZt2Q`>E`{$Kzu*32J070>}$8rX+Tzw5>NqpArVaJ~mIkyxE=PGPh+A!2hQ8fs8G!M#Lr@I+}S<3FX5rf^P41aU4ns4F(Rwck(UaPjS?7{*!*- zzqaY%_Am5MyfR3$?EdBh@%~x*VQdEQsjM~N@eeeC|KHq)o7v^EnjD(CCz(_n^MZ12=#UL9DiedFu%aWelOEN4hZf>4f|w@+I#Kh4Mgj7v{7t^nyloRDeZ;-fL?LIE8)gP)}*5J8ic%>Q5yj!4J4WTOD8 zrTj|Zc5vT8*3~beFz$q@NZR=H3>D~cIc2gQZ8YjuN;;!C$P4jHDotvTZtbRIqekSV zJv_Wga}gBJ6bqa%;+@D~NG43D2e-{u^sox@bZ*iDKQ_RDVF|F=8T;i8BIyClp73i5 z9^Vv#AN}KFG812~_Vs)oeFl!HYD>}Xz!8!qn3eEE1Bs_G;~{1t`dQh0|H>&Jmj~i^ zvQZhMAcUy^@_+=Wcc8{3VEyf%*5&v?Ov!m}jUaa2Y&kOCBXbT_kJ_%eEAi=&io;_N z^kK)5&f1#)=zD_QEOz5pabWKsk3+*7jYiQS6&Id_Rl3UyLy5}2$fBPV(9^7LA}2RY zu%M+e{EAJf-$NJB15 z!WM%#X(h4Qp~tH|J$b^|Lt-pcOXJp1@zd8Hn%C$|pf+K;5>viHh=9c&^&yhgOPuX} zUceh_)=v;MYMuoBM_tIiVYNZ?>a6*LrFqFm{}(upn^bvPh6}r9yRCL9t+=G#e2|>? z+G(7wql~T*M1S%jew#H)Kdf2**;V~5lIg|8|9j%=P52JXJ`f*s8?!vmomBPxzyLWK zwI5_KJdfYZt(fb8CNPSB&-payji)tcCC#BJ6KIeeo~^(gEk&PfvC3ZoAHrT3d{Kg1Epa`0_|0= zH&=_H6Mt(Pq7Al!`N+n{+{6-Ib6l3uAKebY?gv;G@oO4g;j(tHsBzH;Mw@-q6rsSD zY#fax`*bJco&o6&RhgF|)jUfJd=J%Sn$FvW>S~5PQqAmyRfG>1r&jq(y8{@#g+;r} z&Ze`>}f2@acGTE5IdV zqG_tOT^~_ql1m3&9BU1Xa7|T%^eWCylI}&vA#S?aLt0A~-fEGaV|eUGQ)@@u%J>Q% zY>#~q85l%d+sLIYyfOxg3>Bg6&|`q9gw7hqCB>FpBup?riASA%h|?*uyHAFC84*ka z>s(&EN5X5z zHuKwYo<7<%1)BK=mwT2S(bV$3ng+?3V6~6-w;DdzQ5-4~IiP_zRy;T2n}^~uU@F3Q zMhR9#uoeYE!(N+jPde)4d>37G?+cDst7|?=Q}^(9u@{E zxidkGeeD0EBVP?=DLHKri#vQ$rVe$+5YWFDjQ833&^7b%m|;0{O^lIg4d6Cjcxb~s z-s+Y2M?2yi7Pg#1sIN;m+r` z4AJj_wEtZwqgT^1`sNNo4%67_- z|DLWqZpun7JBrM_bP_HauJkfBb@cMPEb46GB2mH_NiIcqx>cH%xLGT7t@O1WqdFN| zJ!7#IN7qU4G`VZ%>kMB&>Cl+(y3zYMoNX{YdX1I(%4Wh@A+VoXi$+CUC8r02NwT=k z8{JLK5=b0ZE<@#9o*=m=+GO>hm=I8dW^N+JiZdHGn`zSRl#4dO${Qbu1^KXt?F1**YgclS{&0tX5o&j-sFm!NQYw;`2ta78Rx*5E)bn@#^%*>Bz11uu zLMEkQOVptDlV~)_s*(+AP(b%{CH#6Vwb%1BzJyUMZoSgz4Kp;71dC~{G=pSMcB-e@KkHr*@h1uYPGeYp-L~0ikH7!^`|-clJseblx?m3O~bz~Sk69}h*^P#Ui zoY!PFvU+pp`#eS;YZ63U~9OksU+s}4TNuI9Q3>9O!VLW zum1s7HOJ?ks;pH+H+T`oSj{`MBWLa&~ zY@-kwpjE)~f^ah3Q(qL2lqOocw=Ds`&1;-xt@fL(%pKU9N8#Ct+Z7$P#0AZ;VzAA) z@Osa9u)8)M44#9e4%&KfCh!p~66qEih0|^k7=D%b8q$+=oS@ zbQm^w%+;}Ax5mL^1jkqF4D}ahP~$W7DWf!^Vf{?E17;qi`RQW zkxy4A3vC&I*=>{^CorO^E$;o~^Ou-%9NFCNkaS}DTRLH zgo%)rgRKuUR-H=J zF4yXH31>>D96`VO%)V1^CguLU-rVbNo>O}F_3`n`X$$S^a{^O;f1V|bt8(rZdE1o< zpH?e_*X-=d1M;ps_BjI2Oqjfyz1`qUMbZ;b=0b8otr-(|_Sb)Yy*}XQ=clIgxdm(2 z*iyMT^!WIAef#t4>-Dw%_(lG>Z9dB~)Ypwv-tu&)8tD59Ys{14wv+Y;T;qTii{$ zu+rim&dfEAZ|@Gg6xp=06T=Lti!NFpnb)0a&*$qZO!|_=MBS~cE#(x5&E&gShtOTd z@VA>@rg+Z+vnhP$mP7pe7Kt|Bcvo6(N@Tj&7+N9Wi;?P0SsO?{Fng4cfHqT5cD{!k z5IHa#u;kI)HB~sggTTEQ@Zkg&_xd^sLY+l0RRi^OzLGVHG{?igqB#W2b!EB$*OcFyqPlE0S(g>)8< zeP(c&h%WU~9LklnZ3{u;f~;aUc`zr%?_zrFYLRQpImn!*bCf?n$m215!x!z*fc0xi z=2OMn;YI^SplL-RA0dI$!cA>hL;F0jCC1y*pLq_yZWnv;wd6{3Mr|_ z3_@zlu4BH5UVMCK=s zQed z)eV&qB5F9AHwI@5JGX|zw*6+*sQk>UK?0Q=i`>A66tuDy&l7^_1|$!Y>rCf#eB^W6 zn)`a@EK5)NVT=3m@k?AYgI*Q=aL1${bE-+mmbqHk^URc+llk#HCq1tTJ!9~57H6*w z#qZ9^ejnyV6n~i|Finy%bQ};M(clX{OdwwOsgBy^bU`L(lW=(GVo^D-kiE*4Wcn~T z5)e(X_Nqs72oiQ&;-aB7RXM0}{W|eW$AB?T0)2Z=1apNDyqg$ zclnUS^X55yGaFrA?)jX!&(R<9|IgnQ1H~bZx~E%V)g5}_XB$C`VbQi#w*mFtl*{{i z&pL?IZB@m!YAdJHtHQR^jAPfS&Zlc--rT_4r4VtYFvl@#C_GY&wJ9Ff-mpuo`xFmr zi>}p1glk0w8BrI}?8#!NlxwY5bOi2`4)aM0UTjcV+uFW0s#%eQ(Nny!Kp;5U0J?1# z^p`h?S#rI8pmW*j{W#l=KS7>o1slB1;LC5ua;YLpg%3jIM>wn>`AVwLwc-iHKITS+eonNQH*m z{sqaFH`(^IGJX(Sqi};S{pFRo&6&iQv844lTvU3nONmW#Z(zDDr-F7(IUFm|tw|rX zi_@vE``(u71Yhz2<#pe?%%wP7Mf1u?>=P*hKU5t6Bs+o;OWc%k4*rJe_>ca1EL7A9 zEU0OHAXm;G6+&?_!wKlGMa=Yw$t{wd4Pp}4jTliB?d(d7Cn^yip^F6!+F*oUv@2K> z+Fy2pyt{$N_WHYTYZqe1dK#$AzOXRSidr2f$7D!|SghVOZiym3uIHN>*OjpMPP|^- zZkFuF%yFv3$FijI6sJ~wdt2}_i7>>&iu<)+ZAefI8SZP@XZ|OrDpWRAazzykh$qjg zmw3iJ83$4Zm;K3;x+u7L}oCmhTJ3?N0PME$1Q2}`3$Y12GAb&E9cCKgld$%W2b$PW4d{O_#{Gwpz%43oF z=poI3LQ;M&5++NQU0->ZltxKDff*Sksj%u??K*;#l$Y1xb6Rem=3BJ$$zPwZrStdW z@k@r^)6i(XlV67hib&AOpRlnwUDvtQWBsm;409KdvR(O+yvamW^D<#u44pS0YAxHJ zLflYR?eJ!4c4G7TlRSSoLt)f}@I^ARGN5#J+wB;FBab)SD0>`a{q_J(Cxdk2#^Z!= z>1kGg9}!36EegrHp>v%+H3iUEB;_=`FTM8O@x%$|Ekiz8b(ncnGForfpl3 zh1a!Z-Fc&Xt?!`>-JN#@V&dCYi{4TBicy;Xfs!)XHYto5^F~)lZ^P4U;6>Pe-*c3& zR8D?JSgie*UR|or8$alu2-|gws|^e7Vv|LevDkf6#~97|PFi;fEpN-8mge&M=K0?= zF|F2&!=nyE>?#bi zImo(oK}Q!~#ITBiSTZm0K%10%0TsNbl^PnSlWMR|yrn|KNHmhQocKqTXsY<+kg+p| z1ccS!KIJouf=Q>tR~Hmowh8(@qBAsvWN7RNIS`mPK-7u0Xh(F!OQp%x_b1vpg`~W6 zE)jMC^*HsV72nw0NicbuOXGt>=rnG~hDQ�Zf)A3d1Em{A$8hs{!vrde+AhUd=cT z=U`@Jqf$0E1H>Vagu(us?^1()-{y<=!C&xOViwMq3M)BCF!QJ+7=l4SO4@+G-jcdr z+02qdINB$8V8vPAtwqry#@LT4rF|l7waiIlJa=_)HTrJvK9A}0>(5t9_Bf8GYG8aj z*VQhSp`3?@bpn>}uD8G0p2=?5qB$hf(B*v6@oV+lFJej0%INGQ9$xaIb5e$*wTL$m z#Ptvd(epGDk)t1HA63k4-3f${b9E+s6~agF#S>BlNVJ(Pz`_ZuQV74}hS_>0&&Tg< z>le`mtxZ_=mBqwX-`J!dPlZ^2^)PRq1>;M!6~wOUzBOJesJ8fYga=7+iM}d|d5QWg zWr;Kl4edoBdtHU#e`|myu`sM>_&xVQd41BWXbr6yENMcq& zQ(^pE+I8o5E$`WtG}sSxU>}$#{eE)D@J_qSoS@;yQ$&iJ=JxQ`Ghy|8&j6rs;^xXRX$iX+M$2B z7mQCwWtg=QlF{W5D@Wmb5V%%Mjx@$$y}nentFe`B4HD6^ITZIx*hxpXal2aAG?X=# zN|i3VADUngn(g_H`)qC;yP0gX;aV00St(e%=(t$h=dGs z-efK%rLJA3;pNTzPEUy4py4XxY|BTx7-FEYibv3FQXZn25&w*)%l$bugXcqol?S_ejWrK_ zzBh3m;iROp2EN(QNHjMyZ?Vr9!Lz9m>WKHGtoKLH{#W-Uy(>k#vW!$(x;S)-B2ME> zNs~>7V}RWQ%C$9Zd^XSdSeAzIQ7wtluC<$+sO5d&vlu9HJ(Hs0Vi-0q43d_!Pt#^4 z27n$>;A>Y{=;Y6m;3yC9ME&g{@5;j_GyA4>3}R#l`glk};0Uu2y|a};jljmP+Y!RW zIf!UO*Hqv`lqn^5+IhczLfew`}rDUIDYn zTgs7p5M$#Cm$QR;d#>$pyTXjG&3|cxGU%)AW=!S zM=iBR>mD;ph3Uz;$t>tTg*VE|s;?u6?C>t#AUa=JELz#3sbl_XG>3dX)$hPvyL=m4 zPENY=vv%y990LAPbNI-}awSKdCx*hkU(MJ_MXI#(!4hcB0d1@C04p+!CrnH8aax}F zv`Aj~L_j?PQY)jtzMEpr0suXhf}IO~olU~czRY@*saHo@LCb+98(RFvFhoXOAM=$P z{=IYbr6Q{2tRF>gvngr`ABI3IQ8HVrMN6_mAjiZ1pEnp5J1 zY!{Uji_E&ia4KoG0oQPmv0XvblodztI;^pMM+vw>GtrSY;W>^X^@oLz3KiA*`ykiS2S zCErweci8)UQh&G)6S;Ayu+oQtC$PXonrZGq%~j4IcO-25E}>Yty)rbJ4yJ)*h^kdu z1c|r{26pg_Tr5g*T3GN3wNeaVd5kxY!(gU5QL8mOcy)O73I}im&CS2oe0Cl05Yf$W zm$rpf;)ZgAChYdw?hix!j*nkUvbKuZfNt2(_T6=sl=Dt3zg%1n&!B&j!H>U=k4O7| z{(ilMuU215OiJcAeMt_dnz@p2NhQc+8YZCV|w8EJ0T6(IJ-4EL!>mvfTtkrj}kn+A%(J*cr-OadgQO_lK4kdKy33WEwz+%(Q zU93Esdgd|Lxl!kRKFuap=R#icH`a~2Gt`PW^x4(}Cl;XG4jt4^ikl^lztdoHCBYR4>{hTi1Azh{GP zr)(EN`w!@_f3%4e?liye-cilA@|-UpzfeDO{IS{sU#i}vvsZO}4RSE)9XPI!0J0!m zo#I%IJP8>kyVn#L#<=K^YV;q>E0{OTMB0sGmMeKbB;u69M%ICQe>B<@xnNeJ$FAll z);TX#OsS+sdI2`tB~g$YCM`0qb;5?$c%)_y6d(}#qD6n?pr~>r?RY{z8+EL6KTO;$ z*Dwk2oNpPmzK|4tk1~p(&NfkKYEBp&xwTE@MQ8zR89Macw*r}>Il(6D{{BQn|oxyr=q%2L8Xi&~@Om46F4!zhg>Kt07G`Ja7w? zg`BYM$+!M~m#m1?HM(oCbyw*WTGvcI5Ag(E@sCRIigMXq_1Lh7*DsQwExANP<#keI zM~neQ9h`_}?VO0+KDhBU_~UZmacR^`!A<%$Pc_g}B~>57Fq|AMbNiTxkmQ+icyi|I zH%pt~pv!N2^^m_lJ|s|n;wz8i!Ks8MG+a=Mjbc(Uz&*#Pw-UtT!0`g4Jo*-LWVJ<@ zS-xjciAh_S34ZA8f4PF>{Otw@K`FVwTAHtu9DUd`oQTk~i6gtNl~}^4Hi#}J9C!|T z@A@3jN9}FrvHl1$F51da9iEk0Or9v30MXC@C*#}q)WLd1A*07a?R`euW18kvI6?M= zRj&bSPz<|`Chrh>Q&i^JX~$5{=^KkXWjnuSBRyNgCRykDo#H;KK9l21Z=8)@n8rV> zv4X30K*0a%)fR!l=WD8|HpHDS@nh%;f2DeCyiei?9AY<;kV|-Ka zEu9{v56~*A*<=?Xg(-zq^Is3Mm74Fmx#UzDMfVVzO9=yc=0xB zOk3ZY^2_*oSl3>tm1#}QVnyFkydXbfO77lO?ulh)b}gxycVqf~HKKdFdj>&c(H9^B z-T}4eS}edM&x>P*ou1xZ3T~i!ypRufVOp)y#|lvVuXKda@7DtYZuI4?&~7Mp$#k=z zf_`nZXesW}QHm3{PP+jv-C$nJt`q!GW87Jtz~h&~Bzhc@an;0ALEVp;4EA=G6UKH2xks|Clwz z^fnma;nwXfS|i&5u_(GlBU1Sxq-yx$Q{UFDjNPj`m^8pYi(;aeCMRw{KMIAVY^z)- z(LNY9K}io=23N>1MpGulfE4P5uIa2ohVq>}lHo|uUwDRW&L45y^mqu%?32m_8YL(R zO|0Hx_oTTM8*#o(Mhi%wg8BM%qhf>Md-P#j7DTJoSdJ8NqAuE>%sA+-!lZ=OhAEoW z)WnL>;VEHcF8u6)0UjqQZ+;8_|C%%IhIM*DP6HqNsN=2Iu^a{a*4KZZ=j&0FN&Mq+ zetsIBbB?dCPeU@(hm%e4%Z9c+K0aiGJ5-~k*KSsI<5@@QWEKy=z^^qj;ZR^-H*;Ft zi6KP~+p(@;dDQ+sNA%J}=2jxt=a|U^G0Nlcr5IvuQ(--BSH3nWa2fTsA1x=ef^%6(F0eIl6t&c70E{t{`9kU4 zER9Hv07%PA$~a@X9(I`#N*y(ianAH*&huwH?f>(C{H>6+h9ke~gE2V7bHk}PF=w4q z*yq_FhpU40{R&Oxc{(n4C@Q4^T$xJm%NPC1$Yt?8q-cfy2aTR{=tWpzj4zrli_hYC zZnhuuR3izuSf-WQpV$-)i1=Zvbq&|Tsb_AvVf5~&V?@fpwz=j#~_ zCrRX*1(BWSS64aWy1^EWM9jPl;WOVQ2ENB|qHniIB3uI(-BbpMw}$Cz)K_!JivT0i zep$wlM~=MZ%zL$h>Q_mgA*i6S8bf!=kE0nXJ_FOs^Kzmx<&gZMe)yrX4`Otwz@;TM4GNqU|uKo4vuh&=p{`>P( zm)XX`U9|j9oy(X(rP3O0Lr6i|x>XuHZG_Lrp<1~ZzHORbC0t~qEd)L&wDiQIm?dPU zYgd4DGzgQhn9Z3Mc=#=_V3nyEmK|V3=7i?KLW{pz&}QhC4JM?XbKp^3%Gg{?8>C2G znU}$jz!|xwX3Q<|aTwh9c(fV0DP~vG1L$}ib2CR#YKSq=+t!rZv2`exs3ztz?F9mx zIesS$WOYv%{E6E-~|In^aL6ZP_LOZ$LqY1^ZGI4UZ>H#JrR%N7W zqxKdH246VP2t;4SFswvAIqVAff4^`jIXDuHMf36{Ia%XCy2pcYM;0Sc^1sJC9Kc*YAEav&2sT6oUSWk_Ern1j? z490MNqqE1EK==w|TNrB+MKs^yKT_@5!W$x|c*7zO`a;!jkVp>s$XP1u>s$HCsxa&> z((uKYKgbt;T5txhb9{FV)r+aoQ`5+>d;XIhj>G9HQ{hi5*uG&?VnGGq zY8qu<%(1qkgl|k39+aw2j%YZJAY{nL%Q-E1Q=ntcF-+jFG;p2~uj@Fxaj%;g<(hjx zFLL}A&^Kc98F!_gySoYlP;=$VFLe{P#YOoI-Ig_3-16~iq-Oz1tQx{hx#Vo4S{>id zT{q~--@#7^;)3QGqB4>SJB2X-s)R&S#gL1GH00o!H(%PzDiV=3U(TIkQXX0rdsJRe z!K)|yQtQG+Y@$nS*3l;e7cOmi{YlRwnJeuFSf({k4y0DBj=o)MotX*#JeM6diBWz~ z5`pEi7{uh@T%O_XU;nMg?42^_?WFQ2c0!Mjhjs5zpN)05O?m6$!gsZ~&9+(ox*NHL zZ2sej+J@bi0f|d`0%L>H3bf|yQKJYqUUjxeg43sA2pg1km>@HE`O=e4V;s#9Y&mbFr3b{uEddfI{oCqKwl>@Z8Bk zd_u>>$ZMdz54#J0r<;-F8y0DIu}cN7rsNI);mKA%#MX0mQ|@MPJD;pXhN~)%mI9jh zr(A&B$+f86{JNlfl^+{ooBW^u{AaQT{^zIGCeqdS z5+vkvgVBlr1>(+Vd0sAvnOE#?C;kQxY`oEu@-{c37+P{2dp7<-aN!~bxsX?v3vVYJ zuM|;&^F45dEUy;^QR>8FI2cCkSL=aY&g>6Rl=gj3nDr)S`pcejk{%LSDC-<=8u2cn*9tsT#2v$h%!n)d1pi`Wrorj4_m<<`Ox+0@qkg-h+K_#vb%wn zzM9Zrzqf~CuM+oVkf)N9pdE1xtUyu$3;{e@xi#YZvWBjz+=U}V8x4@v!_pHTilETY z;oIRXp5YKH2Jd9lAo)RrL`C^|3q$rHc(oiSpoYRIo~W8C_> zncy`(7%?FDG6BrhuAaO9HUB)^cU2Vv!@U#-Xn8X8*r{AH#cu<<5}BBeh32MrWzHKy z{8WonBr8Of<%aLeL&>dnInE99q5Ft9rR23(mE0hl9jKwzGLB<}wzN_n@y$!gUyetd*QC<^Tqp#Akq53)sg(wz>mP^tL+5(xbQa~)_{9YDWDr^(r&Z& zberP)loMt7bi~88f7sz zceQF8vN5=~!-9`VS)Btnha5d0?ZSh~`3h55|E_+dR0I! zH1vz25Pv_r&@>fI5!QiWnNMtZ5kOshzQC40<4sd`hv4udQ<9YOPBB2jaA`Fx(&c?MnUp7P{VLvigJ zXoJ76W0(8N(aHj4nRlvb@XgDi91Kmt?mT8aQ>Q<*c#3m&3OmuJ>4jtDkja%#ZfVoj z{g`u}!5AoAoL1HphvH8eN+lM^m(1n6MS$S*`DxQ9ZQZ*_LIudp8$B#`Qq`);l|4dy z81kF$MwC3PBvmN9GQFVrrE(taLX0R>YFuw8^IOoRT`gy_n2ShmCg=yL<}>|_4aK|& z@0^*AOFlgjE^T{84?v!BMyE0+v^fRWgQ|BHdP|%4hD*9eYrJzBr{ey?Xbp3jq|hbW zGilvn>+eN213z=1lH8wDzHxN<2W-x*urXC2m)38WxA<@VliiOG{s+9kCFKqEYF6AXFauWfyw*U3XuTA_Z2g{r8GG?(w9 zl30?$_k~@95Dbq|Cm1EtI{Pto^4gs8N>!%I9Ev~x{yZcxKzN2j-@LdR4^IdmNX^2t zDo!2deWO5xTLbQg(JXe?_U@>(-g$2n@(2kH_q%q1=TbyOT`h->^hqQ^8Jau1R)jP|N@ zU^))wGKIcEh=`{$D}VjfzrF_Q7M$#FO7U(gf@nbpwo_no6B1K2q@l%kig%S6HddP3 zatIoAJbJ70F>cv&61taDTR=#}l&R4rQ0ro&vM+j*kGd9wTx#~VOU1&|1WkH?JLCX8 zK*GOy>>=CMCWtMMemKGP_+jGWHN?fW*<2AX;ik5vOKr#E0Dz$ik6owt`t#T4*PLxn z*DCdl>8C8(@fsyRVUwwD&<-{`^0 zkTLH=CbKb)JnnDRAHai4J#$F{TcJ5h%Mq@6PTDwSg(U^Rn~cG{g_VM%RKHay#;8Xg zBRaXXYnK=dM*3qe-7rw|0C(Cot_+kAD)8AA9$Y45%;{Q%Vv_()Jt?i3-@EH^D7N`Z zwWE>5`{2mb`96U~Y0YL#Y{i|ruw3{03fw@h4+ZVpVVey}BP&WlfPb(vN>~?Enrkc* zR|4R&i~M2vxo-%e*;9wM#b5rKBL%!cS)bB|H#PN7fq|P-cjKL6xX>0A3uNItDQcCY z8=lDo#YfqsaW>0~OHIxjjVoEO`!v6S=iFsyW$&P^+}6E{)Y3i3*-)^miDXM!yguLke156--E@6u zBlSVKo!P^jSD2}_)hE;mI54H;7z$i+8eVnwqpLt@wn-+T8IsTTuoyufc}91xku`=? z_Soqad^YOpAw;|GOU|ew|H@>P-`F(IA6Jl+$wQ3vR@kcUlG%;aLc9(l2HRJwoP1T3O{Ui;yU8jRr>lm$9c9?pR#m+ z{XD-6y424-wbsLPlS*PoP+VJ;5a=d!K2IF_q^^KB1&{k9h;x8w2~T~mR9Z^#Tpfk* zv!gFptJ*(!0=y6*+Ec!MF#0!Vo$hM0$O{M&W7hzAO?s64_qji$DKt%&@^i}b=-8mr z-0nYe_56rYy4(9^`d?Aa6(P8=&<)UIOBysPm#UH`0o99@$D_NWY~&h~ukb!0?RKWj zTjma_7UlthUbco7f}XO+#)(`_9=2$rb%Wx{sE)n3yYl}>Zg-LSdEpC)e3bd8KosIE z>rbqXP>d#-<%Ri%d&6~*MKS3Q4S3j2Y^bU~nc@E);zcXw=}!fqpQ}klx_HdH*}(|0 zgp~(5tS$Xmm>cu*`hS#u#@}rrbx*U)p2=O?P_RF11+Q&6V5NlP4_}|uJYm}$7RjyA z+jX3IVNfiQNI=Gqyybv4fRmg$g_|b-azVKU{#e4q+%^tR5LcH6bHSrhA_Rem-G1_h ziG5-25KQjfO$%guXvh&JrhK{uVaGF3b%2_h7&6XIW^__$O+n-DX_wG=M4U9*z<|~A zO5gCp#?3J~DFu9q@2(A;)zIk1+z7n&7~4nUR`~{=1HbDB+d`A3AFTdJ+4z*jPJ>k>0U~aY| z>k09euyJ7$sk%2C?0w(qwR#Rp6)}H29%5#7>NlFcmS~KU)#TyR3q@8pf?gjy)}91o zSn|RlXWP6ryCS$w9;OQ&E|2plpd~2!5Z72st|0d`=gVY<`Hx1FylL7y(TbI>bEUN+ zNL`~wnaxE$h(@w)n6Bd(Fq>&=|C1OCvD4tG!dA|Tu=Zn}Hc zXx_c&l?ci=l^9=sHmHO3Q0Jr>4>yuD!Hi(Jb3jfigIANId7;Lk7NcaMnmG;?*Bo7& zzyA8`_0_M>PZld9v2^Y+Ebvm*r4E=TF#Mz#99%qv!fj3>3tXavGDV|CIbY61b?Y_9 z*Ge7{jFpx{{FAO z?*^%|Zh^wqk}VHMRWCJX$#8BZyO9fbW2pcZ#uIu+q`gApn?1IWyx2zORA5M$3pOx4 z9Oebs&^&orq7c2YQ2bzR;v)z=M;ZFF?=s96M_ej6h>B9WDUXzqEA7QB$K+Da7o1Kl z45p-cymJlIifL?kentym*~kfEjVss(7^2f1{8xmp3?2%l@)2T(KWR*u2|=LF*~{&z zT&tCyi>9glcL`~4o|Fdx^+-RkYFYpg0LqXB>jv89=0&~Xh4!j_fEeA%NX(}~@ZZiV zO0%ZR2cc?r&zGL9@RgHaYRh5hB&-sDX(8v{3%1avJ1bqC(?YAjjNI;|acbSu;7!)& ziEmN|iZfa~kR8nZ3vJ|WMMFsiLD2jiC|e>uU`qi@;1-%s^>880Y$q}+m+7}?mW_59 zrCsUdgQ?K-cA~aTg0-a=^H7MqyB3;ab}9}bB<#JjhgV)2vUh^qShdP@a)Nmvz5e#s ztB;k>eyHiTPT6G<>jbOny#M?# zFQ8G|Cgrp%ItM#c;ZzW6ahH1sJf+Lzjetr`R5PB+_-l>cf?l1%3oQ)KXr{7j=-^x% zr%;Pq2K%8qPqkqic<)rg4WMg}-HNz4zAr(vH>}RP{xq9~-1B)eh`RvB zwQ@?@OkGqgzhX2H`uHnt7nTp-@;a&QQRe$uf-J1rOO@SS|J zQ-{9CF~FxjLC%uHmIj9xK_C@Fm?Sl}%A#D1lmHk6*Jxq&Gq=SZ>LA?F!MX_fBv*vGl{xJS0lXCf{*+C6_v5So4`nNT@#sWWl;#tjQ^g;T+#=itB0FRzaSHdIXCS`+!s` zR$>=#(Cl)4=I&a3>f4YFsK+BP4lET&;vxU>vD{7#BWmBh>P%Gg&bS7`bqJ2rY}19Lu(O{EnR)0ldA-t z^jBtSLl-u6FDJlV>|bTDsLBNG`A?o=ES4{+3jnHUj%3Iz+-FXnw4(T2TsfKv%(|!n#r=1j|N`f;$FFj-U4ffd51&|;M zYJ{zv?Kbk^PMKP{xvd>MC-^N!B1%9ibu-EoV}iV6jA&Q;!K5jBifJhTfp8;I(jgpU zo-wWcA#acko*fdh+z(N-j3mvK&pv;_^Z8}dbcx$>Slf~kpFM)(~ zRY~l0<2x?tzt{dj^Fxc3UFN=KCf-=;_N&Yu?BwaraVyNbt7ib(B&5rU@>JJlK}l>0 zABqs4SN9sD%!pc#3eUnf|L$>!>L$1P1aI+#t08dO8Ke;rWsk90%br-)WIWZMGi>Jd zmmkSc)0_`JO4;Evkl2<_K7+cI7NheNKCB)&LD0x);lg1nyf;XuB_lL1bxTzrL%#7O zq&yy9pMT3DIKmZJ$MKzt^L-Fvy@y_vpcIJeKs||L@PBK@RaKOUtZN50Xjh zrP=@W@%3yXV;8TpX}Bn5_9)du8aZo&&JnoG?!lz&UX@8`D>XC^4SWmS!MSBoLKq)} zyyJ2?l%SbR+O|=3X{s zvK+Rl$>|cq0>#k$`m5BD@Z_OX($^nmjHHGoPLmJ&5l8dn=Eyqq9Kus?=y`5{5rXX8 zAfJ1N>4X8KwN#$;lfoS2Ca~id?Z|Rg@t$^r zP^}Vh3dZd9v~H{A`Y!LLe0nI|NiOfa0e_#I{B>I%?r|0uDjE?|zL1&8r$%#*0)~aC zLcXoE(35tk5vGxL*frw3Kzix^91B?Y1ijI(7twqtFwzVv-9$+qAHR-yNSvxlo$i|B zY>S>&v7AT#3YAtu`P9$uI0ukO{qnJSxRtoily2S*EcIXLuKg*FniG4YBG%xa%< z!7eU!oj!}vw5oi3oE&jf7gV#H-H*a>BRAvf*fPIO8}$l<5Zd{}<{^&edok{N6gk9k1rykFk% zA#8V1OrHWmvqa4u^4Na8minu}njg~*OFhRqJtpSCe%cxnSass3l2mD{G-($;BC4!! z{lTukan3K~1bj-5N;q)D2HC4{rHUPLK#b3?v1Iy1%HHbd*_^R*Y1m3yeK8ZwI3`*u zOh_8w4b^RDta{J-p!dYcZGTlq=_QxAII6tsa|nS7;cw_oXHI_&Zfoe@PR!szqn;Ak z_-b=8wS{dXyJ`^k#-x$jYA_0edB=Cv{C^odWXt~ zEb7cEmY7+;A?NZTADQq1-Uuf3v-T9G46LIDU1HiRvp9wK za?+2MKAHm}e3vl~tutp2<1_C7*0jbWhRDK)ds6Fm9po60%IQ!glOpoCPpX*TAs8wa zk#vcf=fz$FOCZKgVmBuHQBU|#N0_;AJ0&-InNcTWFoUp+dF;TB+ICZB`*>I&cHI)x z)B?%PiF90ax}dju4Eu zM;1fT15n{04?fMpWY09Dps*kf&xEsYF(^wOhnF>og=raay`LZGfm-5C>wb=69Y`j_ zS9o2EL*I5J`{|(Z)Q&6c4eUT-nctkv$N6vU?Larc=k!Dq*>@5 z<`}BqJkgRk5*?8^&9JlST{T8CrT3yfF=5rnx5jtB$!kerGSf~VzGE110Ah}Ww4-G# zxIVB7i41`>(>z@oZn?6y)n(QqOY=p2--Zwq1Z%ryCd?XX(Oi}S3?Y;fb)Sp7t@z5O*HnpS6`#| zQ!ygxlcif$9dZZp3!2Mfy25+78+B!yg`d0lM$AaWZ_lYHSj#@}lhZARyKIiHJv=0R zw0zF6K6e_B3XHTc5{a{RO?Fq9Vw2}}m!lf#mtdii5xiP$h348O+aidqsMn1Fz~;PTL&s+*FX@>I7%>Qy}cD z_+}mK(mO8MrT%ULT+u4aR;D7Z7iwzYOiR|6x|=TeFBAWJ>wMo3vKgZnIWiZC^3`oz zf;xsAP*g5eCI_XI!mn)(Q`6j>S1qsC5>-rOJM*ZISw^x{(n-eChx0>v9W4r|64Jw4 z=H8`JcYCYX1vp~B*2487IW|BT<&5}YCoW|yIt>39{9iV^&$py3YCA;x&A_tW1wFKl zWXs?~fbGbccu<*J5aV8_TvklKXFVQXH{e&Nr!W2ldg^R`*SHiQ}a!yx`X#z=>>YhHM8T%fxc*PpUJ9PF}&#$LZUeedw z2@vxVl*c-JX2X@y9{0%#CHd7hRShh08N1Gl(uRJTL%dsLurh?g=hhtuA$=$WzHTFGH9z z!h?;rm;f}G%m>B?2f!Y|fc%Xv$C;=a!w?Fx_Rwum?$@J z#pTAtKRBEyi0KgvNvkEPGf=QEuMSvOT$K4P^^}(o3gXJR=`H-wi0I#9k6y~b5(anK zXBL$266l&#&$c7?Wj5!f8(i8CMklK6g4~8drNF+tF=6X)^4T$H_Rgdv4U#z1B{;1( z0~-WCCCDaV&3nGk;vZv&3JOkSfD0xb%#hS{q~F zGS|Bv?% zpJ5*?*pPvPZ5XYc<0<9QirHfht+E;oPs5E1m_0@6bYs(YuX1e9swR*Ptsw6u|ffe z;&=H{RS5tqw$gfJwf0^@VuE2kyNe;jlWFV9rvO%uzZPb?wG^%UeN zk5kxpHUZ@Zd8D&n(^_9Bc_(BNGScucvZcn9CSs+BtCH1UcUCu;Nze9)hySEj-UDNC zmuj2tA!#GqGhZG?T3_)GlP9+LB_cVmXSx5H2@_AvYjAF`6NB|;nZK^E zN1J`h&fBDQL(&?TBh1H8Jzb`4kKmsr0#8HG4Vb0^n-4Yc3OZUUv|!vrN|phS$46Z}!#wCn6N?p`%VftKFOzYh0%)hTP+n zVvo|3>q=f2mg%I%5XRSaS9aTws3z#Njivc|XvT5su8_De;azie*LKcG5MK1!9y5jR zYp#DGqe_$!;aCAWqYbUO`r<^Q+PWZqcee3!cZ)}4Sc7mYuBipiSj%4W0ch7TYyo#O zMh$w5JE?TYBBu??VdLkvVc$1M>dR@zY2$r0kT_2#^1)M><_*c3`O)^kDMecYnpvYV z7$LI(v8dXdS;7>vZE)H2^}4(nmaOMOQ0__k5Bg$$3A z;e;(Mv+Q5*c3gH@LBR2%V>vmdcsU3+j+RQ!4oPM+_r)><>V$gsAZB#M(y0|^LWh5@ zsh!#3$n1ZbB}|!6r(Yiu)~Ckpf*VdYqeo##t1~~+Q^kr>&C5j_09l9e)c2GXz8Tzp zZ>YW*P*MFGhFuT*q#GYS`i{_hjNU06buzAEoCiTKN`Ayzu%>j^t?QG#U_VyR=`|S{ zF-r{|x4ACPWslZDPQezl8)R*hh7jaFK7PG!&nf!yo6T-^ww9bB5`|e&KcA71k)0IQ zdos!4 zCN#IWm|1~j%+)KB{TOwRz;D;npD;Ox+QpqQ#iB0gRAdz`*{+18c2kfRCeh&U7^P*_ zfMzUo+l=wg36SL^puGh+y={uVX%z9yK#+}~i@DLo5I|e-GjxTQXz?difzW8~%hvBN z0qw9X`XOSPmZ$q~@bCad|6Q<$A&UPP9mJ-T+Bb74S}QLQDDx#XZa>NgvE8-I4Ic#L zv0QOix^F?7`ya-*CuKV~>9VW8T}ErRb#qIF-u67vOJT?Q;G!-LoqW72C1)YXAJh{4 zdoc47-J*Y%*1*)#jCby!!ZoXm!85#aPFGq;_=k)34>+r!y?40`sgqa84-^3q`ME0mdy-Z893_FCXH_3ma&0B z$y5eRhEw%)_~SPQ-B_jDT}v@ZLK#QLu z&odYp-dfl!XsF$DJiy#qF%-%HmkI?(nldWy%cN<=iH_2tVb5KzdvLLoGQA27b!K5N zsqOiy?jzcS&GbKE$*O&#dU8arhA!i@#u*HR5yZ#AMVP7 z#MflXxu=BC?7l>9=VWwKJ^1#=yqs0drztt4$p$zzFvgp~SsN|AfQ`C?eZw8H-hjSw z(1VU?GjkjtZH^3{G;OgyUhm`i{F;x3&XWfNS2MC63R%elEepO2WAE3Mu}LJ+;=aO( zFz0};q?xat!-Xo`yNf@VYfl0J(P8IO9ERMMol6oERr$xJDWIDUXT@vQ9)y1fVY=8^j!~ddE>gzzm|rGp?qK6e{K*Fm$>^aPRP9JUhZ2* zYQ=@$ka%wYRzC;`!0Ph=PBuBf;x)n=4hU?+bEZbuY~HO$Ghb!cpipGyEojbdL+;?# zF3wMFdAE#N2)8XTg%ZI{o}d=wx)KQ)XXYVCYPBtX$U5ihVqo?2k>_TY&JjwadA zNSl@=8xk#xv*#20*V4ma*5Z|D&5JWw%NuBKAPsiYKC#3uJBcwG)JA0?x5caXG9kga z0f<|I7-rI!E4hf>`Ov8p^J<^UT8?sMZ2*T362b^)@G9S@JI*Ey&MEs?l&yx}?xtPA zyiGrw>#hWkqNYQYnIJhc@QiPj`oro2t+utqS6ZN3S{64g@U^!|%Xwx#3Q%>^B_3ED zaKho6N|9muq zZS!TigP~9><%jN1?!t+8vjcOHnKV(cXx6WGhq+o<$TaJV1LU=rI>C-y2^E7GvO^Ph zq!J62#4XN5TOBGdK2J6syy%q8c63;T+I(+fu;~_OkU{M{Qw@d~W-|ylL<5_$;B~eP zTx<+nhrz|(nO)L5v0|ANT!P?P9GG3@;?gHuKVf3lGA53Ljz+55Y@(caeZZ$}2)^Bv z0W-y5-d-uO^xolLhh(lQUg`m%@)d!JNSwD$Hn5$zShpunnJ(KdXP>_W67I|9JAPld zbQZJwLeK)cIq1juX2!zHrg$mNz3Eo6OK*KRqn%n|W}EG^O(tZ($%U)Jh%|F)SgXu( z0W?QwY&Kk``8_?lw&6x(!@zAaNi%o**>m>xHcR+;Os@AsXeQ4gvTbT2!c>3JY2suN z1A_D~rE0zt#rZuxKIhiX5V6=5)bF}^{hpr5MPmuvlRe}X^)O=^S94eL7@}XTIU&j8 zr^373ts?cjCaPITWpA5mYwMx(h9bCqJ3D3Fjq&9V;I;kB?naSxff6|8+IPQEXVCI! z$dJocN)5Pc*LNP+C4;x#7CpVNUe{urZ$E6NVgGAh8x%9vj-@$ddsKI8*a-;73XJa! z%T?T%%vyO=uH0VIxrI!be?WcpJ&j89=Hd&@O{$OW&2VM0GN6WY;%j0gPz8b~*>$%W zBbiO1RwJd+^(CGP_5V6-Wp|qwmRVmQ6b2eqXi5CgPwv9PU#S7Y*luAUQWPpeE2C~T zgBzC8(PkXfELDqyB-tM(ldf)CyP^)G)#!(v-%lnT?Ay?MU`x^qm! zv&nJO(#-rH2iotZBB$UrX`3e8%2Un#3l|eCb$6p=-Zs1mTTuq+apAQ2^QKab9+VQg z7f)XScO?-fI-Fpg_Yz+9g(9WQAH+ST=w_~8S9KA zT9Er)918}!&}DOO6hq-K5`dhv!tU29szI=y1GS@hxu0`^<~H<-{+#X-rXSU34I}ei z4TB-E3_NkV2{C=q98ys^`1z+U3*-6s>tStEie_#REy5rBV6LE?hW_{k(3C2=0AX+m8hcN1K;(VCSCuk)I-)T{DdH%^VpMUSF25Fbb z6y)iiMc!SgJQ#EY^O`*EoEhGU&-pnY9~!JW&zFchS~xFb6oaVN z7P~&1v0C24VfIGBjzB0RM#gssQ`XUFjwAoaf4{Me`uF~!tFqALS9ibBX;5YES(~{{udl2sjsdLWAT!HKQUvd3Lk(q#c4m&@rxLf;?p8g+@MeS?hH=gsH^-%3B1?6A zRRHJnQi}Bm)6vaHd@Y=K(=9J6QowZ55r_2hs3}Hdk8w-IX%OGkMt`O%6McPs&YS-@ zlm=&d9;5t))RR3qD$PZPXOfy?Sek(@b8d6RaQhjJz5(KjRe$~CNEw)Ur20^#Fw^SUf$eQe9lIjlQ$+GwjGF8t<{EPyQrb@ zYqZ>bE{qg7RRMPfEe08K9b-_K`TQf2He4Bq%@vkM6bKsKAidOnI- zkx3ukWi33VgPnN^n%S*fJ#0VJno}<7>A`yGQYgRfR9;Hh1ABkMphfQ5Gc%;3NYUIb zG@CUyK`0{ZXa*o)lbrdSWWQH4Hcd)LBu%Me6*3rSH*$Q;$)b%*u)L523wZ=6&|TGJ zmJNvwpgbP)^&b;Enz3)P3r$Vgb9&4?K0iO7&(l|(`U%^N$uiI*!<`AIl2OQ;Orl;E zsw?w*7(=Pxy|>8j7{gtD%EwAZ&tOQS8hN#^++#P}G_L_2xjA9@wVbUT?^<@a$;0`4 zJRSyxwc&8w`MRRku&LXS^M1R{=47NjIjFipIU|_Y08+X(KiaI$t9|GiV7n4+JNatI>(0*5Ki2roLo?%*;qCBp3srj8UNw$;JpxRalgd>L z`&Hn+RX=x+A@J%fPU&H}h1!sNYbvu~C6dV52U(gYmYSNSdA@qwEHv99eAU2D_P}8q zJ>ND4woBQJ4;^k6Y_Q~iGz{@;H{1+pXI2CG$W1I|G`%s5E)KoLC5~4d90AqE66)4t zWp80h#VHnB56DA~zLz|=6F56I-Oj+O5TLHz(VJ#((`s`ffp;#8)JigVwVijK=OwzC zzNw$RvXaN-)Kf{vE8!%}z}-bxx2hrG04%Z^(RwUWtc_d7P#oH<)_?&!G)ro9Yl|G3 zc^7kqYQc1KsGelJy#~At`WyTVU-)zCyQb$|A4bQL7^OqQa?UAZkoZBVlSv~nZT52jX%A1xSawHubiLz9P z4_zjz>K8D)+nfud_ZCB8cPe)qc1-^CGW_0$#Tx zm`)%!ubDaW)E!z_glC7+Al-a~!HF7Soq7-8;i@w-C#e#gjMTkE=ExkD3XBAVW{Hbb z$3+Y2?ppM|%CB=k+(|eE=jjsY%$4jOdEyaI4267l_c&D!`PQqg@8T$`AK>HT5Nf-% z(_#tKjvyXdf28qWvm^}j%)4VAk#pjeAoKrH_by6~<4Be$9HgRp{{J)AY|ZYj&1_eQ zgkQqn4uFy(E311Z+oq-}GD0DdAmD!dJbn!N6!!uus=8F`d;A<3g|+`-##2;y2vIbc zo{P7kEzz(GBQk=yW2PFoh%N*JIt@ib4U%0)o@7Vw+TOLP*0;ta!3 zw4{Q+gKJGDieQ2AV4OSd9gNIcI%D+U_ELOqSwqj71fuK)gAL4qK)z{qTw{}v!@gR3 z@w+k9Dj^eSWs>F@NWUQR+%lrPvw{ROV+j?*`xt)D%$z)FSg4sXYe-%&QgbeiaRTMa zS2q8wT+(rtyxum+38aPHH%w}kdlga`7RXu##>HD?MN!mF7m*+Bh}sBVwJ0ky>;)ag*u|1snM&U>;JeVm3Q0MK=y{EK+W+fA4*29i%=QE2 zFAzynQX_~kCXgfHEeS7=cuG1@AU;=Uf$!jvIEgIxTO^WLCJR$69P?BB7l(Pmhx{gM zs2u;2?nOZ;!Z9twEGQko5~~c^7uDaivD?H?LDh{29a^pj@~S5sB*v6V5<*Rpv<5X~ zfZMTzEqt>&=tWr5((Qc)BR-vRCucfgPLG4>!-lvKABUE*x*%Cf{M!fwJ12L=sfNJD z!%j3bfw-!);{B7xS0kT{Hf!htga%Nt0Yha%yBt3~{&3%ZH)YMbB`32D{I}!@$Gbd+ zs*{~>IlrJ!ncDYvU%SSE)aC@oSZ}jCYkF#;ilX#)$$+bc+E*1sC1-?0-diww=Ftl9 zJgbn=BT0;)Bv-68Wq-U9Df2B*aB40W8)F}Mwt-tDQEjHOYHImQUn5tZDw7-u_W+1r!6KRBc_IE?JG7N(%BaZioTGL>Tdl#~(0ep21 zSB(|Ha<6z=))?R`6V?!n+WsX!Xr*MJV$DbO1U9Lp4z0gdKlga=&Tu#!Fg*I8x9-X}d zy`7S|?>jBaPWjn5S1R~$795v5@i}SUB{bdLv*{rPy5BU~7zt7D^JaOIaqyGn#;nlb#w&#A{QJ5-TTb5RB` z_$f()N*hnGtI~tCRK{RxXrvqHLt0fZaVT-sgfP!MC)cCFbRMS%C}zjATH7`)!>z5G zjTEk+odnMWR>D7*D}_G-hKHZ^TwXBSNXgZckTx`357GP-ejOS5K|>f|Z!GHID6KW* z@nCui7Ue=?3KQE>i*xtH-*9Ug&{C#Gmqw>0&(zs9cmKC+k%Bo`Hy^6?6{Q4AvM?pJ zT%c}udKl3RgPuNSQe}UFkW(2G!HW*`!0Tr&N!yVr<=L`p!fr_FaxyPcyTzQR7+EKC z{T_V4_00stE|?IgvJOjaE<>_hlO*Tu05uV#ggLP7o$q8e#3oIP9(02@b{wI)g-|br zqG7p@cgBrO4`#wHss;@q@5m$#JFa)~=w|d3>`wpddS;5?;|%dCdapjufqe@i^E`KR zzXZL0INPz0!^3Dfwa%Q?5l>Md9$ahqGfZ*GSyhLbW)w}2v?x&UQw%n{jrp9~x?Hnv zf$&FtP-)qpr%~A4`vyE(pon*(s{|A4Fu=wG`oS}5AwD1~O%!p=9Ui?vsOWD&J32mj zMENTgwvnx03?p<0CsoX&S-yc#N+7nM`J~tVTP8PsJlI-u{~_UnlZO*<(xx#^q-6joW^mXvdp<-73b&KV zVDwF=0}4b(1&rbw+B~JkYCcYPS{ZvVNnTA0)`SuY(wYW^az%kke#aF~Hu&|wo7s3- z?`evO!SRaTkpuPa8^%SukYSE@PMVWdcn*U9yZ&gMj@d}k0YMhjCM*n$hG>Tw+fR!o z#HR_rZioSk2)>0TDoK3Bdo+eO<3!87y$)#U7Sjoic<17|yq?iL+c)rh!ph^$M^DH+ znE5sXOKz!p!n?PWXzHF)R5G&(rvT1*wEe0}G#EZ8t%#;iDhbP`7RR;xPMvoH;rUkG zgrc7!@K!7fIhd;C%vgCRa=DVlsGK|cR}pXzPZ^8AAtIwgtz$j3%c}M9IB65>P%k5M zBu9D@0|-J4{rv)TEW-+I2+E+pu_0t7o1OxVPY4?iiTp7h|2O!Gc9TUTu*^lf@w8pJ zjK$LSbViIY*c0Jj<2?L`@_{K%WyN^osbCerfQ=Z^l%q1qjRV%3z!)u#p7@hh5n!-Z z>7|_%%+arM&d!t<1iVX~bsw7kvrYJERpukjZ`1uo8PJvdrlKo&(|MjZ)U4Q1bks)i zvR|+YsaIbpjzkFla523~_o*7DF$r8~=|9o!O)3OVb}VI!XD9~?Q#0R#l=}u(gGxJC z6~jjHJ<6U0H6UROnkcIAyc$hp811}613^PKDkcJ$(=}VYY z@F#1|4SiFQKdi_Aow}@6!XP^N{_m|FR`Ns*Dpz^PZF85R9cz$NcepV~^KHKbke;xT zQWAdFo6JDQy(b4iT7dBG7vciA{AG&Z&qR6$ z-V^reMo*#Xr&D=mY9VK8`mmcstp&ygPJUkya`Z zXkp`M>)E}uI+^!rG=&Q4WFltw@?-601A+CKVyeiTS`h(2w33~jRzzQ}1mh|`&QfCW z`Oo!97f?|pDt3^eXK>5lHt1W-seEX`2F+_Iug7jsERISIQb?LG(wo0Fo$@@-jYcll ze$?$7y4tuW5S-n-kxWuIDgHF@E;|cgnzLF@C~F|ufa_C-P|Au?3a@MeJObh!E9O0w z`ko?!T9TrUQnOBbPBev^w?F45i}&sZR&56KPCGz4Jk&0zLL^i>_1Bq^>^IX2IMLXe zjc!~dz8a^RD;cXZ%IrypH54hbn`1l??ycZwOzLKaI23t{Si?DPKJ~FV zCCH|Tx|6mP!74~(A@wpp?k+s7sk0Qqdjfy{G_Y?i@ia zRv`%1WBzE9@QJ=t_gVU8#(-2dZ^7$Sc}^&Y5xJ@=4a|Wh2A**=YEi(Y4>hT;K6>SN z!qC2Xu3|pZ0X{P`Qm+Jrl&k5S_|b86vJHj%7r%)IfS6bi*8HvcP06-rzq=p&m{^N` zR*x>^F`Fu!SQw!zEI**~esKQ0*m1a=L70hafXgxQ+h1n;FJ$=BG!$4uM$Z(#5e*>5 zX4J(9aRMtUtJd5KbA2-b$}edOO{8bE2YsKl+?g$|`0eU~Gw$JG2ud;IjvE-5izJL^ zeu~?QI@73?U}M4lGF5T2YJDSRU&m6N5Ld&NV1+#j zo;eAFz{bY0gc5RgltD3&64Y*wOLI1rt4w33$r-AA5-IW7gbWjMB+)sCXfR zWO_~`mWIh0e!ZR(X72@T&!g8A5}WeLXvCp}Dm9c?Ycf@#p^m0N;v==3VcEc&8fu6K?u14IX)VMccp;X7EE#8>VGr3j4p<2wmU(!@LYJ;)D-nv1UPy6j z9YYB2ubu@hnQyBjn;*^w;q82FsyRzYZSfeJw+1+n)V!Z{yuYF8x=E+UAmZ`daI_B0 z%|9(vAvmG`eeZVp-?5pxRZhG*0l^h+I8#|7$t2YDc%L3Q=od6;D|LxxVjMZHS!X?x zO>c6p;O;*&yJRUu3C-#~#9-h_Ofj9&jL9ek)LN#JA8Ff^;c4}qiK>D(09uhp9uph4 zhz9nd9v6r|u3=djjY3e0#^f6w$fPa^UClzyw`N6+0Njudh?rC{uOAL44BcNF-#Xl zK=C9D2nW(fLwdJh9LlY0iGRTM}WkJf$}URmwXlaK?$j2 zKpHUi3C8{LkF%YZ*!Hh=*=`FWf78Qknj(L%N7p<32l@#AZs9DwZQf`duSo=h3Dflv zxK}HdqEX|h(-4*x_$MB?N^y(SAsB7y?BsNJ4@Rp$MB2f&t}Pgh$OG0)aU5#WVYC@1 zVZpV3ZZu=-TKw0JR6^>b-6Vagn6KgR z#ZH}SX)Sj5#VdR_6iqi>8x&-JS`Pe7nNekCD)`?EZw)*y$V^w%iAF+VuRZx%%yrLk zZbr>yNeiTP@S)HyQ?!0cw;X6ZkwKj}WEuk#X(Iv8!w)86h`)OR@cKy5Sm{va1kZS* z#X8*^(JdjY3Ji{p4m13+_UY~B0K?G|mLU$E+J|;2@;JA6A{*B!5hx4X#Vope@%Zw% z6p%s`rPrBg@o7!_Sse_)z`#`H`Aoill$Liy>sytZ1|J*Vam0&&P4D4+9;Ma<@uP%K zSiBI+x+$y4k31Qq7BbT6guG*aeCb$8eC@%lvu8ywxNYRH9V_B9)$ML_qZ}IbU2iRV zn)9gvtQuew9ZvEi{$L4Ufcb|F;k{vu;XRqRvLn9n`>_`I-~aiV1$MJ4|9;xC30U1#NcBs~$>2;9U?p_z5%Ds6G}(eEvmwj`!hk2)5sd2x zw{6?^?Ih!k$K%1)2t7lx|I)n?9gDi|wv#@G@75+t+YWkzrXzy-4{0ak!qID?dP7$r6U00=92bxWiZVxFK;>+}DJCSX{%2q9=p;x*GDc&fB zZ@+HLc{QNjHsjihj*YxGdFSos`;VP+9oQX|%?XhWJo{7Ge@fzCyNxU5I7+qn4ZNhf@QR5X{$IU5VGoc(puPf zwr$<8u~LG7WPvSBIps?EqS#?u^o z7Y)+zwjjnIBMgH+BLmOKuA7Zv&b1$W-p1Mca>N29j#z_>wGRCR$7Ss|7BvC0a2}Lg znSFn39I4vajrin+$Kp&)V-s+bGE|$*pqKzjX|$3#8RJK3S8DE0)7&p^G|k->j^{rmU9bYgM9a%Mqmc3T@~z_JNah);=B1O%K|SOT1t zAlJ5LH|}~_ZMyY%Om4ix(N8<@-v7GxZ#e$^(^uWF2d?8{swI`Lxh=LJARn87@`V0H zHnT}I4vaI4x3xIH5TBTfE4&};eAvLi8n#0m97>na-g4-aHXl9i%>hy!L#zmS2_6O% zz;tD=TI`GrM#ShAXjq(&INq_*62vzhO=ocrTxBfO>5`U_D=3YJHpw|lIA+k_u8V~@ z8mFbX9txQ%Zw>>bLhc;6B>bPOZB`u1Q7wM_T~37>o*9OSD6TCVZ-eAw-JGixM};Ah z5i@&M#LRk5wbm9yHYg#^s+qku87i}7H)r93RpVE2a8Lj%Z?@v)5r%uQ1hjRh z^7u}f7tI?Pv(C2lT*D0TDnE`V1Gvzh>Qwy3(OCAwzdgbDkz7zr`M+Ca|PqQV;Y>#?VKW|1na zH|p2F9cQ6f!XEY})ISUB^RWqeuEGs?@ndZ7=F1R2qH$+`7oHq6*bvftcp!@7?sET| zGi9YG*->!g9XNQqxMV5A3kx1bOC%uex;s(mZVn24!><$SKJ;n9YPM*6d7kFG?^Scikim zLvTkjK)UMbs~PNo)OD?{FMogk`TRcW{?J`$u+&=Fg{kt4A=_!nJ(k_>o1Q$bV5qK) z#u`k?1N2s29Wgh@+3Y?&G05`QnJDaP-AaSDp4PZE_KLy=94%X_c3<77#oivnR&I#J zGeE0j7*LbZUB7mOOVpC(fS6^vvGs(b-F(gjQe}*y}D)6POFuCQOxw!NF*(Q zV9xgfkSZ~xCXm%$t~eb3KfY;GLqq}iwSZ3>yU5}E=^y~!^pndC8t#RPwN>`&H?=NKWlawFN z=QEm`S_I>j&q8(K>D{;Q)qYHMI;F7gY9MMz=nNV(AgRXe+1N+fxbLq{MdxJ~ zTqsKN4ZGbuUmP$YC`-)`ZB`K22Oy(|vX0r+nbzb&6}^2RaCXY=LRwZ%K|z}uk`O=& zhT;GQ6Q<(+8rBNNo-pDW*_R3e)Cax+m zsXFB!)uv>@F&HL@EjX*2=Z{Pni`g7soylJJ^5j$r&pedsio3xcXiu*K5!dtZ<#N~l zy$39Qam7n9SMc|=gb~H}Ay1ayWa7f10gs=x{0NDv9{H(S-E*?Dj|53iO3pV!B~u_*+gm zW1v^M$(VL1RXSlL4ycj9h%(qL6>)M`A843MZdT+qLC(imfvYC>G1VM@Yfl}aFzr5w zZD4d_wl^lcxwl(vO(b(R8ZjPZmdV!7MzAbsk(Iv084LgFquTyj;)oCjGR`s`>OmNv zE-pS8FK48NT>Cnsp^y2V`@XZdk>5{c(hXznJ#Uc5hL(d>e5dXc2d3>Kh-0(x-Xf%@ zV`3Of@3<1&wpPr>O8_!j02mTkoURlQMWy3_q}aaR@Qba?Ht=!Vgyj*alt{DF1z0fV zN-EG3WQgl-aFiP7sviMhEbm{$S3n82NT2m&crz{hc(6E7FNgaHCVIqoU6YOhf(VKO<$N*&@N;5YW$_80lp-bEaaxB--CQj2ez6Ubx^ z9H*dVjGp&{CP9<6rZnu5#_O>k6JIE)kB&3t<=XIp?bnxGekwIXn~=Q;(yt7^g6Dw* z0fYTi(A9Umo_26gb#i66<;w&pCk#pkXNqtqaM2Qi=gbMikQ^nEiFGt1IJ8TQOrJo# zoH^hy85O8h=4|bg;w<_$rNh0%$q=sUv_9Dym5tRETn4oix-KCZh}v`WY-K zj-mt?<8ai*mt+NPtn)L#A0xuJzC;@uk3`Ey&tQO)7xV1eJYUA9w1Yr1 z<_c=lq7a!^*130&$Je+MsaP?k2}w^DQ@kZU&^DUjFo;v@pRQe8*yx701pIX2bC0M0 z+P6k?7*ajr;U2qPyQO&^z4EnIkS*Njph4{`@JA5Fo5K)3{JvlOgP3+t0Sc^`Ps!mJ zBoK?|D3_7C{Hi}RhQXTfRWx-P@Ou68`TX7*xj|~r?|~M)SBtVvVsIIKV8VjY_h}8C z;;ksQCRF}!l^Ym&PqBHG&&%1l=95+fT@L5Q+-`fgn3w`c)pjP{_r#bO?F;3c`1*PX z!qNq|LMH6qkmA=*_Db6^!$^m;Pb80p402**-ol%ND>SGiN3 zL_7q;cy{8v`t$x~rRF#QjE;BU9b-T@(Md4H;!dvXbvB;!o48auzp6EgYGdRv=o}+2 zv(T*ZcpcETz=46CkJ{Sf>#LJ{uZE{noexpe@ZdW2A&cpgxuAi1%`sDgXM>$LweRQm zpTvm1+P_Ucd-s0XEX#)hw(I*lQowvOm?y4Knb@PAp$0s|6!@W(z%xxkA>+ngZ z_LBrc$b!q)-&ST9pkcUP1{BHb%XbBtm3q#VC~xMK@SD!CI|P3);l_Y84Ql7?LYvAY zgXB@7j>fl(w^7|?;3q{Zb=4;q#4NuobLb$1+H$b%@ka{H{@Qj{_Yw069lKz#1xuMc zc!I|Zr`NQQDIfIT#gxWnC7R6nMM~Wv%Sk;EL>Z{qSt3M$5Dvv(kjNPXsaaTs7;}dq ziaNUcN_VO@7>}rAC^JNC<1*?dS$|tDqUZz3bH14qx$f$mzu5nL|M{I4H%3JpW5Xk`Rb&a^}%m(1!M zIauXZFJtb#i4+?55yplWye}|3PfT1eh`r|}4Q-(`R#b1Y+JWBTA<96>2&EdmkpDH*v$P-*1`B8b z3v&2Q)|~8t|9^j(_vnv?eQW z5ggx7Am%8q7+wS7tIhII451qRuYN<&2*iv^oSN3u8U=;G4PuuUV(1Bjm2hz*l<0dd z;bjt>!wBBavKj3X&vMF>Mq3a=KM`p*iZxiNK|lQGKmW;6bJ3QUQ#_1)>sj&JB1?m> z(Y5BU=ixjwp{KWg2R#(LjXrdT#BM(-ABw%(p~-t=5FwYOa>DI2QEV-l8!qNAn7M(0 zbZzH-vj}LO*7rxSSv{O7FiaK@U9Kq`X`gAY(Afe@_!g$=py|G#RCAGhQC&=()tcXR z^Ph1aSOj~+h{V7OmH9|E<2<;(N(D)87l+;{4-D}Ib1J8b#j)|U@4)=}csnG-SMA>V z!@!zZa08ngIyVUb!LnGaE;8BA2TQ0-ND9nzyq{JDH)H~4WDI##=Jk-uIJ|w!BCNJ^ z&>*>h_QrEcEVXCI&;#aE*#kE2OCF_)pv4+EaL5B@>~henh}>s$g48>ia>)#$Jy#M{ zjI_RwPI3!sSTuDV$pN|=kT|w7Sce=se!aInW~7Ht!KVrlHVOYtoxHAYUJ)le;h2R zYxZqSd0`S7Wi8aY^FvPrlZ`6%UT-%EKOD#@N&e=#N&i$eJ zYH32Q4k_g{&`xiQ=vD@6J&udfBKz4g5$0hM{{$-<2!V$pQA4ujAn=hp6S+ss5A45a z-?#*4Wp{z-0)*K=G!OI2=)j5USiAt~U?}2n&~o2x-7A}=VfGU#fh7a``89_N8camx za);dFh%Na6%tpuB2dVL$NM@XiAO%DpK64KS`B?WSfJO9VQHhTu&wys4R#l+$HJO&G&0R{pnJcW;a8}QyHdeRUGKBWo1q1V zlX;I@RQ_S88`(d&!s-|Qi3L9f1J@JF*V06o{TCPab{H6?l;)oyn2{wvw1c;Uk6#-Y zLLS<}Et#RbO-fodV3@8iu2b$lzc9`$#-T33TcVm@NLWCw>RMcepmUWx==33M)av6k z0J}!QLeuJ(Fwp_`EXG&G;xOSz+&!JAohPW_{K;XDw*ueJZNw0@nD0K=Wq)_IfDr`>; z1!LhBiIp-grvnuaRIL!J(i~x8>#C_1*SyHfG$8R|Jp?TZ#IEVn# z#IK*+uEKC@CTg_HNH>fUKEQ-zQSa@3r^M&(WmPi86BNDeAd29F zeh2y87N*c3iCvL1j_F2RXA!e=))V|UsG~;*L4uVg;fDioQIb8n0}ie7+%$&Jh&ZWG zrOc2r7mMC7JX{h*z!a;9=0v3%U#r*u0Jn|f=Y<(%FL0ga`{S`G`BSXsDwD{~&JIf6 z^7yG~A+urIcd;rmb0Ep04y=AP4qJ9Xa#o(~g+m)Pb*t)s-5qornTB^RJ5!noxRj1m zwZcF0X5sADYn&N~4C$ zh6p&+Mw7u`?DMY*&w)EPe#_($`e9=TWksUQb}h-c^xgukgfwi6&X%`Ub2GPi-MOV+ z<+|qX7_*iVU);#ab~2RE%m&v)gKUWCoR5Y+pab)|kAm>6Bc{551DqpX*_qEWt3h7Y z6EL5uJdM!A19TH+Ej+NOZzqc68G^b#T?d$ksAdNTCR-4QP%KnhE77h$%6&X`BD9_4 zq1_Fo`yPBk$igF*5)~Vu`^_TU8|JA&Odb5C(f;kufFD2pyS3sqWDpwT$UVpniHmVb z1J*Slz8uagSbK;PhiP*(K-bTSD&dn5`H*Sn@V=6qzFFt4Iz zA%sr=3(cX#%Yf+#1)QXI=qm1fJZKFaGC>_9k0Fe(Q;y2Qp!gh z{SXuI#GZI!BP^VhaZy4xAC@Anz&TKFhLCT8gt}KvI}f>S*lNfhk4>zx7h;jH;@spr zIMIMWrGuVr-29C)6;G`@$VXli3)$v)>l4nRlNe0XT`C^xji&DBgF1W#Ufx+2|MiA2 zuyeW7;1Lu#Q1i_0RD0-!A}wRYhBAZpbv#7@b?Y!X0;w`X8VyzC@*YfLF6*}Eh3sf< z5!$8_e;;@Tc)irlid@?IyH|BP`ww={==LO`odl&&iO+0j7K=Ajb2D+r)qj`wj}h9F zk$zIllW8h_MBNolbAbWSfENULg+fqe3H`2+6zyyT;#6+XOLybgNuF^~V0rIcY7bK( zYt?A_0D^e3%`f21ERG^FlNgw z#%*{%VVe;DY$P*`2S0DyeoXwIOfK=WI`Y0V9Or|1W{Pj9jQJo6g5lxY^QKgcLHBdz z1AOR2n1_c;f|<*B<|2N)Ke%+-xqByQz+bYm0Zpq472WID37A@1DdqPhNAkhBlB8op zYKQw`#z$urxMnfcaH@Pq#COYfkj;3Msr634W*MC>-VUOS70nrTR~u{|(#S_JsSuTI z<_gaR2P|;fqy26Au#+qkVCtv2(-yWcDX}?0hl`VI58n3&v*V>vvRkNW&q5 z%~e)V?y-IR$pek}_Gw99{9pgyf0b;EuAjPaE*6~@ER!I&?u_cmE0ak#o^1eql~OCS zU1l=LXtkhuVoHc4NbVN)z-zIb=CJ8F@0vpbsmP=4MrwCd!q3TAZn3yO^|mYS0-=A^ zAEZ543771T5ES!a)We)uF~hZ>Z8VW#U}7dTg4Ms38LL67W5zk!#4+lbpx(hRfse&=ZsszOmX$rlr64 z9-vcXuMgeBy%AK(9CVEN>S?A+v0MaY4{ih!k-?JWC6=HBk0fQ;UClpJ;$g5`F-4#^ z@)1J1eMqj;qqmray7O))q>q}QPDSfnk`~LIK&Mu~lzsbUdR_V63tBBbiboP<3u-fh|!GwG$^s;Ni zgt@b+vnDDv%vrjW=Ycuwi2iwWC(-Ts{Y`nETepxQW$5TI8TM|4vSfg8I#YQUmkkkauyvf{%4);{jAmf9x}R(@6SU> zF`Eg?Gs@+gfiA=#_>l_%IeXB?*zSE75>upZkyt$Db228_QGQT%dJFn2V=F+mg)V_S z(bzLBWFgL6F)*>*&>{!Z&g7hNZ7&Cha$kF37d1D*MnkhF8Guv_kT7qPIKkE;SH;|1 z`Z-RrJ=%EL2CPNVFB{$T!VRphP0| zXnc2roMLQO>?{O~>|QJ(m7`w>48%Pj(~Z95i7Z&JB6RqT0PjV*3JcpC$|94s^ms9l z^-w9&Dp`2taAAo5U0=W5g>G3Ib ztL8hJA_}qG-SPENV3O^@XsSbjanuWV@`NuV9LeDBP=}>Vk35-t_Dcbq z%Hq2?3ZfD=Ud}51NuhS8euo0$TaRe$gH4SFrlAWrIxGx3xv|_Tt0h|#M;oX0jcyT` zCUCs@CfGV#R1E^Kah1Iy-r+F2&pyvHzX3A|1wY0`WvrFU%NIW$@^OH=o<=CxwqMJD z+Cok&Bx^JgwN{FwVLZEWDnpfk2V#DlXfABcxu;n_2k!As<(xZ048jDs#dXcEORA0j zygWDRX?yxa#~hJitPZ=M-LI?cb!UH3=10*v!*S2%iB}*HK`^?u6}J*leGg&ASffpb z+C9U-k`FcGPAQ{TnaEKX8=3HRP3HGG&-dUi`tK(!dzdDilV=F{>M3gz?IjqOGtEN8 z6h`6bxcHsJd{6iIYx6-Y1%4wqG5A7T}_(IJ6o3@%`6ia6n||MfH$U? z%{8N552CdjDusO@Tk$tk zs3k)&`Pho-q~Hyw_ulfI#ygjE`%WF$>=8kAq;XA(Q(^{h9R4*``72dyFB(qr54#rONoPA6|3A4YS+nk~P^DhS30j*Tb~((J_Q<#{JGA90d# z*;45pQ2zVAEgQb$>(Sq&Gr(Uf5>{jq{mp$m1&^yT(ED(;%YZ4c7D6}AV?U-i8eyjb z3`l`ldZ=P07S-A5GknU;)PvKwt)D>%4w0G;l1CqQmeN8!kJABR^`9u7OgN06l58d3 zO{0=S>}A_}YqqHycR(178=G)W*np{Oa!WxDz-1IiqT~uS0CGfPFi;6OGJ`uEK=vQ7 zQMuB9%Z(S~AXtT)m9s!(VCr?&q4RO{dRJAN#tdz#>1a+Ei8BdS2YfSCuv~0i^J2*zYP4>2tYu1f8$F)a za{2mtR6-FLk~oXF5BW^jx-JwYp3YZ> zd+n7E0qxg6pU>~}2&{9n{Q0Hc2p2^)!0hWO62jnB4*2Tu56}J-GZ+*joK7j;#Z&WX z%;tq&{tGXvu6cK1O%_AQk(?XbVDg_xk!IqUt` zanfHPwl0iHd>0KmpaD~uhYU_ZtgZ?I?JIP%jawr``R(NOQk|-bI7mwKWWoN;b-jUg zmFo_LpA7fpdEj^nvMk7GWe!L-^adEsY^JT@Rec(#47&`UL$tja)Q3W5ECYBiz8^I` z&OQl@-u*=lCC~$&bee2%-o8IN6R#7dx~=_rw!3K5*Njk%Jm`SDs=-I}*}km=dx-4| z04xIeD5e^VtqCz_rzDR3_%wPZ7q~#sJH^%p1>lrYJU(2R%oc-ICKeVq0G<|7!ZU;V zpk#4GFX;9R zUA>r0RN}3XYQIIeav>m`J+}iQqWBUF2H#L%T|Tm;u-eiE1I3!8zl#w=H%%ZuEt3jR z%_^jw4I*R`$?G%m44^+15FsfXYeI;{pX+0?>_+^kT+#S{EKzEv&dwQipw>J;u&Iv;JjKQ%cc%L8L|D^RZR7;Tqp)`BkxZ5&gVF&fZxz;C9A-n zBGbbq`e8Tt7Dn}K_q*}#i|HXw0~Yk!KBCSOhZth92g&Gz$(9-GQv>|Sf)o2+Sh=oZ z!TW0iFzvzN;DR*fYH$)QRFjT|X-@{I)3BSx3UL>$m0fHx!&l!+`10fZ{k zlI}*z>pE6=Wqse15wq$?(9rbS%A~f)gF6UA43NpE+GDZz3>)f603Pwvt*rA4Cw2mKBDF|)DmH<>}Nyy^Ynr|!aN&3DRlt7(X@oS0S^vq%e+8x zFBmdvAlNZMq8FxZ09OSNObwx1c7dBKV*((ehFi8CSYV4~$c`z12?xEak-?XWKJAT? z2iZl&JP&wL)>TFGj+&;XwC!&!& z{%nT2D_J&}_`!{v20GBgrlTYl+Ujwf&+l*P#LNXCK=o4A{=$%Kq!KNnhcG9pV8KN1 zUAHu|iGH|E4f8gJi==zHwq^(aF{ScHW#Vr&77Ldlvgx!)rtvsRl!(RS#98aho)A`~ zmPwKFtDh+4plY}c{@^43){)?NAut>;gT$vq=ib17&!$|YcP0NbPFvauFpPP~G3`4m z1D!UR`82G(sWs{s2K83>PixrBB|Fm=QxY|Zta@ToTWV`S9We+%&<*OT7ic*6+>QuR zLZ%^nM;w$nhr~?mS{%uzlfcz z9BT2Wk2aIw7W=IByRAfrXhghgUc)Fk6Y}MrMh>z&!AyD95W$SV8wt8T-daZcpX^14 zY1~twjMAU72m&(Isb@~biObLpM)$E0=D_2QzCBjc8DmffCcvgz8CG>qND*G%^g#qD z*|w;L7OT2N@@hS)iAjqR5u4T&^@`y4tf^F(piNkpEI$Zx^xYm1^QPzN)tINFE@V+x zLNKI zb1g=Gew!D7t(iBCwMfc8hGZG+P0oTBojmd((Tsisb>6bfacaR>^k_jQN{a{4V&Mu~ z7N7y~2B(>-tsxur!;vOVoG&%GK+qIHegdAyd5}R9IM)mHJfkO|r4})a`BNVbbNxZD z|JW-#vdDI-k=-pj7)M131VV9vH?NN z5L!R3nA(_FNRmBF7YVlWcs`$9Qb@{_>x6F(qHAHUzLro7ge zG<@9!N|`5U>3x8`4KC!MRZ(VP!ab<~x9D~>e`>aL;QCd6{`sU|{b+5#j-B}eQ}pCd z%6Whn1S1Rsio`;HcdP}J2+C?Y{HfxUze>I+0 zk~?NW6sRt-k;;k3gc;vo!Ts*zEsV1~5=@>KkhpGydNmG^Ez)xLU*25wCS(xA1VCW+ zS5j?Pnm6`;T>CQ*2??KulwADr<5^}-%~008XIqd1(3?>^jE*tf0b^Jr(;HI@l;GmJ zeDFk4vhM;fXIV+a@tnP?>cnGx z82b%OCW`hTLmaGX0E4kCjizMtw}vY?xw2h;YYsA%FoI9aZ~f)`D5{I8_1Ho&Z*KV$ z;8RR|PaSvmVu*YEKsi;N49){1m?NZpxm6omIO0NqE)tMPWG=EM>8#OOdKBPj-kXX< z-@{fz&g5I`p6jL$7Uw82k6_j(R5qtoY)!K$eIPg8fC~JLSus$rvusTP?HXmDW-bR zNLXD#``C82z@$^KP+DkbhpOZMc2Yw~c9!OySh9L29L(;aX2t$6_AE(o73|9fmXJ?r z;{#PvQ2V@n;@warY>dVFa${uO)s)E~y9`z$EB&i7P|$HS0d*{T1}Q&}Ojl5C#PdeV zI5)77m`u~?Z}!JSO<7&6Blbce_yp9s(Kel>(+SQ&$rv0mo%msts&c)7PH_1CZnb^Y zucAB#RWS4&^J){B7TeR6tE}tI9@K^r`+(xmp^W0|cA~aAta0U3lTT=SL`Y%V_T$O& zbz0GIuKF=>Q1pj=pfAqIY#REf3JLv;%)}c@g3*l=Rt{igas`BZQiJiMO3T1S-dY|R)zV^+>U;d1)p!V`^PONzy!gIU-*x|Bh0)NOIxI&D!~&{4U(aCTEb1rNyl(9 zvJr8&eGw}xJs%xP7U2e2MhEF^a@h@#tAW&Ll+mH6`A|NcdYiE$o0^$-fA-s<3UfOK zA{U=<5~M!Bx)_~^Aa0jF5=?JZ<{}tNa|wK8Ot#aZu70r9vz$B@TWrRq4E70%TU3;t zX>zMxY!t(|F`;&A!vh|;oDu`n8ZzY#7Ev z7StoS$*eC<`Q<_Djvwq~)}i*EhJnzkL|YYuv*1QT50h z+7RY(B$ON#Fdrc?3Z<$>0!nn@M#Q^!6kN-udz&cy6NBX1NqTf!Io0c#{3DOj^F%nZ zUS~M66?Yg=cqz@I|KNC@4AG3o+eCO$=G(!c5z?tqRMA1%M5TmINqP|r(g;&Y8nyk$ z{vhRQe<(XakrO+S0s}gKWjoIPDD-crHPPJy_0k%&xh#X45{Dubk{UfMKJ>$35XaV3 zi4%v@x{uII;;h2x8y(&b>e&kK31_KjgGC*QGhNWOB$6d>J-AP{U9LGm?Cj)AZ0{}z zg6QrTFk;zW&*x-wqXx*a?CQVRO52KfAM zp7d$0HqbcPXTI)XY*jnL)QpDwaYUELh7wc&!6G`6fv!W!+`}m67R>5ZZ1x?skL};gAN&Of;qL>lQpv9sMi7#dt_t2llmI>Z#cgH?4}CzRzn^LgqRV@=|jnts!f-a2q0Oa*4n0v z&*{`6{%f|JF=c)0W6&sNT&?pV$Yd>jQe)PtnK4_(rS|!eAqF~{Kz(~BS7RJLrYQ@W zFvGaCq9ap@PqHhTeQ=hG^)okw8P5H*6h5wF$D}e;iBVV&>2nNiy0O5GDGmWakO0=n zpaLv~Qbsr85z;u?tMRr{&4Xd&vgWHvqZ$v$|qz*Z{`s&Xk|DDtqm-yjaOhBfS6((~o6Y7>qX`&rOJV5a4-h!9zc3 zVte-5DFFkI>zg=WvwM8ew%(h;eZRIXT6jIbz7JIBxqF^P#(hex2?j(ALl$ZzE4Ktl zsf@>Q{f1M6VJPaYpDRr(W4??O*n!2#(4yq%Ii>&c*sAL&sOupqSH0dRWuhF1$gCA( zZgMeNa1!Q*rz91JJil9w`JCFVRBCAREsNLJ7k}1^@`G%R9|b>}uZqnoAsEfMG+#C# zU=&u;Spk+{!C*k*1kA83=RVL{FsBf)?E!WDlnq^Yi_lEd#t(E+V`FaM#d5}sz$)lX zXIeW2fplU^FC9di*NCf0J?HT}DUG)JOfrx^Q8kJ&&Q9y0RODu5FYq!x&+qH~d_5l5 z((N<~RFO+h4(%&~*Z)>~h~o9GANzyPFZu}N7fEFi6JPmvHbPbSX+nZp&#Q})9`dx^ zyC~^Kgs&OY;gf>i!?m#Y>$m*Ze|SM!-~KbnLzp` zz7oUmbNjl%%oT5N%LH# zXe)vXH-IrOGmoITSLw}0GXRKY2Bta!uMbZXa++Do(DtTZVVUnGXBZDNQGi=!F7MJO zoiDDgKtEqdAV=9t*Re%&Hci0ZukSs0pQO6Fo1(Rl{wM85|9mIKck$laxZocEVUz!9 z@CNm$XlaB+o%}?;K__gJuvIaAE18x8^tn1a=js5j6Y=>}M;UP!D?YaI(=DQ_kqo#z zmtZxJip6M-M*Mr30DH{ayUqGrZ3Kt|Z`st}DM%~EiX>lyG3O70xo|PH=ENb1h2CIl zYMf}iMc7tKmJcp-W=3K3cmajjH!cx0I2hbg(OrY{MsESitmP2ESSjJoCJh;dWk{02 z*b;BzdWWMWWU|P=y%2*BN63wQ4Y>>}JI@L26#-VQ3Fb&!2;=^d?K>>d>uP^}{Xu-K z^LT#qu%1l$yE#>*X^aP(R){GVkbbFWKm%mgWNgFmNs{v1!WWb-C8kJ1Bh__GB18+I zu$B3qYSiHonTj@;wS;L8^_MbmW{DSLMjHKA`cW!% zE0Bqdf9iVoSeMm9byw9b&H`Ler_BEY7qal8V6fSU^WB8?CLF{9vbbt6E?CyB!SNt! zui<{eGgDYoK`*1i*3GV~I96TT%Grlu4TFdU$wD>_%QO(O*G7FbC#&@Nd=k36{l|a& zk){5Lm%vcWPicgWrq3d=redh+c$GReGpX-smR$)=D^iLuj&`OJXD6{E zf_o&8uU4f=qPn}C{qt@VvV1;0!`IG(j;+je-Ds23M+(IT5jeC-nB{<#@|lD{2f2{+63}1&I9j%K2$4d76-5E#tGmXG{Eq$;lF+~ zQbcfX;3a7n-V<&@E^c1`i~SYWgiPs3n?h^e2WqR?l`MEhwxgF37{Zf+VR1-!#lQgh z_`=65X&w5lnL&iYGd-TQ@O+Z?p77cN^gxlbuu1fM6ecnXr66q3u)~lx2V*_I3=?k| zs18}VMh)f0n|B(@?CDNj3L)_YZ{CHVXQ`z?#JF@2XFIx^_^Bc81OJt4e}|t(lM%hO zK(?)$qv}FBJPZt=&?l5cm2h<|=H?SMd~Z?7#OR7NA%}cr9o=pbTqyG(VkKlC5)0<| z1XslNmOzGAXeO(W(BIS#Vu*%8pJXChC_#*@E6JD6#-u@yTQqSySnV(w4| zf-xs!#r%d;E^B5NI&ngF6wlV>1gIYrWcMpYOHl6wIIF(WOILst6^m{nFx;?d)>0VL&Jmt+BemyBajoNF#;eZ ztnmdjkEDMSjWrsbkrg%+*smPTa)i1_K!%j$2tpIQMow;ZV|omy=Z)CW33^N zlt=RnNd((r^i2<&kEyGA3N$4ae3Ig8y-<8EsjGSs&hu=9`8v&Nk5rDhZ0g=&J-js{2MI3kN-zvw$CGWSz0VjWA7wvF$l^tb{FghK zAHM)%%P!uq!gPN5SHPQR2yhm#{K8eZjVb%L^n+uohU3zDQKq}Z7fTM=f+-?AJ+eQv!8;=X2sz7 zaB+&c%eZS@%%!+7wkW0CMCAO<4PlmGq|AhcsG4Uel@%?RT}B`!mmsRjvwF;8JWU<2 zQ`bLf11RX%3wQJ|z{v#6+@dnFv#d^IP$q&@K{unsaThS)qS! zHP}W)lmUwIrqsY1ZD|F#W#?}avkEzc0j3GtiZXQ(f9;Pwy=pj zzJ{(GOzTQUL5ymPlnbKCZxPp47@b7=t9E<${(1KOQM3fF7GU3Bv{kIa_o(zYC(K|N zlLJ+6?vo{T5Qdy(-!(!f%@FvqkUc`jE6%RrYV<*vg5P;l>{XchF(WDfD?rr0SF!OL z9{VAm{{T)9LUWxtL=*D5zD~@d=o??>r@rRYbh{wsNhk$Ab!?bJGm29*Nma zX*Lcgq!SC0n;V1!IwY5ACCYwFby}GinRSV($7pYJf=ybIN>y49$YGl5#2{ZCOIlTs zxq$F?)&@T;+u&DfJRBR^^6&risrv;^iS-2{&I>C#=)ds~l-uaGxAvhh1}6q{tXuA7 zjGm47Nign@MpHZ_XI_f{OsYc(uNjVM$c<15WQ~|l`@uS&<)evQg_#+q1bg~mEC?zg zsv*9LIAhgZADEG{$gpt6JCLN^E06{!z`;9#eBIRtMm&dW6(50C%m>xkF4Zy3CAg_v zlnHsy$vlXI5;-zpgs(M`zt!-~nv3cV ze-u?z#L%@t6ykHeW%l=Orh7g6&q`Y1d*F-BeRBJDV9jy&I`;UZBND|z_ai36Ba%6SwSKS*-i zNe^|E@l7v?8hhk*rd5+@c2$@4imbnrebbE(M)(0!(^Px{1`?XLD4~raCNT=wT6=R0 zJL)($Z*fFfO4_T0yQ~vPaG6IQ>WX(L4*Rzt^o9eSc$OtcF;3kvY=*WOg>m}Q0m%G! z%uEb*K&&$-p@R;;m)!XemK;k*bsok8ud;K%FfY&nhNW{p4=`8uGBE{=!8m5{pK8p$~`*2=8)D;g4=aNl4q4m_G|e zT*+7DwtI3sW_KupQxfbSD)YN4n{N}W4dX$u%Z4uuT69mAbFMdL!~{!updODh`?B~n!uYykE*O1>pGurrnt2P zGtIG^3Pa)TQ1|cF5X${`9^n@l)}JIYo}vW-Yv*s%zC zkWrM2=W$)U$Kz}7_YR}tv_gT@_a$GYfK4H!A*knD^n2LPX6DGy28R%g2feKzK!f_j zo*K8cSzUlpbUUu5v-=46+{@XjHUH6VWN+gUkBFyiHzqW0uhn_Kv~( zD2jePfhrFK4BHN7v*jj=5(W$QmzCSdK9%=2cng0}#*5pVdNc5#uWL1rXRB>Pa**q1 zm)cbM`BDMZ$Vc+K{BoZpmE-4|7{qvOT4bQImu?4i_V~Zan&S5G%@k7vvl}IsKh-dU z8JFYL5O)4(tKtscjcUn>3%cZx4%}!&M}2I^G6>F4GIU?n-^bZYB>G{46*j2mnFLTQ zthox<=8=0~brJ;QC^h$mvd$mJC1>`CeYvBcYVKCwKO!aZ#0Cf@X&u>NBGf=dK3BxIwr?F0KnJN$YIW=7A6>b zCz_-+*5eg+E693|u=&G~)SWsv{ITiIAVGnf-3p?^oD@`K=F8Td1t&*w=OqZQ$D+9n zn_g{?ix4BY=dFZq#?0v@UcdD4edi}gi{pZvBSvDD~CuCmZ zXvJr}DOZZCx#jvH#vn$(;RIS?r4gZByO|Ejt+C#-hNCywu$kJ+^Vvdv4EVrGN$5;N z<$Rjn9iNS$XS4bQa9hPKVrF6B?*ggNk#K#Z46$&SJ&%(NBnvCJPHH4}plIq&=2II6 z^3UMF%QEl*ghw%M8uo5WjCs&zOr(&}n}#Rs_xXfr_Bd@jh|wTP8ocZ&gKi@nDJTQ- zIc8?HAUk7PWiMGC+xk$ISzPE3@5wzP8Rp7bpn8s%T!CKek6({*=8V*Hb2M@F+&vT^+rd& zq}AzE<MRvimPB4Lx0|1H3)Nrk|i8)=~7T!(p z%#&(3OQWzR^UkuPEPlc|16affLC7wqphx*??*l2PfkdR>?q3op7n860@o2x8=}(e0 zNKXP`M<>5Xx3m4~iG|16=8Iuni{-6jzL@*Lf*L-45KPp(IAhB9}Gf{op_(`AfiD$~`xpwI{AH zkqcQj5?nw_LdqeZFt&-1*hBBXkQd3u_dNR8S|$vQlYA`#OSDih6z8*~m+F#+EzF$W zc7{G^1sr_s7C0DqHRa3On2M|p)h;J!g@@jwh=#D*m8(u{Yi{RYmJfe?eI0$4f^1zC z=6fv$MHG=SvvzBy@^Zs0!@O7%NOB3v-ecqF#~*)O|Nr~>EalkuUEM`CVzEQrH*f$# z?q*b~7*OEPMG3sPOq&Q=LP*XTvHO3YFNm33lzKWF_=~c33AzMHq5Ux2zyM;UB4#Sy z%}GvUa7eS%Sx~DO&4S?@pz}q8Pnc~;`!Yp^;bX77;!dE)tZ>9^>zQpKfW|axW`~r z2{0F{?(6gQ3~$ldk_lupLNgoGG|sU%!?v+lgT-DHcmAkgv4wWm_Pb)skQ&5+A|iMC z+J87mMsNIJrt-4Z@Fh3cBjk{!d7y-FqlN)ky9J6BO5hN2rFGqk_GGi7>feqS?Vf`* zf=VZOHppW%u_8d4;n8Ab-=n@l+M@k*JmR|0PjI&Vhx#g!*?02aU+~@)7p#5YLs)sSrj5r$h5or2*NfItnPWS%~h+ zju!|xax<%d44|%wlVdx*ILj6gz(j_^+qupE3=-t3h>NZANB!ZCxT7$LT62q3ZcJ{b z*opFS?8LvI9mF&wP?FZ5_<&cd&Q70z7P&a(HCt<}sNHbiEag!(3f=CEH1KRJRNV@} z<7!G6SCmOqjuCFfUw{shf&8Mr>YRvuXFUdr*>U4w?G&k6z~?&R0vK%G8yZqi90oFZ z)<&_`p9yhQKRq>tVdV?SXp`cQ>>&0=XRv%yX9&iW?v9Ez%J;$XoUJ8Vr^e#emlNQm z>56Cft8??W>>vN@|NOJ-P?-+P2Uz7iD|6H+ktjv?gF;0B65Wk21?oHm^~{5AS6$3O zp4JDF+bqnwKU%;Wq|KV=FilOgHU3qH@stNhwzlT*E@l8w^Q;~nwD%myV4c!!aH|p4 zLViLS^Hn9V7~E*R7j0J=_jvRWYCQ$7cXAxhcC@QW;6Wu?Dy5Okx_vo>VI*W4v|M>2 zMK<8h$9Q3$W~ND5mc){>?4Ts--;gU1e<6R}%nzUyvkn3a%A9?ZaYJBr{J37*`HMMX6Wy*mp}sZG(nYsv{=_) zZN~BQ@W3MFnQuT<<<6blb-iqlq}%%tsY7vF7bVKv=X$d{Wtd;D^Zoq|J29%RmZNSQ zNr6fcD!Yh)xC6%t(kM%Q5u>nZkv6g^P`)4U^8`9W-6nLn*v3+_{|#NYvy5K6 z{7%@Io5d9ZuM#x8alabM6jYMn;Ab=DTgKU)Aj;*IoHPqL$~d&}fwE>ddo^p+yO??d ztn+mIvl=U$h#3tW*kLa#AgN)H30SW2*M2;|d9>DOPDn9aKrviJBr6iZ3LcLUQ_)J% zZ7Q+^tb!4(W2zcw{jL7lDnV(Aoiw(1=}jY9fYLjBe0^R2aU6Xz{l{0&E$nUQzS%Y8 zq%Y2Tq2$-TDw%!#JOyQ_jAZI1toZ%)$3L#0`@jGB|E?F-S|YT@$&I$D)l=YQWg&62 zq35$WHO;6*i-}~Y^+_dXsLL;VzZAvf9C;%3mzaE{97QS5i2x3c< zd}lf03ekAOT!^SQSX!2rRf1Acfzb+!Lo@-<20XYP7t)BNM&tyDlv$z4+$*rEWzmG8nqe8 z(mlJIM^3apCuK~Y?O1+;%kiFG+Na1bC1(I{_pi`1^d?*LUWNY$5)OWr!3Y|H%n96> zyLj@W_fE_)#?a$oz@og*m>3DLqT|x!A#{U2QW43~@F;-c;$^JsOUenmuH zU)MmU8eLU;i_?5wi7D=oz=qIoC~(snzPm^1a&T7`xr;Cbfee=wWt>w&Nr$CE;+&S? zIOEpx8qKUuq{*Y78wQ$-;>}=_l979J=DZ{2YAR_HI$&eL9~Ri^UO~6(S+`VY)>*RL z-t+5D0s=NJ?_>yBwGI;sa6R1RB0WQnuNz&vvN1#R)`iAi$F=dFqKGSz8i!KZ-GO@m zx#I_heVTpETrwHSTZ|TO2tpWL+Lr{PXiel+W2HT#*nVr)fe7&KAZCYCZr5J(!}ywHR%83Syr=ZC{UlCxXhB9ZU5@zh3RZ()Jcf z7o~c^k0n4t=weg!ac=P(M+czcWib{{+yHZCUa5$=@skO4aHw)$?|od~`6QsN#7Ag4B4xEqbmhU>D7lB` zvdA~dlq3-XjO+j%u(a}~a=-1Gewm~Am-w3Z85zHAb^x!x{@=mxbUEq7=#b}K*=UlN zL@__ft{Kx}VmjmNf$fTPYEv?cs48G-Vi2iu=kbgz)XEwONd%m_-@v*Df0(FDvZuG2 z^CHyABQy4p863fg3*-X?Yztah8)6wk%QXldlu&Nnro}K?7njniefNpJ!LV#!BSn1O zD3O?!el9Q=+aa9;&CM(Xpzl?0ZS zENG7I!C%MUGcGzX%XWi6=~r+I%(SfOaNHT05G}T_wGm$lIkW?B!JbBYl0&RQop=!W zMrLh|`MEhQc2%Qj%65!g#2k$Xne#oz$GIS%=gUe1V8M|NS?Sr+nShUzCRWnCUrfO) z1u$_%L({vi_msQs)&Q{Np%(M46qg|K2tAq6TE~u1-b}>;p#zE&6r`DRFR0j+v*SAr zm6jx~walDi>?ag!p^CY288t1dL$_E)2-11HV$+)7stT#tiR-%tVoflX z!m`$evCZd#j`gbYf@L*=??I@$m%^E{G5xrj+$u|_sl(2cd~UxmSY(B>z7Mzg3`@f5 z#>)x%;(@Cfe9)k~9Ze6pINqVxtmWj?H|N-ZBEjGzTxLz5Zqb3oGBgqh4q*?4A2P=l zhm_^M1|1)`FdpJ;M{wK%WnKSA5W)1D<|(qyXE_S9W z=uX@(u!Mt_?RrPY^B`S(9T3=bbk0<^q|8l506XP@m8>;Wx28|9Ut0-|^2Y1ZFJBc`_+*F&^kT`;L^e)@1d7dh zzeNSM;4df{4CwyMb~b=eGq^DkYeAk|mDH0sdXiJvLO9fr_{Y|d*l;l$hC%N+cRumo zxq@jTrM5#9h(~bdE=-TVhB<`U%=sFWM5CMI2lH8TSMJ89bcUg87$+7RKRO*!PtV3< zj}oKFd;#g$vkh4zFo6R@UyB3oKCwn`(DTn+*xomU36uSx$waM~unawSdamLj%~TDg zkoJ?N&yGc*4vWZ0WA(%$F7#25b>7QpZy2-YLjgM4$s}`{ki0hlw(?T6C?nV7ZPf8AS;C#YZ#ET}d+sE|!qSA~|=yqi*-SdTBlXKxa zrJHz|r2Nfd3LTV$(la@62yt5c8;SMW#Ad$76-YZOecn~=oyS>((aJ4hN63YGVN=oM zwjM5!{@zMy>6~+T5)!ZPxt8ekiFZh`t@9}-Br=KQE%Ev+GM-e&n}WfR)en;}8#f(O ze0*pnHO$qQBZw<(72RzWRwKL2OL|=p!P|NUU~ONJhxHLyDpkPRlm(T^dag~aUT&~s zbwk}(!L(F!$~(*A@S8#h=E z;10Z*y1h7MOg*XO{^ZC76B0Ug>@aL5k-QFLDBZ|p`E<&nJ?Puy08o1)o@+^{OF%n$ zbKNSOB-)8PqB1g^?yJQE#v+qAzkNk<_(T^JoG0vs^f2=HRA3%zwyT%XGeU+VA;N6A z&@(?9FQ?=uR3ut=2;37g7wt6oZhBg}eQ4oA?o{xL2n9vqF0B?YQoh+eLaE~iMW|G9 z43li3a&6Dg=l5ahUf@ct6gHELg2obI@Cb-U_Qafmh&^FsDuU_UV{F)m6QY~2-)Yr< zHavW9Y6>rUzrVF#gU?VS#D90+xK4yvLen^m3Sa=ZAa-T1C&0>yki8x9;} zt8Yum!Dg9KHEnbYCE?r+zU~-rpX=%S_e|BvB=kJ`gq#5TlUP*d*pMGeFW>RBhGqCLE0;>ju@l!W=Qi?J;g zn=*)|(s>L`ZAfg8DTBg2ZhToQVGQQbw_w4w1V+0Uh^3NwutqnmxamoPnPMuC069$f z7}^7hCEBlzG>KP_*+fLL_V(AR4ln^5tfrJVU4`WlWMKe<8!VepqZh zkZ#)_wQ$lP3DsI01kq5NOZqP03PTJ~A5odJtHc`vp9kg#cy^S5cVT6S6Sp=($eP=! zloE*W%Bs>$$LcgBT=u92-(e~6(Qb5i^1Y?hOhK}G^J=&2?9VJK{mRDSpV$goz+`vL zkxV7AA<%BHN;(Kl_)pT{70MIZ_iMNM`u=`mYvN_vL;yP2WC|`2s9yugC8FbeQ2HdW zcy2J}v72Sf(r@1gn#c3`tc@$VYro-aRTg98^LemDkfzbQ$(yXsA*f7Z zEtaWTlqp@&45oZEjjvBX6L40@ZCDCsSZtHb&5T$F(V;fj&GgmgO&G>B7Gzx9na_w? z{`zDpV0d>6((T3u!Sphx5u>hRrSvprH}I_lNbK8ooN(aLWT!4hmDIT|*y8tTCWOM- zV&@wdNfIO8+t9t2fn)-^aeNJV5|X5_$XTfmq|Xp1stE@TD`Vx~qz8@Jw33Y+)VdZ#BGhl==_7506- zqcnDSL|JSbvxVr%wnUdlv$>_T4E@!Hh&_ilJz__oQ*d^GoBs>Q5DYP2na>0-GNE5>IEi=kOHP0=A>FUP zd+bI=T>D=&jKj|ciT}z!n4{8F=`+q;U}j{q~jdchriHvlUWnXwFM&`hO4>jqJ&s6GXyTG!iOx&{ERBOfRrWa$c`DB-THzd4no-zcEpF+xD{@hQ#ljM~~%r_QI&kfQp#=Af}K& z(R(q$PsLrS52mg3ACEt%!>-m|lbO(oiaa@aolI=D$YxxFID?(+7&bT?_>Bh{ED%no z$|&2p)Emf&D5=sPTFmG_O1z3csV?&vwV z$%P$P9#-O77Kx_W#02ZAGQL~e_Xo|~s!WBe?Fw2zoJF>sV0Ke<1Ca|WQXeEH2#U1~ zdV;6nN@gcRQd4%J0NqipACX3m6EfoMXSZ-0`|&m6a`d_sd*_%6*P~T-6RQ!jMCt~= zzmMf`XODk8dWbaBMjkWpQe|4{gz|mAx6vABE&FOpEG?a7WMNqKfAz zxaKFsLme#tK~@>E4Fh*;`{Q_i69;TRzQ1wbq37(?Et8(EU$JMSKlN?*CLc5GwzE&P zd5!W{JCH8+O1yv7x)x>)dq?yHo8hz|A1*#Z)|YUM9*5?V>rMoZ5%bxh#A)0&8v_ci z2N(>^XI)FqZc{XGZSYE&U2#%&+=unfJ749@i$1Nm56pcYL0RTk<34YP3iyM}4N~Az0#$jPFXTl%x$#k@jc~n@Ey_?&k!+_qF{N^;0 zJQ}lM>;{+RGE8Gxn5Ns&8g9Xq`VsL-ysAecq68xM+jf8+H;+Gd{Kiw9)~nHHzbItr zWT&idF?B)(l@t;$hDh?AKzi+kwf^IeujljR1;eiBJ@XIX+3A{x6H4{quob1bt3~9| zD9mEwn7Lj1DfzCoo@ZZs?qFBD_O#W$XurN5-_H}Ar@-hZNh)PF zNYg_;!MVnQY6SeN8~pRnKl@2^t@-M%Fy_+m|C9HoTax2SdLRs>XLY~-V@hX!jC7=1 z8BW*`wi^J_-6ONIm#HIF-^z^eg#-aJ`?i>3nFtHovE4nWck9}f9T$vm)N>?5gr7{9 z*1z;&n%}!W44<`#|9Cez#ZLT(!-EU(BkfwE+#rV}yyT8eDx?<7lLiGVMolZ+HR`h~ z=}2O;QqOZ!nv*@W8fMf1e1q4sRENcLD}@UyoPh#X!C`Ywo}0@GOz2!_GVSA{Q?4vH zZP_1Xe2>llD4wlBi!&q9J>A{!HGOoZiG)_my`#l)qC|B)4Ps|EO>n89U&g>Gqu86a zw7v0uF#GTf4s}8mH1f>Vm>Y-Gm{)A50oqgOSv5}GKQ=p16ayd!Q6yOB1ZRm?)==(CtaK>N=ScT)t-aK`~$(;S(A>hJ%3-#xqnB6}A5$Z_LV;Py@5v8_|-M zplT!I24o^&2D&ip0xeEcisqP89^ngxya$DsCLdRmeP7xqFrs35qF+RPcR zc_pLAkOY;Yq7X5s@5t>tH*kU+H9Sg`+m&*zj=>f@f?@i|tI5{!1f~jtJB!A0B!|`( zrbJrGC1ZwhisajuQ0u3hF-)cc%Sq~Dz8Nk;O>oPFWf#e=avYOvFHtwe0zLkgZKSQm_8S7>oq;Ut0&JtR#c+fy zR^Cy?!9k@%F}?7LuoM2Xg)1$2wzNgpCM=#(J;+Bk7K)+o(bio1sh=yDy$wsghs{a` zvV-e;=>DXFwdy)DKW&ORR_mkad6^_65|U=}cUW+%26uIfX$Z?=_n@k19NuRq?P3=h z#hT1%;SZYEjjUi13CWY=cdJiG`RDsBD%I`E6f)}qn+;58UN<%z@+;KX+zzmm#n2AQ z>p~MF(`uAa9rhvVN9!JsaqX5%lu)sPM&20NnR=~4jnj)v*>xr z*}-W%S7M}C1kHnzG2}4~mZ?K4V6>dYe!1ceY8f=jVm(6w=huP+nujNUAUc{%E|i0V z_%`~VNd0C*4VrN=zlu3Y&FdbXYfxSA)r91Pg>|h0?veZ-a03O~dw1n>WAm&CgHXp7>rllbxcGA#r%N_Vx4U(L}z=5fGgw%HiL_ z-vfrnd)}KX1_|bAR-hXLz>-Hah^e8?Qm<;L3(Ob?0@UI+{ZuyE!;1@+{;s+`w;pog z841f&4rW|h0xrdhu%vv#t=BeQ+&@#hTHuEk-`=+4Aw=w|2Di-}q0?qV8aGT-A8U zurc1pc+lL2o`2LMB)l$Wf{P6h6qsA83Ca1!z&%A`yhf_m!l!o>lEcHG%EOTJ`5%MT zeGi%ceeVusiG`IE0P$vHIf8gszq@v&djq+>B3=y+{uEtn0ynCHt3q(q#DQT*hh-u5 z2PjKHos%^pe1`ZUT6U0p+hjw?dL)ieM%Yl)jQ|4#Dm$ntGfe4NW>a!q)m(klIuNF; zD@(0l=mvPyo`$W+60-ewwgR03I?!1wN5P8zh{ug$GFQsrskuf1 z{7;69xDN)7aiw}r;7Z{ic|>;Cb84Sc!X#}-M!jI@-PY5+XgO7Y;qKce(T~$J?v~@TOPC0)b}Ci>w8If_~!;#$Crk2R5yv8pi>n zBHDEuE_E4n6zSBX(xF1E?a*fcIJwLOhBdw-oxm_TDawjOoTZoSWvPw|kdC8u_iw{s&&gU6u2Xc== z5j>HQVLXs}b=4@+xz<64?YX!uK2PxqgZG0$9ad?kg+Xt~FZBFDqqy zgDI(NGHPDjBRD2}GT}jGn;{HTzEzmK!r~ymY0m&&+g@K&N#TuS)FKVsDgEa+b+{-+8SyRdr*%N;E8SzgX={!(xTx=9-t7XW6G;Lx#LMqB z$W38*mVgJ}4;7F3C?MCQ zqUs@F5oi_j28|6xD z-4pBh`Sa=4Iz%(|VL=(p*e}|@P-CiNBg7wo7Xg1TJ|9pt`2wOQ(JEy#UX^X?yA&+E zY2EC0qe@=HB?uA1KksT)6e{#4}nPog#{6DtPJ7REgblq_Xe#0I z`_JYy!zl*tZe5v>EqH%nd_o%oiAjfOsj!Uco2&tc8h&)rS_TYGyFHdXdXf^~`6h0D z&SF11P#PxY2-|4MR<;;K4pP4lj`E=$ts`(%VkCyOd_2sG=NbgRAKm~znug?3ekS8i zI>W2+&{!MFdEl9c*JPQO|Lloe@Ae#*(|Wj|Fp&FyV=9X3vpn}BWQKV8!N@F02)`WN ztxBJjwPDY*$2IpyLIPO$$vR3|OurL45A*(~wkud(SrB9F#=J z{%oj1YwLb=PWz!2v+|cGBZ&r4L%7<~a}ZXE4ym|0F(Y=c@V$kLGhqzgRPX9AY$cFC^!VZjj!|FLZRA^R7rfx?&JS_(N915;UHjYiT^+b|MCrc=Q zC)=Si8g^$W_FDyGCi}IJ7UI6x`fwVPaBwTc_mbG|Zz8-JgG4|-09GSvng~%YZ7yfG zMr5oe0il?}u{!m;i|{%>xX%Rk?T~(?rkO2B|JXc1US5stN%8r7lI9RN{NgyCVmJ(z zfD06i0f)G=c(Q^V2ZMkr6+`UsEQGoy)^E*vS!Qywmf>tlg}N*={l?&R=gpE$()lM} zUq1;RY`pNTF1GVqW2M5vy3QN*Kk>AQx7Q&={LJHxA2*}>F+Wo{f<}ukPP}8|G|U|Q|FPpqmR6QAR~-JGZ_g6A*czb6*MTK2NxL` z4C2(i1Pq3|!9Em5b(t0kOhol8u)e7JF3~HZH*?sGn`cA=48`+>fhQQAU|!R*tFUUo zj%;N@4coP2l47?ZQ8x_eVP2Tu5RY7uM_d+XP+_zPVXiD+N;}PkAtiE(lD$0LpJ+a} zCi64AKAT`=Sh{8bDdz%mC!aQj!BuuSiqgT~&=8L1iV#x5uSQhiU8?XTVJw^Vqc;1u zibN${@H}Al%}iTRxt5?rUXX=F6Uu$xdzwAx%SmAd*9|JBRU5}JQy^6^n^|vh;6lCZ zo==ofHD328j)%Ufc|rsj0tCT;B&3c-8Q?B4Wyz#G|F8hq-mHn`7{-CM4#|#6BMX+3 zut|G;#UxYqvdo%;K@cX>Ub8HRq-@zdg@Q2UM(UV?LxpDMe?ya?8(1r)vU|CRoNrXn&DH}evgxZ>FGIM2v}CeiyX4lbAF zMZ_!T0UTgmAxL*bF&cf|>H*dF~kuIiPA1&&Sf{xI+8 zDq^8rQILwjeo#809XLwk=a$a313E6lH^mMN$TZ{)P7TS zxj@7}oFDe+*OFSEdr6dWp}z%$*lLVHy`lNgky9v4bbkn^PqLvZS8)Tc(<4TX3eL;g z&Ax7+a+fa_m42hSDNuEG-mZ&4w#wx5z>*ST1O=oX?U24rB6orL zKdq^)7`pIpBdJBUaiEbpZ0-3Bqegy%b1&KkCK&j#AaiGO`DR3$1AIzlf92rn)cGFg z`~IEyVS3ZgUKgr3#e8+2El$1#R7)95f~X*e$b>Tp`+EQhv1)ew2Ps?mcA^JZ2i+-1 zx9>&sFE)Y`mG^G@8jAFRcy`}S-BYFCYjR?9@{gq^me*hZ8;vI3{dtlwfP~=gG92r?&?lbJUXKmqFjMMTYrT*F~3Ws@&3>SOV#xAz5~NJZk$&rb=c>C5sVwb6dp zW9KHyG@^a{hZa7zYW%JrPn4q2&DcF&_=~n^U=Tih1*Zq%8?xZ#L}nl0R9H9{ z;_{-0o=}6zsIgI^nxto9PP8+mh&&t5&Z(6$S^_Y}sD(GOe?}*la+DLtcAH&C^`#%= z0sOwTto-iLe?DdDjNVeVFR(4R-I`QzgxD=@^peAGD2mDuzN}l!&+g<#L)t2nS}hCC zrTL}v0GGY4HA3N4V}3n=*NlT3lsuv#vg`_GerN9JZBIhW6ekCqMGP}mKs+85V2J3cX=;Yt?QdPN!Ua}m(Qw;#JdbUM$73UjVq~nyoT(=q zQQOqirc2qKLufCs$42WK{zE?_n`-s_s_vUe`_*JizTd{)$SuyGJOqc>7BZ5bl|`7e9a_F*>qxCKX9q_Cx4BO$@6#U>5iB za!fCHv_KyJ2^&3U+TpaUDu@eS6CFdLoq0jsbPZl!v@xbRk$NB+X9Op+BKhPFQk0}! z9Lwe#iqKW#caf1sGg&9KPT~v63M#d@>mk+@bzUU`s+BnL>MiG+tZ$(T$H;YEtT?&N zqdl9(ky--l{NP-iKOXf7ZoAz?j{#2pEa2X<{V__=hmUIJ$j#wNnOv&;<$Ymu@S^X; zUK#@ymvA_DivvInuLBS_9P$$AC-Yefs?mfxk`WON)Lj6h!X?Ynl!(c{f&gXkZo(mj z22+V0YBu~R@h!eUa7q|XHxsDFSl|VUt&nq9xNwFaxjA7p4o=osBf2X|Jldilxw*Gm z$kY+HQo9%EA*CFhs-2qO*gKa@B><09D)LG#0@s9TAS0QStxS7Fji<7bH}04WSJh2( zt0*Ve2uwt5E)x^0no$dXUR7285X+TW%f-YM6aIx4ost7k;pp`lDlSe|IS4N%OcSx< zGQ;LQ`0ltc0nwXnwoIz9VC=I&!S;G7hGxm&5!; zRlm4%@czWqI}&g%4{>bxEIt~gCo6l0`>Wo{QzLeEZp^W)!hmA)s$gkqCSRNx$QmKx zcOl0{Y-B}5$%ju;B3E{+?oL-B13E&n+<=jXrl_VlDZXxdKpV)$hSugrk_}(RO*GA2 zp5R?yf5A3}h)BuGUefJIn`heKf?`jC6k%Nm9;4L5%;GM4$$|}48(_XSbXX^Mb5=Y;%DTP04 zV;V;fQ}cZDbrTD$ti;%Bnb*2e9zrQ(298-Y5#84!UQtA;TvXC4^CM9j%Zi=@Ww9kY zlGsNKAeJ)M?8#mfEwgR4jG{EMK`acm4V3yW?04-mZ4yQGjpt*#{YhezgdttZcE7Yt zTMdiA643`)9hBh(Yl)@|>_QAVyRe~725SO3=4$+Yk)uVvzs+~Z)&+@!5R;Z-7%#xL z6}x-z3dkw%aA)Q-8s&5E=I$&QE*BN(>iH{_VkUl8#c2nUj!ZXMLsvJk*YAJ3alu6= z(^uydHc4rjxIQsfX21alxC~0LJXv`nl54iOexnI|D3>M_!B`R>aB=C*gPDk+ja(x9 zul6551kmJpP9M{9AhixsdZ$fp!XQC+$&bMfv$a&**m|*<34;{F1+`wdKWlMFOH}3! z+pLIo--qlFJHuR)jt>tHIar}b8OPM5RSB>TFQ`>!H)+5H)14QbF|0Kjy7$%WUQ+}l zWXjNnhq2$wPTe7A${W>09p9DP%>7|A>Jd2}oon)GAAsCn`pAZ7QQEmx8;5&Di54f# zUHl=;$0Qj|o-A%WDB8jv{>K)BaWL)+h*$LQg|S1+Fpv{kYF)*V_uq)!pf$0H5)=B% zR@MyrI&~Do@e_xIAN7PE#ZJ8bEm(pFa5ezv9eM2P2~4dTi=Ua!M-_iJ+W=(OC}DpZ z3!)h`hnP%vg;rBT3F`q8_ca`hCn?^QY!Kagm!*>%{!4AW&?Zmzbhep9Rx@T3JSIpN zoV8K!9hU_!Vsm48QC2p0xx`Lkh6$49guU+m)whAphU&6iRQ(>aj09xk`x~dx(6jow z+2~hKsxaFZR4TZ-2g`-!-A|!m-t#v#E9Y14X%J~xE1MhyasE!z#Y2d!o|dbjTV0Nb zoM^(yMlJz5zbGfB$Qo|?^1Krn!p%hJK4R-5%d;)~>Y@%VcKWg1@bAQ+fuXB$UXz1_ zTm^bmPqu8#mHL~2m@YgJRlYUTW)V?-HAHYxXlN_)l4dx=fH)^mFPD8uz)L!y|5S{` z%Ww4Vqx<{1!6|YXmmTum;=yq$HN^JB`1qkTaxcay(Fw|(o*!rQ#ljBA%!zW#`fS@+ zq>Sm~giHp017wCb#Iqp4@VpN-t=OwI6>9&nTIlHWJ{pEfR;oEf%KQ@Gd7zSJpoQEB z4j>Q#g@Bw%zosq3Y+CW|!w4`ghKO)t;Ln#(Mpl5BoRqLToxio#?w`p|})j(&>U7YM!|Ih#a**tnXwws(zCsPiyIl$m5B!5_yIAS_E zFanztKz&GQj%xBh%A?}`IIG$15Dyx0R%oQi;}E3te?5LYc#W_$FMy>0K|sF0z%$dY z=g-=8?ac8bN*g~WTZMt39}H2a5JKT}@k4YFjhIYO3G)=s?oxvpOvK>T_Kl^e+unUp zdMp!tQ&x|Zzy9sPDJajcuNdUG?LW5O@|8YXW`-G+1zV3oVlSD>o;~bE-il=pC}1k1 zl+7Y&;@7lENsI#H{H1Fa3#MLKZ+o;&h>s__q57t^ zhB>0Z<{XS3=dl*Y^vM>6t*G}5Wy^?wP;k8s&b72X_MVM(zTUr{&!1;W2NIv$%b#*^ z%Uz`Z#IJrMnhi>#Ah0i>y7zRNPUvDoh)y9m?+dhNQCk{E?mFby#D-q+JAUEkD4mn% zj{|TunqDM(#iIkS-C~vFHZx3tTHkVUJ5r%vYfcT){ZaG$Aj=zUx?D&Z=MiDi*Yt`tv>Xo)5=7Q`&$x{oBuPC( zom;gc06oT5$gmtN(I{1-5t*~2CR-J%k5p)K_Vx8Q43hwsG}m2}=&eOx(%#6)rz#w} zh5azvxwgoMN<=pcrKe4*kM`*z9^JM zL4)RUZ0JYxY9=HT#wuq}zG#M9i^obRj@KClc*PL7W_9A!h)OMy9~H|Yey4!MshEQi z4|<~j#n%GKOMk*;vf|6v8K`dI4;TOWTl&t6*mX}}@+4#!Q|VX8-fSKdO-F||Plh`T zq>~HatVJBIV#Iqf_&BUMkRdI{&T(g&2Q$x$mf&XpO#Y%!!n<6&RE09y;)>n@b#5hl zIPH=L9z}{KBl7=Nr`Uo zs^N?m$0GXVWgA{C$)U02oUqo~OKyti7l>UxM#7+U+CuBVg84bS*q~tALzaUlL8oqG zR+AhqIn0ir-n1R~A>D*l8h(JQ;`10aX&h)WZ9k3Z{_T&Ru6{O8XZyt4eq{?B6Vy@y z;dy#K>ELQla^$a@OhO|SD#i-j)S1o94~kXfEyl=(T^ig+r393YKO7RvtACdt``#tC zO>|)V`%Kg!6-R#%`cX;^ZU6r5z$pGOGm_X2UEDKXqfPjtbv*S|YC$*x7r3yP z98b~~XEXlQ+VM2i!w6#BbP=--MQUtrb#8ly=EMUbgb;^GU?jW%463Rzmy<8#7mcyI zW8YT!C42s;aOR!gOVt}SV9Xa_*F3AWkP_c-=&HZQ%JU9zPh& z{9f{g;I|C2^Lg*P;Mz@PWLv!8j*hl(!i;7xpFLf*547#lyxy8Or5r4Apqy)^JF z={TfN6>f0ZC4vx}cRK~&p|p+-GX^q5S+tZ|;EDLTkM9~SZE;ie`PBuEZi;+C04cAGykRVt;R?QUKn8<*ZLEG^Q5qIE ztwDdvtt!b%^Il2xRo{B_S>b;0_dpx8T6R z!TXa5$C}9P#Ik?n>DgnTMe+}WGIF%EH_q%tANdM3RBC49k|DqxdZ4u3LWT-@db3;q z@f6kk2EfC{4!=NepPX!>3-?@CKlVrIHjzi+-V-b_1cSS;*apCAtr$pc{h(lLMWzTI zADxyVh)ztS2=sApWcuIj*UmgoWxFcoX_1RE;+Lw?P95=Ts(hH{f0E`ge$Yg>K=FT4xM4?2=1-^wqzD%}g9JK}# z^F{{2Bsh;+Zkr$`B~jnUnq``~%j_z+;YeA)ACi2C2S$=J* zPcjuk4u%s?REsO|TLug)^cY5FbdInrO#9v4O{>|%HaqS}I(XPu#83E~>v?>69XeW8 zrQ%R-XTdfOlV4x9h-I93URPs?Zxw=Z*7iGkp`nI)2Uyd@4~*WVFUC5!s(9D?{#%V` zG}Hgb+rb655z1gE^M;S|w-lAQqJQ3W8MCV2ViTccrfYbbEM*kNfhJ7s(T^`1jPkLS z2Te#OHy2?okOLhw47uA@_tc!roU=EC%z(onlrT#DydEzoDwoLKOq&c4Xke~pS{ z@vLpBAH-m1@X+pn_}L8-qpu11$oqOn7Q1KnwjCUP(hhaNY+woR37+-c0is?(3zU$T z#Q}gQEY0lxC`Rv)wyt=D>oh!kQpAYmL}%xzD0K=ChH<;Y+g(L@gmeuE%e#3sU3G{} zb~KP&{ylgkJqgC9SdsY8edqrkByH(7FcRumk*OUT-q1UuL%;D^*gcRMS!RTupQ!Vh zJO9VX1Jtf6U(-S~9b1;=zWId#v}lXHMCG00Yc+DLNb*R7G>tD~#T8(X1L>wpm^po- z!=gR-X&^|jd3|~3J{of4mW@^*VhxWL4IR3tgmMQlPs3wf5pA(QmIqw;PzMrP(3-}B zbJ&Gn4N!+Hrn>DCAl{4YMvI|$g%8-vNMR8ZAhrc>G%BUc`S{4r4lv48B+V;IFby6Y zCs5RkM2+eiUKFE~Lr2zi<51waIS@nfR+->U`~43ao7h8NA5np-DrLR0W%C8f&pOJF4SEpNEqEG{wM*Sfcsmts$t0|B#f5Ez|=+ zGMt>*Go>@}MW=3%wMRpE^lNl6hNzWfx439f-Jo^F&?|7w!}&llgxBrX9R+$N`tO{X3u7p)FT*Kj|(&3YoP-f2W zAjxKd-l4k{tyT#Xouy^KOLty1fZ<4Zr)U*ecL1}2s*Ew^sT+X4YW^{3twFEDwy>E5 zl!A;B@>7ptu)ffXL`B1m`J54QF*mNUEi5ZbbI8mWmgdpSt78~B2 z-=|}P9giZ8n49wY$cl)aq`U3>zb-QnrJNx3p2p#alc8O?1`14GbUm&LXv-MtU6V89 z%QXp_x`tZUB)meb*2Z_zgwoT6I@spb0q z*-(RWG%93DX6_9G`B!y7k=!YpB-x?mGywV{7`pb6QZo6I4b7D#K!jS?Sq9K(12X0>>Q0ha%t{R8ix2Puqx)B%1D z5=vi(Hf)5*Z-{xKAL=CBhgdW<4R~9MKXIETW77$_v-2 z+th-0Va4I}#oza&-@MdbE+z?*=Qb1^db7~Ae9mKX4<@oa@ylJt=xIKp{{H#13)KmH zsl{wOi?kClUxD2jp_%Wed>6>|)~Tu?S#v0-70=m9P8 z5CGgvlNJd|==M7uO?t2?Irj%_SaKkDfX>4d`v*E{!=%K(A1Vr)WPBI%?Tv`ih-SFq zO>|`lBQ19W8e00vhq?s)11$Q($ke$`CsQETqd^=22$^KOq_({thj1mUI@&qsPxOpY zx#|D5pEdS((qL~AtX&(F;4%}6G4(;Ac(H2fWg#5UwXVH)rj;oG0w}My37oBAN>bs~n=N&D z>J9<%fw|CJH3WW2iAE93kg^0rmK1>u_^NjQPNks2x3F^$=`yoV_woptauFpQpzGh| zxa+pZol0kYpx33yh|tj(8U!0!nBpTQ)e>2tKB?POokKAfN)vQ}(frgWCXeV~pot|} zFhqAPaJ(ow3nl#08rS)*{YaTPpY2g?E2;HsIW||p#Ny}^vxz>U*q9+P!D@P|LkirMe zx|vSDQ{W|0}LA|lo%r6swk95=deq(!B-p-Q|22woxDr~4+cIe*KXP$ z_y%w{j*Z~uhle+;Ti)?@`IGSAkaDyDC+Y660X(2HTw}ISx_Fr&fEPU; zUbTgbPFUN)*!absFowGMyW`Lbj#;v0IpHcHdj9QckG~iuCdSPZms4Bc-5KzvAZDS5c#>@>-G;4Ey&IYT zmCniMK~5o+p+y>kg1g??(;`*;ihZHThNrZ1ZB$1GV&hXa*%{Ey)A$6fYz=%g5gP^e zkqw!uu71uhrjIJ|O-&?2T_AbD;qzLBeB9eJRer@hf^{@0H=(8;z-=Q;;=4vt!j1sN zx)B`9sRhMYo^WwlV8YXs9N;ah_`ktkqV`6{3f)`J&uTD%RRn!0ioJS)t7aaNzi?jh z$74GHH!@|BNqoU)!%e;Ai_rJU6LqWGkLsOE!zVdHcB$T8mB>+aO-oUV8PVrUys4Y( z;Bhf*O2HLIQ+P_#34Z0GGwh2XmiDFm)uS@ZgV=>%hgz4Rm1s;(#-rp$9~M-i$dy>j zNg%54F+3Ux5lIn}Q@!v@t>DCs$wEztp7+DhI30JxTyS8DmM|M4A>bhvA$`qsoSP_- zp%4bNx9JTbN&q340(<57$GZp1`|FmuN~?<-9hBHK zt`@*BWoGYa({QI)%MhRnDe)&76bRLNRugLb(eA6ZVY%XzO|Rd)#YqtuAz!GtMGVkU zkjjlbR389!#CwsssU_lS(0SsPk7lYAH`UlMM&7g7Nfou16at>Vg)%fPGmj(47-XpHT zeRp~@p_$C3{tSkh1F`O^;Gy%s7%_$Z?Uq(EVhaUd!8w{tF$=UYB_F!5+zM!GpJ+e{ z6-rbdW3p&oo|OVJCigwGUzDj7yW?sw>4Ft#`t}%Rk74Ur7RE%wyr*$)Qshu3C@^_G@&Qj1ZZARXCAa-Su@jZaqMFXsYp$)! zbsCeWX$^H-4@rOFlyeBv)~%2cc25uxm)W}Y<54()f=N~(Nz0O3C$o7@U!u6lNrQTb z$h$Yy9Eyz_o8Mqa3@EB1WuvXdN7B;vOY8@LH=e5t9#PWaue%KqQXjlxWXd~U^ogJoBVXOsN zw`ZVXd{dusMe%X2w!H^{1Jjky|K9^9C;$TGQ;2nE@8XEb z%L!xB*du>Q6k8yhj$?7!XmTi`<$10OObOtaH&(<&bUa6}D8ft)F4;Fn0clYZqwT>5 z;a40y-C)yR129yDq5&4yB_GpFt|~Z~$_(g=4;F(bTe5B%3TRmGgLq-b{MNj48gaK+ zha|^|Y`+{qw5dW{zQxg_x1YV-C(jSyR3*wNcQ&wr?x4X+DqAQb38_zQ$m0u3Ti6y^!!jPLNtQ{* z>qW*)t;L?qY73Gk9!>xD+?u!jhghD{*x7;=9xH4nX|_q1%%^AaG8 zuOS&BPPI_5cG!c^5$*e8?K3l#6yBsU8*3ThT}=t|BxaRv{JOjQ22ytq6}}$>`>o&l zB_>?HE^aFGYk1=uCP^O<`czf|b0-O6onYuaIQ=G=#iD@_2$~KT7abbWadEl540rZzu)QI#jZQ;v0n%2rK1nvGNN=49UV`+I%`1v~Myi)p;lra3GOeaOw|-F0u4v z?&+IOXS$_^r5wW8N*PKKMTXHWQsKeZBN+&+(hmsE!3WJ#hhNTLRz2aOM}2QMga7&e z{!6SJDfx z5T8YYgB<)e8&v`gZDO4VDlpSOadcoPqwCs0!yBwc7Iiyh=m0qkP0G9`RT|e5;R$yX zWJEGEG7DKVOl8=jU@t8{1CM2|*hs?v^tn4RKo>cZ?V7KCZ;PuquiXaRWX7akX72Zjc~i##YL zhqIXtqf&_RZC=CLgpk!u0QWP+#ljschDvxE3woQ>wPz#ve~6P^$+$9J-C3VB|zi3WQUh0QvHIbK>cP*L%C24Sq+^ zyZ`A?b*hfO1yM9Ae~V-(j0JPM2^Q;gJW>28VIT`dcTt&KR1v48b)wo8U9p%tLJ=XG zRE3ngav9AXH*!;Q=y|x)n{Zp$RYd{@3y%>cX|%;YEqFy1eP=;X&s9;fJInbeJ(0pg z{~TLr`0P{$y45!1LV;BW3R&<#1&IhXVDNc7b<3$BIz!|}Tn!At*8Vb;x$j6Xm` zi2nCbEDP6qKbhFtaJmIaPYu~PFu}x&q95HkI!1#=XFsoIbc2dUFQ{>dBg~b}+(DCK zrHo1v=J0>*2hcr%uX>et`ark>sT1`JQ;ysaHXPb_TjR!y+$K9GdK{Rl0U?gx)kAq?GItHSESrwFMb3J@kesO;= z?T`)5VL&GGW}Zyp^D^b9t&gUtR6DUx1drE|^1Uqgs{|q9(ABJw&-{SQg1~4ZDs>h_ z?|2l)lRPp_Vuu^g>qvqK`&HyA%m7TZmyBP0_(@Qzos=iFrnRY?n%2)aJRs$QXn+@# z(LQ;= z(@P)DGYrxl`?^kzL(2VP^(Ofl?Q{?K$41>f0Xx0cr?9~T*c61*qq+8d{yYY!o}i#x z%|ub1N6lb|%?8kd&I2AK5*C|nH(g; za-o1+4NK0>>S&}w_Bmin^1%WJR$@7VA{63z>O-g-z5jwQm330fT)X$i(!*I&RX9MF zG4#Ye+ZXZk_HCzA|E$SrXhq~>uDwf6kG0aD38wnI_9e>cb!`5_5>n1#}6HF&f4rZMD)ZmPs^C`)@+s#N?WPB?#$8X%v!ns6J-)o!^&R039+-xS|j zJRdyQ*Sx?TwIQt~;>K z2wQ~Mf{NaNS_m)n7cI<@`=i9GC`G(0E2-Oe z=~CRtvUq}2u$ZoSoLXEpF_uco!;F7BHf9rp_;nBxt}VYO?9uS%61*|qaV1c$BagKE z?Vu4Ic(_8c+b^KwaO2ii3X88wTxPjUUw6c`3c<)70_;C^{=(zz>?JH3UAhSw;Y_Y9 zI-KtxR7)%Y6m<^H+d5lKe%Y2(-KeW%lV5*gHlsj%L5@+kl!ibt~UA>s)Vj##-E)i z`mEazY{cV5g8H|<7--l@u2?Ee#TJZzc)Zu=3q2*3uV<%xVOnR_~!3$z!rOptT+tHE;0T5cW=ELFOlRmzK`g+%~Hs68DO=i?*3>nVouV zBFRx@0(PGyUjHg8&-*_`TWQt!&ZQ)HLra380Oyihja6P;qc3}TQ z$c_sE1V}G7@Sj(!7N#`Q0oKag@QNSLKF0XI6P+|Xhd0VW$Ib$qVm$l4yA$l8gKv&GQJbeIe0Mi# zoV;67f@CaIz*7ueXC_I#rRu3cu&iZ%N0zqF5X*7!gtZFF&p=<%_0C8nHFsh_%0EEm zc=I@yDv+ACwE*S|kDwujTx>HjHKS7@W|3*JW-SSle^`~ngUk>jdv5+A?hRTIUv-`h za{@)Bgas}8P>j4M-PH&&p`~MR5H@}P)}Duqf!U6Aa9!um+afhIj)lX@fpqo{PM&!W ze!C@9Ou0C`1@m!ZJzy>7IvARPo5x-BTE?ov8jbyE&8YZN)>~nJR|z>W%|AEO^!L`)>3*W4g_s7jyMlENX}v&ki&dv70wzcLttK#8qhsSDj;m zki&2J8*Q->jptcY(T)jeAR3H~yG;NatP-xRCF935R2-1?QWofyPC6(8W$+sDPwWwF z-YF{AAG`s9WDv97v1iKYJ9E9%4+_1Ll+CT2-QZXE-ss}q_WU~U(B28d{i^rvr#7W7 zlMU_BY%25Bo;y6)sJ67qd2#ZbBCj&*ZEW-_Z(76_Y553_7XiooSonVVrdrs$=Nqy; zgOh6Rs)v$AdL@{a)i}|>oNOAkgJ6S2f>;e1gf$B$>KY@CJopr_;yNL68KT)(EGnZM z88G)-xgGR`^j=@1FOGAibPpI6DFm#NEuwuE=L44zWM9_hs70A5S-wP(gEB{+Up{RF zH7^F@wbFNNzD)tU*V3_sarZC%SUHr1S;bZn2XiAFd%_Qqkg{Skk)_+Bw|viZK6+h1 zSy3$Ud$FhAMGF@-?0fHwLBE#+OWyt9P`=l$t>6&+eBAcRt@L&3)vtEq6Ws*eq8o-d z<!k&Z1gtWj z#yS$FERLY4upP+*Qxg387 zlpP!1C^HlCjMb7$?Z?B0<$Z6DM2Ag#_F%R^)fuRWk9m3`%Ovj=Ci_;pt^1D$n|t#6 zY^;y&HnuVxXvdRlttJ^EhLBvAEk{dFulil|^-7N&nUnx?n@O9uKh%FgJxXG%VV+v# z;83u>m)vY1c-$1tLBUHiX%CCDo)ot2l81R@U!Mt%9y5;0iVl!iAL&sob|OKvxpjta z38xDy-_{@X(+ZGZlUaSnz=N&g^Xq(zeSiGu;^a>Hq{+d#iEWZXsbfx}BX>Q?vpk7_ zK>3ym29jnKDEAV4FYVH7#EH_KC$$4~j<1UDi~oFs*oA{L>ntEv%PeXb?u_*`m?1`C zhSc4Gnd9S5L^J5&^MEN2Ssmnk)o6)S2IA9zgO5BPoa~bI4}%k!YLRG7oXy{E2|9EZ zH5p%OoFUCo&1jDrMZW(BD8InAspJe=mY5q_nc_%tAO;MhYPFiCVAX{(hVoH0_J&ul zd@*0cR)5lOwk#xmqc)Ff$Xv0*%5C`b4k6L<;T{z(1uy>ecLQ!i!~Fi6{xV1izp>r@ zl`HVdt@L*Qgm5`~T8^{cHawk%G2rOe`g4HIIKp@iUVnod*eQ(u#C0Dnq|@t~INwQ9 zx+!VhU5f$28^p?Y@H~ZVw2g^eF6-Lumq2|!R;+v=j>Zb+w{=eV$J@Gh7p zqYX5Z{9Ew27OJ0Ui{b^$1L~nFhR@LHhT{nG5?%RZyfl4hc=f}uR(`xf2;IMq&UTl> z*|O8rZC_;7oOq>SqyotNoKxDG_IyIA5+F#-15|?>hBw?WkZ?=01T?1HiyM*xa;DY3 z@6SUb;91?zG@j;E9~F9b6+Wk)cUbXY{1K~`ltx*JWNUo2%Otn(nmL_cy^`nQMX;u>1Os# z*+&fnWoO>B0LG@Uz+&?C;lL2q2ViFM;E+X{W^sYc>&|e%^P3dt0qR9$wg>8(a0F*B z;7;CsQb>iwJH=+E?VzXu`6%xM6>=guEi6V9Tfvboa{1sbkSju)Y1PoGtOMbXKVcpG z%9C@ZJd8Rj6x$A-Y;{wgf;P#)MY2#bz6X!W!<6fFY06?RGvuzpu-x?auII%bS?*Z~ zB_`mF(u^u_J^DK>{^Q4EXFth}#twZ2^xaAANLbA! zx%6-l)4iC*l(>>ob7KgzSeVp?I3*{d#ZeB29{4cQ~ z2SUZNdbuTvTZ zgvxI+j5tG_@FGqdS{c+Sm>a?pLS^nuGSfWr>?TL4vpN0pD zCF%KbcA5N~jq0XyR!rHF!Do1EUZ&t4h&$SBqy4Qpo;vYk%J9$&&J_v94CfKIV%MyI zGw{`X+OJqA!$>tX?Y*86?kGTGgcZr=5jWC-wkO9V+B6FD~$BlpenGnD8v*VogZ1;~F64b7_hAOiaGjm{| zqv`d*C66gb7R$I`fVg`cAx8Fx71KSo~k1s#*ypQJ^uK7(fAC6W{TSGtKCb@B`#Roj^OoI~L zHoq3_zrz|@`tTdpeO=!pM2ZrC-L0wX2mrWFELg_JUZvZ<%MgkOg{~e!HDT}BGsp3z zq?<~(abqjvc}x~R1YOu-ngvVra|nx=Fj}8F7%_~cmWxeeCXUbY-K4R6u2sLveQZM+>ggJdTQS{fK`1ctTM;Y zPsGm-7NQtpT~hg4V(^k#YMSqtjKytjFvlVCa;{r5j$WkbYF%V4?eBX*@E7r5FP-W`A~VdQ!C z0KWwIQ$u*PQq5(@yOND#k^_ek#s839Fd7-6)X+@VG}iP1OBYuNT1H_2JNfSoai4=r zs%GN$-=?GP?0t6D+feujc$xes)jV^bav+X!sr1%!uul{UL!6N$NsU#_+f)hOR zjT#R^qFSk`V8h_6?@p(Y_f(_$JQ!XQ7cJKDen6go!&!)hAx`zP$zw8buzegvPUTeK zub>%pD-qk007om7SJ6Gi{8%E&v~yBt2ab>9d0w}@K6+&8qnX`y-%Sp&<2VSQR~C&w zbRrz9blM#>Za5!xnn?-YA&f9-bKEj@H}SMpWT;b+WM}_zwfSO(jrR{Ts_l>~QAiTUs3da@ozX5lRs`0~yPByaML(pev%XNlGM&u0h1+YPv z2Q-!I04$INF4;ghX{2ZNz8>4*`6$tfEMSAxQvPwH(GV6rh@d{$B)(UM{!^VXzI*9o?8L{-cQWg$-=Chl z+RwNmrhP;u2~=lHY-(7ou%7WvoCX=J+l)vFIak$W7-?!Bbt<179!dPfaSPh2`KXLc zxxfT_%{rhQ!N@|V(**nrKcN|bbL59kLy3E5HdgTt$}Nrb((+( z2M_jF)PQfGgyw_&jZx5gVD~@%*S{EQ;w0Mh?)*55pT5-ER;u1w#TuC5*1Da`lTy3c zfI(Q!tt#6}imbk7x4pY263DZ0&{MCW_kFKFs;X{yFiD=?)WL7VJX$C%yte4p(kZ-A z_DiYlSbpIa9x|U9+Gszd(@-GA@nipy1`PJwd;R(I**N}%d@K*jO9A$jqecNb+prgV z=c_$?)L8YU7US+CFGvU2)y60i%LWqDDQ**?1u9_V#(8xA_4UQnAo@k-SV<~H)}YRa z92i?~-p+7z561BGAqX`oOfh;j1xS!T60(bIH6}L)ZI`yNx)E6>7m-aGS@9LxEU6~c zM%(fH63Ya-(V)9hQ)rojLg4vzcJyE8AN=)Wm%Rxi0zmOy)E-lNgRRI$x>?*UzhkEH ztpJsfmW^&j)tC#2WI_pUZGZkne{423f}nd>Nl}Z{s3A?oNSeVYK>enXM#Ot)h(q#+ ziJUVBgI>2nip?ovVumcaj7q9_ib}`?&1YDN1k-FBF%1cMy9UcS1c@ZVWM5gLj}C-m z3O0AKj?9{niH($iN=~t@Z7La!9@fH-7hQ!C1L$KO-3^qI27mrM|8?$$e?1-)E_bkt z(LqKWL_F`W|0P;13GAkp9FFICbDr!u$-CV76FX%|ef3xZwz^e|WBRS&133f^RAk$s zT5VvVtECLOJYFo9l0;4&don z<=!c(*^QAgP+Tw~;L{GP;h-uR5R1;}K&)}{I`%fFhRA=x5>Ysz8B@D;^ov7avTgW% zRrEjUXWWC5Y<|)z;)oIsUsg-uo?goztqdA- zHFLd93n1cnv*>ow@%Q|qYhEqD9YY7HnHjL49$jd3BO*NzM~Bx)|B14X@U)FX$>w}^ zT*1|g5=YJW*nRqZ;p3uC80HbLlc1+ywDH0Oi#(B>4ve9IjYcn4h!AR~)1?miBh+!( zmfxgD{`tlY-y+{Ffxef#(6mHgh5F%w|BhQr#`a3|B!NCLdtaA zEC20A1w7j{p64c2S(}BzOV`>T%U!gYoKm1-zm$g=9ID$i+;fIeAU%Onu9`N4rb-wd zB7fq{iy=0~)0U$i9cDt)UQJNL4mn6QN|J4;JhTSFX5bek8?G@=@`D`!aq&RpzunW- zWxGbg%Q{c;2*spdN||et^L__4I3A@}OEjL-7>2WKI(x>m8T{L}JoeA%QQGb2*)*MR zP8i&#g040bf^72wC*rWfi_DBM`KS!!2Z-rbp_c{>Tu-$dtug~9&?45MXK4qLK3u5I zJNW4pt9kLW^vYifo#AMt>SLh~CC(8BgA~x}M_G9ot>Sp5xO3x!|9IlTSuKX4MARZ+oql^eG>;zLClD2(bP;J#_FP`E8+2$XBfNnfk~*k(se1LB zm$XbhCG|?QuxQ7&?K>0p`5U2HUYA_yzX41xI+8$p8>2;TJv{GNonajK9|qk0#^?1D zti_Se?=-;boHgCB*Vd3%l>1e4>Y)o}W@d`pY6t!Jy5suwDMRtXQpuCUJSOlfeTczI z87_Y2$)Dpi;nS%9T|ke_u%;nT&FwU_$uu(#*FfCWHJUi0lfFK%NV@Y|AinYrLj`;Z z1Mz{~%49!oi+C%2+L6P_rE96g@wyCbhMozbl6twDd}x;s?ZI)C5q$sD%w9 z1j%INQztRD@*ohsL8jC`t1z2{y44ax3oO>pbc_h09|YJ+c%;h{pV3KmaUKu4;5Z(SEtM?770I)%pEK(JIGl10+7e8zDcwpI1xO2Ev(+6MD0*WR?Ps^fRIj)-T?Zt zta#S?y}fHs8=xnaUNGJuXL`0OYKM|W0ir`7YONKKK?o_4#X`ak-_ucLdK`4wKn7+V z_~wR@B(>CUH7&>$E9TRJC*K-y;wGH6I120xJXQ0l6|NCo&hj)&(S&VZ3A#6paT;$Q zh;h7Kwe4+Ta+J9$CCoU$oSc|XiFs@getq1`W~+!8bf+K(sI>+@j}`~*wwI%F;miO- zRQNq)qW|Yc@e=!1SwsbSLurb?bm~rvF54Ib-!$i~|8pGA&SNKkQTuv0NefnS4Unt= zV0Z}Os=mY}-wefnX`I%iLK;S^fJ|15usGs~3d2m6iJ1x|@(D)ep}MR?94SRIFy8ai zemCL~i$R;_h?82kgW5|)4Tdc`1@Eon^Xp6O?1&RldZ4@uYem$I?dQ*@Bdc&wXUKz< zI$3eZW9jr0VI<22JJ{@rbYJv=R%jNDye?#s)qEbb1de{)ZSuN3DdBrn512-ah#dTJ z@s0U+UW6&&q^t)>pMhp0s<|p%qwO**ZO2wzP!m5y#O8iUf9y1KeR%(OZ4PY-fqCh@ z=v(o83;Zh#~7rWfUgLQf^u>_*;}~m(h?togd6BSQtJ( zz+D20J-;xYU-+7exd!5{he20cNtjzHmWWt>O_qGs6Yf@IDe?rA_ujU)Y0!4_07~!6 zJxQT^{TDo&ku_IT&k4LN#rQzIT=uaC}tl73K1&(?65b{ji! zlLajhKBTbS@v^(g!Gtmcmgvu5{t(CJkWQM+Ay`dJ$#6kAsHvbZq}V{DhLra%v5{PT>(YrMy_G@ZXzmYVy#p{=&ggv1p8BF@+&~7){O+A4sk5*K zhX=8tNIGbH+)Au3Q{q-iFDg(!Dm4n++86~D#IiJcjTXFluRWS$9&LO-Fg{X4#|AH+ zX>$}}zAS4K8cbMC{I)Z>`hLxi$m(!Yre6I&8)Ud-fyNy9?>3F_CcJtH;^s>K9>;laogGPg!Y`}^nf-yd5sV-P3Axv7N)bWk z5vsOlVg#%pY~fgYhFhm9eLat7`?9Vnol1uA$8L3zcmk7U!0gc-xh;2SzYhCLy34Ks z#1$2%V^QRrP>lq9vH#x8pSx`f-8$glkgdwJKFy^t?>wdqCKn=FA^kReJr60}^ozve zb|}d~@~9m!0Yyb={WM0^&$SmbCvAyT8bm=zfEDHrG^TMLO?8zAwo8##!epUK&g2plE~uTt%h`ADL~S z=St|RAooZVjL3#|$cuC-C1=8Ibqn8^gd%@+nZ{~C2q zQ*@ud%~p9hC}Mfr44R}RZ4pC*6{-vuI;@uk^%$5uVqG}c(?oDA;p4-jFv}4`U9I`B6B=wQx}&07}*n77|P1n;SG|^v-z}J`PSykklaOr^_7mF814;riTH4 zi$Xi`)wF(+-5+&>VVI{C!8Z>W2TPl1Vof2FyIICYJ`s!%Rz4s(2?NOE;q}4)#yO@-S;Hfuqdgfq z4U(}6CHVPTp1G4%&9Oo&q=3IbvF}TG1rcJ=IS6Dr(ncD}O@ zp2skG{aMM!Z0q(CoRE5i5l`Pd?Ch?fJjNn%Qhe%)1s~!#w}TNgT-mjLCy(f^KO7n< z#QS3WT9T#NnCP}ZYpmg#{qiPS1UK+!f$^P!3F?CJX1^jc=NLh`>QrB$5Qs|##k(Vt z|AZjng-ZF8JVP8crkS>osT7m5Ns2RprOhqI$|erk)94QF5U^@q@zS~qArp!?U>+&E z6;|CKK*)18Lznb95{ILC7))@3nuGqBX(Q6HSdrEk+Yt|o3UP-q8eBun%%H$ugIr%0 zxa+o87_glD(0FVLZM;DPtI$ZluE)l-m;74q_g*6jJ}ltv%X8F)TUTz3)88Kt-9GI* zcQW}$<^KKr`SYxr>%LF-m5)oNhyd5Gg7t3{zmjL?HR{IYNO#AwSW#r|OMk5`qXYlJRFli&lP zms|^FoR4$=sJ#n5qB;pkCh|7wL7A*Dwyu_ln%5u>TtwWZ5U``lxZ=S~U4Dbkj^>*Q zShK@ii>I)}5%xFSAG%%atB^_cfW4!U2r*?mjCSh z&&r&UO}tD6-6D2(<9SD)Afe1LNxz)oQT8R{Kd04hdcbycc}0b&=tL{Mx?$Vb(Y99i z2hD6XpeA7jJ`ETBwo_z48?)kVWFUN0BkP#@tY&TvvVLP8s)8=TIu zYcAhsYa_9svmH(eq`0VzUPx-^Jd{bSG3=Y)Yv$hJul*PL=Ua*P5Y^X%h7jIc%P9jE zg62AVqTryuNXT}2gl5^H1TlyknV1)1aBd;ujHDkiw#;6EJ~$LFV9JhbkCsR(1w{X%svJY!CpJXvgd2i}9wU9Gg&y`2aLeAp;R+ z7!}RWmoZ2Y-<%c=`|3cjaR|wBDpJaUja4x%CVV}dx;$R*8$x$f%y;5A51k$mvDrnG z>ZI1;?HSs_D&!L}Wf!0WkLS-$gV}%3}Ndq_gG+L*jwtwb$gZuLokn+)|Y|R>hb6{*Wpn0;Gbx}>%QjOh9>L?K@EOjzlR7D z?ToLfU^*m)+E_7#A#jb}y+(CxjMmH}{xaf$bKf}1{uf4@F!J(dbd&M~3XbJ4l}(xI z9b&q~UxA!)Yv;COpU`Ki28eZj4~qk!R24XSjL1_$Fr+K6pOuzU(jX$(E4ORYDCev2 zWf%?yzW9xsera9dC_*h`V?g7tij62eLfxK;PJr{n?+lsorBXl>tRUjwvvz9l9GS6r6JR*;P@5;z98mXdOYa%3CO^UanF~F4pth97 z)+!Opm8vH+b3hi!R?Qn&7z1JnHdx{KOjau|1C!VR-^@s+uB;5nZ{}i!N}ep1&F~s8 zan)bmQ%3U9YapMU>dHcv~^hqOF?{3tex zXQTDXCQ@oSTXIDtO!G7xknN+p!99EA$yc9rooDpE?`RT1KnVH3EOu_R1NJ1H-h^(V z1>xC*j_!*mhJ^rFPmg>(@P_NYFgm8hp3vzzV`6*>_2)&ypSS=jO-gu?ZPQmNn zndenyp=fobD9Y2?Yc_gXxjAJ0mBja9$ih`VyBCQgt}J+Cupz_|aqWk~VMU2X#sMoG zmY1N-Uk#1e6MH-%-ZzYVnI>v^wLDg+k|a}M(Q`^Btj} z9*Rn1@5e@RHF}qgvL)H?ZYv?w)wWLwPIMb(etqr|r)7hb2MFtsQ%3x1HIe|o$$|2H zMQdk=-%*rB>Vswf(VXu#N1yiEN$9@@2YgW6o4=)QAtEE?t) zg(9KIcZA_Yx|QU7@pX<@4tLP=S5C;}` z@K5&cB3kaRyH!}vQ2jYqHKqjzyW8!8pOPZ00=|?4WcA&Zy3fbaFF(=!WmIX5vJ5Epra@Me_@vr!+c*-NQ&9+ZSB|1Hu!2PAMg zio2*UEvwynJI(awodQ=Qc=Ub&AW7o+h~YqXm8qojXlPAcqA^;MqAQw*&I(wBeprk*QbJm^v`QNU>OP9&K&|Y!M zO9tYH@`K{ZaYhi!!r$(V#v9F11B0Et>4s4Wo}px)y7s0<8_3pCt=yPBTuk-Qy1|i? z$J``Nx|iIAo*WtnWCFafbrjVj;-t^7E&(`To(GrQsuy!deDA3#k$^jo)TC85aZx4! z1gr6u0_m(_M$a3eZmwUDUMk7GIIHyoHGy!L7YI&h++ir7d@#y>fRQR$Bt>G`ifx)* zVXK;VhMiJ-ibwP@bcA4|CZ*JZrqG&8EuAGSn*+R_m^y(5(8n#(n((q^d6V-qIZs7w zT8hc>6zc{LZfIuvG$1Xh@Retzs!0=ZWhKmo-=Dl;m7Wbn7{9@f2m&<#S8NJ-i*18> zD$B>1ysQR5SW*^8(KQIeW7bw$uoW&C%w?4U%N-ahe5D-{S@YmeQeoboRB8A;{LJ zHt3WyyGQp&xdt}{Vi4Y6cXQlXf>DT{ATjTszwfjhpF{wDgb*$spHF+FV0IdYvwpJ@ z{J7$WfBMzFA(;GK*X+N_Kd+d8?_Y`wj%LAEohyq7RN|fw7mt>r#2GgN)m{rE)S;B- z9QB|%4AjJNCC^sVS*uT=;l`Xn7*}zVykZqX7h%X0PgS&jFQ}|uUB`vfbsjh>yFpbf ztAZ0$D5d<4wS82Qtp7uh`CQu@VHL{=-b$&(vc69~5R6Kp0#PAyxeM=Ow;e_{Ca-M# zYQk!S`+Xud&-{Njrc}{scGf_DJ$ixm?2!|FiLu#xBlv7@6@@2hN;f|oeWHXHbxKIk z3U!51Vr0(+tZ_Q^#-n}1&K3Hns{SbSou0j+I}TnmxikkLxIlv|D1V&eg>~G%K+&&y zLV0pQgYn20k!RBT{y225qsB8yz^4Fy516DhIdgLrU%hVA?@T?7iW9^PZW{C1mOnp_ z-p1oN*&CdYFka0-$`YwQ5i%9{zi|97W)<6;<*__lHb|w^pG@wtl z&+Ga;j-6a;i@Ucvx@bAe0fzv|nJjXn<#)-_9>Zu^O*ToaCl#&L9{qoPQSsMMmNg`O z>)k0fILtSVi%=xO2LDvu7DIMRuw4ut2gpyMd+VejE4C-EFx`>pE^f7Aid$=X?RJ8x zeXOYcb89V{3=zq8w(#-{7>a5`!@cLqGWHw>6gv+pPSpXINNB$ ze_8+IyB*UlrSGCNA3pB~Vy~-H{7tX+H{UTK3@{yPcNf9ckD+$Lc`R+Jr+MYRVT+oq zWj_4w|6Mw~nbuX7)5DA38HxO{M_Rnp%+U>h?p)(JKS#ywI;Moiso)FKL@4S6qo)@H z%m*TxRLz57;h+_fc0L(58=6$;-evJw{KF0pO=f+QBB^Vq@B8 zKo4^|H@Eu~kPRHW^u}Z*%SL)EI9KAm*xtxt=EuJMy`9z35o)_iC9?IQ!_|Wzy)BU$ z6gV40kAouvA#4XQ%`88j?pe-h$ZMgXaMnOMR|7D}wMN#5jF4NhXX^jCmBA~jn2py4I|$+s+IwA(yPTj zsw1nX8Mbi$12zr%KJIJH8Aa||!^d7981W|EVRjK+6&WO9AcZR36zq0IfuR|?;*=t% zlPtdxe8f_sXki_xQ7gS^fN|2;yFm^UY{YQIR-)7??}N-vIjA%2H*r7P+djg;WDtkR zq;}|(;&vt@fzYLw(M2S|%T0SZH%Py&*SUwa0d6Mp!w19F)C0@H z{1cKLrJb=B9(s!dyhOVGGxUMq`V2NU{l{>d`&Dlq!|-0{2vlT#&K$6N;Okb2R?b+wlSF5#4F0`7;!ik6Sa7G z!+3WX-`viY)Q{LvjUY6b&cu5$-70kPpIC3hDTrafpW(qU&|S42TBab}KVp+x?#*Z0 zV>>gvSTh1{*Hd*VIz`=GR|bLpkm_^3U7MKcvXt)p=dZ_pzTopXLiJw-slaHhq`6zu z;}%zZG+7XhzUo#{z1#fYOrCY{E5~1`-Q^H8tO*T8gv?t)Zy4BRRLmZ-L0N%xHiVAs za#_!7nV%;cR&V_t)}tKH!81f_FA@CK?2i`j$9Sml6grMs*k-Q1N|L4=4}3GRB#NTVvs8c2{mNM3WEiG+=UGvbRo%}|(L-BMW| zh<8J9R#56T7>jsV6-rX~wQxre-0EYiyFq-`6IQq}OYkjxRt9nP$**9=r5b4>Og;xa zrPCl$eH0;#Mird`@+;0@TGP*~j+QDNV8n!~VT!vAOi{GyqfJk!$K2R0IU%lL{SMCz zX?H_NuACuk?Oi9NvHzcQrxItO$_=;1`_cpfqo$EQqR24?5kl?;yFyrl0Z4R~6z{Pg zD1n2AmW*OIuwLF_uz=lh76|vK4Bppm0ApPfLRsGTte69U;3Xy#^J-?`@!h%IEl+h4 zBOtu*TN9^JSlRn8tHE*%EZQ)kH8tqATofM#r}cYuCTgE75cO0>aTt&9P`@#iT_qd! zz^85FqbFb)nvd^yt!oUauo$|0VMv1wsPga zB(As+8R6x;XAM{53Lc!7D7=qkLrNi6m9{VokVRBzOnlM6TlyffbHQdI^;lxkpqPh= zq)_vghj!$x$9(OE+8}~^`nnj6C-qbqez#eqIVqgW;QkBiHf9;rGC+b5Y*CqvQvvee zHK=G~$v;!*-)J6xsT+jrIGp*=D=E(dF7&Bu22H33(g}Ed@Au1P4pMe>Zt&T&k<-Q# z5MDLwr6dg)9ZnlVGSaKb)VxP65sR-fOxRr`j&0v+007kav4$YGX{gw;HJMJ{uw;o( z)6=eQ>f)#u622*qwyMx_`q&9T{v-lp{cD+9CO@$(Cpw>VBF)Liu8?dRE!sZxgp{*~ z<8-(ntbl1yJgQK>HsMu0XM2O~yxb+bqJc+~s;S_R%T=Nis@>z4kB-f5AW#=DyrHC5 zltAV*z5v^PxY|tP>k^e>)lotIW12)5rU#N_VzeY_hb04w30GkgNgGjX@=H1dAu}E- z@WI_WRF0?q%R~RIDTiSMfH*P;LR-@?%~=c|lh_!Gs-?tEyg9u+<;#bEs6}MS6uZh! zm^z~58>CD)1~$DXDfp?1)g@$*yRYkou$me z7LXZ`3^L4We1_&kgC4Yc8ZLNs&Cqj~bl6jaSu_lx)clau9zLLTIt7>?LYtuhP~$-J zT8)Ew?GBObrkh9OTvRV;Nrz;$3bu{ZH>=(e`+%{NOp-i(*E!q$vntl;1Uwp#7Bc*w z@4i}|z~VwhITKA`5ksU*^*is(@nA*WLGq#A-+)Y*;vAiSpnH#nO4aOa`+g#}3K~V- zRkk6d&LMqzMfSfD>5e)=kaQgHn-EwvUW3cTeQT7%^x}2LMf-1A#z zlNkVpcirsAj|f)=N$;%4bRnbaa`|%6>~Mg2|8(bTL)P%0zXdpfd*;(W*U;isyx2Oc z5WkO`yEy!Mmr@%%knA;f8t15O6iJxPNnfD-E zq6Ne+ICsBeGVvM1bEN&agZszWyC=iV+3Ywpgrfm8My8D-dfjob4z$_*kDHpm z1Ky3l?qmRrMQBQfa^PHA)?vZ+3G@Aq6JRy4K6iY1xj>>+rd=%fY>gE zN{cc%)ay*Nr~#yf$ihbiBdUe6(Bs4_8^yF@>_@Y1OoM% zT)&2E$AQ!~1UYEcU%{v85XP`V(5a5BN~n^-QNYb8BD9N^d&xZ^>WSl!ZGlKi>Y_+aWK z%!0C*hronCUiJ(x581HMg+!Y5CrkzccnJ}Wu@`Bu!dhsN`OO9fuQ*1F0C$AE9jhlg zBjBC}L4MD0B!-kIg1iM5lwOD%NYjuggFtZ?RR(vEh%=C0*>2SNMQ8T*^119!-Wdu^&F3)1{H)A-Kdl}wzG)baTtyGMdhpW19q?mQncw;qU*q`i&wM`m z6>zBJ&%m3hRfCn(B@Mchm*4u*EbYFNOQm`ZGzsxFL?mP{sU@`#W(?-AU|p;8vRNI5 z0w`Emhl(S&bE`RHNN6yl@62#7R#T11E(%oYSTdezIGEzND*?Pz@4`Ixv-}BS@wD|V z;G-Pydg%J6Y?D^pZsrnO?UEOIg8J+LZt zUiWTb%5i_)7XqyC?R>MUc*19+s<`aq+z;J5AX#~y7a|W_P36ibL5$R+vpnRVxDKeH zv5E$;4%`hWZfKk{L+MkMxdieFe*Ac#I!6&=dGJCYR$C&mRX1IN$o=GJuZ@k+(4U*1 z!Z?vwO(;O4>zh)j#4b<8Gcopb9{%Xg;%dVnGGl6P@8n8f5mi)PBxKto5CU1V6!w0( zbxdVM2=u@1+>_^Nn37vPmaSkyFULuSc7O_<+9S}gj!4JBJ zOV0di5{xn#K-;@OXdCp}N70CSHA*FmvH~d&-MkY^ima6&NDh1leQ}qQ5j!^|9BANM zo6r7Mk~|$Wf=@8!CS9h1m^$(BWCpu6O*wkBhZ+3>m?B7kUt_x?-boq(wYncM+F6YW z1Q72(mRTqPgI&n7n1(W7pm`z)L+H{KwDE~69V*^k_@c!BB_MhP0Zk4MDA>n7avj*$2#iIZd2+0}lM!<=q3uc8in*44o#ILo( zC)^u?#fEtaq}ur=-hZTF21u&($vYS?>#|8vo(cZ=&Q#j${c!VT`QVmHq#pT-ifpMrOOxN$3*L>nQ*;k{+Jf zUEPz_okxTR4>K5`s_$yjUCfr?Cj@z$4e%+2SQq?#< zp;*-^8OdL_8(`j^C4T+7_v9r3kh-i$HH`hD|KpEezj`A1{T@hnaSBdH3`+8-LAZHx z$-G`bnd=m#eot{UedKsN)X<{CD^f42v`ra?M?v?vcu*g$p>e-iLW(=nmK~$cN}P?Y z>JZCGp)GC~I+CK>^D2!%wu8(jakzRuw9#t*(5uZcSZOAjf&B3SVaZ^r^PqYTOr+mM z4etTs5p@DX)CkkoaP0u@LUL`I!v>m6Pn++TTr*hvcRzaKV)#{7?Gt!}ACN0u32GyN z77WND0)xuE_LmluVMAZ5*SyVIjlt7C{0bz0GDLtt9#L_fTj6E3_pAKRKfeTc z8L{ExF3WG5xyvM8YQV~Dtdog0h_jC&BNU^WB6dDfe~z(jbi09c~P305P4Ci&{cVI}V} z5-%Ixt)=JcD_J7YYw_P*MZbK>y}f)G-k9T0x!l(+)CI%+uPv;ZXY20^;3O>j^Z(N} z!VP2~iTybD4)FCk#WLsAO`yq?;24>0J9JQVp04U%ECC|=A&n-XV8MkzMLOf%PIMew zYK1*bV_l_)w5%Xfnn@nS;c7)Fl=wDq1xscQMk^cVp_49oT}t9BPj)K1 z4%}#yadA1aeeg9=uQ7hvBb30q)J z(Lz=N5o++rOTkQSp8S1?TpJAbI?EbJhYCSrcyENpj?Ce@oD^7Q;vB^5ViZ}vEWYAR z>wc4xzMH4&!;=rklce^W$~WU|B>I9;f!~?E-5u61us9hDIPI#{SW;V*T0xHqFPwtW(9kb}3MK_`mDt0zp5gXrN_4zpP$5IZA&M5{ze#l0C~ijRjh# z?JMHcx=c&JGXz7>Vi(Cc8WLcmDJ&`3+`VuIvnGA!u>ePFXUUmd3Uq)iy3N5C?XFBd z^TMtBb-_KM)X=k6D!j}zXkQ^+;JSG(&fjk(R&S)8uPL8A2QOkF3g!g<1e1*DbirL_ z4^Mdj@4}z|1>+?)v~v0mXpO|6!rzgTZ7CPw)eyzJ470n(zPd^!KY{17-jqHt>;aBC zZI8>gM)|>FJbEY!rEI>rP*heypc}xIJlnMGjgdr>@rMa>>VMbZco}8xa*WOZTkyIA zKkB$g1dJkHDluAWE&5<}efp=~dj|IuS>A^uDH)2u9_+;CHZYSsV@f4=e&3r)DDpn3 z{h%=(dz8ZSbn4@E`jr2Daeba|&_psfjM;8Iruleu#NcZX^4cGN{NuP)2c@f(@8(;p z&A>x^_IrKZB%5v`7t3Szc-)_jXB|iBU~0EmH53^o8Mqz5TdF#-;3Whq08vc(N36}q z#~y?IW@&H3pr3{w^ni6stZ$Kw*X{lS2-ZUF}x^%d=)}pc$rvm0e)f5 zMWgLR9FGacU`ALbSzY5-uaTFp_7(atpt(`kyf3~?@MVRQr5Zhht^f(~HcXF6AalY< z52*~==x=H@C#&j2Q?@qKXat!H$@)oDIY2~?4K)VQ6t+kfG{2Oz zA$!CzErl(eA`?*IoMxZT_mr`ZoNKFQU!g{95$cAH<@e zWEtBw?HKl1#!gCRWZ)1l;g)37ayQvypQ-6&y?1(@mLa|0;LY2uFjYirZqtVst$*ZB zA(L6y$jj+lS*u_7e=0NxEUu|bqa~Pak{V%E7awJwP48#H{`WJs(o655cW&TmLwS;P zvL+m!jW2$jOBQB$jdMZ00z{n8!R>VA<4;E~;%R>(dF1c?D`_#9fKihXnDd#~rrg?F zQ+=Z&!?BP|b;fCyv0z`&paN;Gkxs(aA+-*_*iJuVA7pDev*r5R9M-M78H>46rtYD;An+0lUc0ZtLrDAH)3P%}aw#{Px^< zo_#cey^Lg8e~q5=pM5sk?vDrEze@e3BS}SfqG@Y3nC&wYKJGp1UKN7~<@=R**57q- zt=MRpoHvOn4V}sY9bi*lGNG?bsmse?l{}$(riW5ba0>3e!|W zCN2w^`=#Fk^1w7prsjYkJvW`iP0~V;mDpg;9-e_3_mUy(Y#;Lpp1)OIK5EC!iex;e zHvvANCX(+ZX>b_H#mR$?3Pfx0yt`}4L4O90L#i-9)wzFC2WYnkR~m}EgRt>1Y=O#WSYvY2R#H9H-%_#rLM-DmV1 zuzqTT-`Kd6vMgD^AQ-MZ(2lkLuS6LHRI*K&OgX`_QXuA|aWRfSb%=hl%q)|(J^E*2 zm?eRYHYnoRonL7l^4UFJtLmM_Bj*}$2cvJwb}qCbcwHQEr?xK3DHRC?%n;E{aZ3m0*Gpio0=Gb)~Y!rj$|EFgdlS9nJZB3dKaZ6Z{;6$_ps zP5La8QoylwJw#aTk;%fsfbU82?8&%skd14hAO%b%%SDF!8c0KDaeI8yjqQ*_pS1d^ z^{F523vldgGLorT@7nA9{vu3dAuP7}Vk&`v^Z5=!Ya^GE=>H4+g^F6rUQXvLoOc~s z*~Rh|Kbl^;4d=1=e(hu(DL65ZxTeFwjb_4bQ%=Fs>=4P?G7$hGd{&l|&eo9wWg|a- zNsp1AJO^pB0yhyhZNs@u%hkUo<=lCsP{R2W-i2V)zC@_w)lP>$SA(I8 zJ@C!#X^JK}8JOl(#V-b|7;jT4@eQ1AN)w~bEc~zx=V%Clfvvu#xi2zI_Ur5Z{Q0iK zdvA+GdhTpWi^W}ayr+$zy!ywlUn7EI$o;q-w_hSZ>#iDuo2)Ss#^4%OrLb^<{Dvz< z(g=$r3ebFIDV%Eac!uYukntO>o5J&71V@ed;1U!nSKE2Vwc0*``!U0q6-!v2-*RJj zZcqm$K*dAT0k(wi+gux{o#-(PNrl2LMybNKElO=t2+Rw55K4_u9es!cGZE8`)sr0S z=E>0j9)A0#BdtXGy_q`5=GB7~eo?wKlOLGFi`<1{jNL2=uhTY&A^kEjhMK!+;kBEz zf{e`{M;0s0@Wi;Ky3MiucoUnSe&vsvv*CwE6bP*jsI=13XY}20;}%`E?2-)B49Fgc z%#2a#miVJOI!Z_vEX`#=XIIj4c%q5Y_(a^biDD=NM$&FCW4A7|VfK4FyAcvT3u_;X z;!c38IS|X5y1`T^Z`Sc@*bPKNf-+{SNtlbllS%a=fuQ#aQ@-rS{IOxe>co#a+p}<6 z-yRCrCk1HLiu0&wQ(?R?LAm(8SqoQQXwo>FYqHO?&U%cU?O;}o+OX%jif+fj!B%i@ zFcQl8F|1!;@J41zu`-asbQgwk$8qzDO2F-=>z3$+<}7Op)_a$33cm~7(S~OQnI+$E zSBPGXGyED|rpdotKH2T|yIA$BuQ4f%uHJ~KoFHAhX8%8P7kII9-@;k`o{yOXJ5wkN z?6juEWh_3u3Tmw4sbMNbS^^!LFSxZVz)NPR;4Vn8q}aj1$=mlJTuy3WMg|Ywl&Qd5 zcADe}n4QpYQp+a(*>OQjmf)HwcQQ~tT@(D5tpF%bx#YA%-fv1)Qf>$;sZQXh3r}*bhg<<6R$uV(HBuJ4K)ki(*N0u`F1=d3t#q^r;HK4!&*wkA)?)NU+xpz+p zbrU(Bhu-ZbF=9p4YohJ3t$PTM6dq{m+aXLvXA;yB9wGW=Nk`qm$t4tkuEoifzQv6g zVCH0T1@Qz7qAVF2g~B2y>gdgN6i5dQ+Wfp~oawes*t9aYt{k|Au+BCXvhwWzyi z8+KA9;_`5OML{`8^-5LBvCAD92H)PT@y0Yib{x)aZ(36{d82!S(brYQk*Y@=#@Zj0 zV;&A&6_6Qe#+C(3IND>N5lvCqVe|_9T}f1XN<0U>Dxw)@E9{QV=>`fx=FPbh3&pcH zmck>?8**Pm3Fs-fzS8CsEt%g<#!-difsqz#RJyW~qk?t@-T-c4k@5`v!#~u|lMK!b zzDZ)@i1p3_aJ=YItM=nyY#W#HJPG~kB;qrN0h-2F=kBA| zMefXAn0ishrX3}?j=M_sx3Q!7ZexPy|MTqP10jY}15D2Ol+bpbQ_OGOx$oPn8Ky33%t z?vZ$R<4)2b%9}M7@JT*P<=TN`C-}=5LP)17|4$l`&nw57AIooEtX0;vT#wJ^B;Wbg z?`&iLQF^e;6+GjAyGEMII_tY2#3wNSW&gTtDqy?~lq!s&BF5bn9(caK*1LHQR5pY* zMQCmW%_~C^W}OQlFcR%ZbW(48A zwfec>aI^XB-tp`M@MXf%$;q{s^AIg$QNUEEa0?Hn4;2#%5dl-~rB8QWY|kF;cGUNw z^Ub(-cezR0w(;h*+etkCaXWt1$MA`{4-;*rmyS`W*x5BX^zL6H##brD!hX!WIKyEE zt~gY=nqTc{-`w3htKzO+KQ=wyGaucA9Mc+Eo0#3r&7oJex=HGzT(ByzXaTZs`}BZSY_?<1=1jQ~;>W z&^2pv4LCWZ9mDg4V>!ZVQ*?HWIf3!-Y)t)X(cFdj6Pj;d{qC2jCac}0H$|anlus#H zX*Jd!#U6PvGn6zIkq5e^{xqBu4iE0N>hR%YgIvlIbm~l{WV#RSpr=U`5YBb9>!ZBh z`~)E2^Zyh`9WqGgeDl;KuaWL2ryBzSlBQ;C8a#jSMRqNpNi7QU(fPcxo8Xt7KyDH} zy5Utp7L)Ajsx*Ccesfw)^$FzaFGyUxDBDIodDkTsYBn zUxipNDlD!#$@lIU#95&Q^?x|#DQ$uIvoiYIu%0eZo~FL2w-3AEN@a?ZpRPj?aq|v8$~&$U0Rp1E??jqkwe?=)c)stS z<-ro_KuSsLed6O$S{FB({~v)9VR74TEAsk}3;R*Utiv^4uKwe3*FbFVhs4aLq%uWa z=B4tp#Mm_1-prb*fHX*YLgJ%1JQU*|E^hC~X-w}Iv!^xEl4ECBE1bk^)M6WOfdrTf z##=E_W=R=}2g=<(!hW>C;+erW1$a`9>VUmFFDL5ke)>k?O4V>b4T$d~s|Fj5A#7D$ zQXOoRYtcPKi^=<{=B__i-Tm>USg6O94xYyCCceq4>GSJgeP1+AAh`>u3ieIPJHLI2 zmgvXcPL7lgkDrZtEKya1e&197R)^6o0tR~~?`)^M4@{Z?b#`8;3S5o-2o!&vSLpmf zQMzLJiiD7f{iRK4c-fdUX$S|&z!X9D!o_1G)-_4|18cPeJ4&92(QzzPgXnQKu9rh576sm@7-b`FY^xgE@Jghuk*w|ap>{Oyx!q4lIH()KZNGpl|+ zrK#-Lh+*)%30T^_7cZodc2pI7Axr6D@f&ulA~yQI{az9Fi5myr!Pq4%0C9$KkoHsl}Qlxb6wV+s0sf ze#aDY;U|1FJO2T^imp1JGod4W_8BMdgP&QCt54`M+Tl6qe8#k<`SST3>5XReKha;D zd>&>~neSta#4mP*b9Lc1YJ3-RCnxE({cCmAD$ok?}UA3viUWH)xVxpisJzkT%~s{+{1PjJ+)uX{gb zd6AO&M8mT37_6uyPwO^-&cMC6oH}q?^Z0P4 zSrce~sFVp>gi@@{PtW*Y>nb+B--oty+=F31`?wvyQnhMSzvrbID@yXU8irS)7RAT$ z;@`s54_Ed)p8%X1|KlIEla5EeltMylxqz-Zxx5x8k zemzSoDwxnkVI~S89&r!V_PA+mBSPyXQWBuE3f4H!8&WlncMG;f|MlBJFdRS!8u0it z?1R9)6QF`pQWTNDKcib_a8V?S1%hSG>1KQ$I_3#m7+IuAfbq^FUL!6W^A~GEpta~t zU3EN`6YQi%qx*2jiE7J6WvKQc`!3+@nOZC$UYbDNI&`8{2_x2GO3Et1^sn(~q}v}H zD$1rFpJDj>H&W48BSorb&k?7*vS*RY9E_?z{*1{{_JNT!4nBt@k*N?E<&!AbA4Y)G z#grUBC6?7|Lg1-8~XI(lZbTsI9TZ0*;tUw{7j_5AyN zL;^~2Bc{?1`0fb!wN``(LskUH+O~bwx6`VM`X2FmXaN0~-S*I}<9N3Sx#(4L_#JOK zKJ;$Cg|#5Q*oe9v5AUIgwW6-HsIjt+3}z?Ety|1%_g&tgDN{|6mQ!7(oZs#?%8rdj zn9bwP;$V?hG63*d_xH|SV*WnNb~(_Q2VDHQ_YDYEl5HRU#))Q@Pd@A}MKFC-`km{% zEPHu}wI1Y!d_c=mu}teu2}HeCJPByX3QvXZ%t_9! z$)is?&CA_d)j`E{ohw~e6!wAI#l(fx2sFyg=V8~yY`VrfLeX=^7g7@?FUc7r7G391 z=Zc}5#lH0C=eYg)RRuPDv{-GLJfKP?X~v!}a~D(7bXCt7ELuNI4hh|T~^wnrL22`hdca$26I zbj5s63q|M2a~Ve##HX(qZOnS+hI+Ga$3==fKb$oE!~Hu+xtjVd3A({r{VI=szTwHr zco6Pl_U021yObro*Ll%4Zb()##*;*zI4v?`?cO2tV|vN}Q}tEG7j+{gHV~210}EPZ zzD)35uE<5Hc;=4nkBq?V#XM3ddzJ6&!U>u5VMncWIb+u$!nq)5|Dk__?)Hr$U;RQ8 zuUoH58!hyh_?5nAD#xL=O~MdDXEp{iZNpR*m&lI6*}N8%{9QYq-+^I&13cpb^EHq@ z(%MGuZR)mT`}YU)!0YC{sE_#PK(@0CZcskICA|H~+ZgTrzaF4Sbr#9)$9T|XtX(e;G^x6%SO=QuiiLz7^oNN z0`Y#M@mrC>PtQFpLvG1UBBI*{d#+kku67h!f*57-FzS$cYBxURb)4LTv)M~RwEdNS z$(t&&J$e!rgvQ zg6W;gVqQ`Ey%84cDy-nzq76fOm?bKKppZh)sbS5ySj&fNqb=H&CO2ffbQ||(s+G!G zID`O4a5@T+tqQSZiySLR7DIZ7e{aCYL- z8Iw{yG#HW{N%Asu+VnR0-XXc1sXESzv(-j29+%!(wdvEo;kSFC75W2m4^3%(Exw7vG&+CxweElbR%Ww9Ch;YuUKIg~& zmBPNcE&O?t82XV#^j9ra-T@ik;5P4UdSSgLW-D1?!qU_UMZ5(~l$I@=J+~ON+~NU< z;}UBW$)gBn?tkRKWc+QnT{9HzJhD=UhlILfZ~=4VC?E(8w$5f8vnHb80kARG%m+EF)B_zOF+YI7|HP$IRQ4GpbKQo)qox29 z&YJ*n)m9JCEFm6@IX9dsqn1lXqo5~Dg7MTrVvC+mYqJXqfmv8s0V39~8Nz6xDrrk#?)9fp7e*7nR5J+XoIHiXiOZRvr7Z0I z6nYh(RWh$jj+`hMV>7Q&${#|Xg_(K*=hVVlum{kb6UR)^ylt73#ghcjEy^C!u(> z?+|Ho{dq+xFDv`FU=|>%Dp3L5!!zBXFumSPWiJL%|7}|!_7<6FfqojGy9Ga+9|b$$ z8qz8Z`Ung>t92-qgDYPY!9eC$`Yw3sm(3k59>_lp3BCinC9~X3s36hynf_~9slT9S zd*u}Sb~_o2uhod^d{_S72mPJqfM{=ZiB*&JPCBU8@Li#&HE!E}u%N3@J;j9x>XE7H zL(TXE5jOC8fN`zjnjv&Z_VHO}489rD?J#A^I_`tHD*E+>7bB~`N(s4h(RDT^ zm^{wP)eiS^>C#$hpByL?sxU=){r=LuILh%|SP??w&Qi(-U~9q&^39b8#X}@#BLvb? zvQExd{3-xri4EWHm|@2+kH`2>!ye`#m%SB)lDMGh-FBWhG$f+@Q8%OBk{!cLvT5wN zt(&8i!3DVQE2k?MJO=MpXm#LJ3QxU;Ls07MVb|a#33l!x zm$H8Nlya+VQoHIx9l3xbXVQ6|t;btdx>3E$&t8{pGs@-ONPe;y+I$*{yLhtn6>G{lNa2Q^6OPRo9(K z579J6PFrTq0|OTCGWpQY?*xZFlW>-*H45kN|VVkgQg z$K!2T)nE>agW^#FO;$gCN{4^d8t(-_!xWG$o823S$;{?ll>nhn< z;|02qvUgSYi3l(^z;j+`=7(cyMjRFCjlpZd-He;`-9luykyVpyMd=K0n#Vkl`r*&; z>vm9tt@E7)j6KL2ID&$DqEh1O#D#@u)PR!6f!1u`vRUnqfkh)uRn#?n6$G8 zgVnQ<$K|A-GW8=B{9KZq+DB#mtN@G~XQtzw!kkynb7k;rHE!6Nf{91^lcf z{t^(DN-5^|>-4qGei@;+rOs{}9#TyU$F+0~;CL>o7>_+hLTNN( zutj=OlpjrU)<{t2%f{>*c*iE9eOFavr1^C>wPurE{3LQrXwy>uRx&%EFiZam=Iz;K zAzVELtN;@G?qBq;l+^RH-$iiv;Ij}?*N2pVC{sR74ik{G(sc9eGzU7}e=PnN~UjrS18ThfZDvh^u{`-r#zw7245+_(gD9AuIVkN;2b2_9PVw)cisz?{((FY@gs&-ijln=UYw$@W78g!|4HbvX(FNHq#nv77)dMvsnxi# zLEDf9b$pvx-_(1pTA~8;#Lh%foyTwLowzb3j$!aKx}#!jx^bg>q@g0?93Vd$2Kj6% zq(qho5}P=|_nL)-dFYOsduHft)tsq$8_6b+4hTm4JvenNdDxi-j|}V=a%1!Y&1iX@ zxE@sG)8m4Ch<5#Q;lrIi|Lmnfuf}#kPzDz}M=G63dqKK`iFJ~_B5yE79iG=^iRBZ1 zEOVaPn+!mH2bbAh=fnxN&($P^!hS&FWQN(@;lVXC%?gp^O~bHEPQjy2qL?(zg$w;v zjxsd6we7CP%U?miP$EpP<+D#t^vd?}WU;)^5H1{1nf<=yTk}bGQG$!kXFeZg;_AhN z`}g;Re+fMJm$jS3Yxwy0M;|YL@y&0VvWlo)JW&CuDZJL8%+AD70*ku_P(Y$t<{0bD zSkfj@ZQJwJ^0UjfCyo)UvFBz11vifh#3l!w?fG&IfcgD~WwMyz`_|szv(rRgjX@n> zCcA^G84NoL)n$HLPS|Z1=+^l;!sm$Nr&~9ltW_$I;06JZ$yfK*)DoCHNa;|FyV36lUt(R(*J_(qS;5U(1zI}sHn7VyaRr5~~ ze)ydFIBw(A?&tS7yh(ld*;efHGf0l2IK8yymZJyNnbK+b7kL1a<)(1O1=yvKGu zMMeQ7M-F^olwzc=KdpG?jUT?gI4T*3T>lDE>g=vyqAp2teEzeWnoDk8fw_?*pVX35 z1)*sQ%7uflN)XA(W$g$F3}#S4C0K=)T;0C8HRVp!fO4IFFcJ=)pS@|;!UKs%ITDg- z&xl&+qg+z;MvzxnlJ%Cz{*)bQwJ)pt{Fp&RKZrH+?BbY5XzV>97Bth?X_JRI$uF!6 zm0qRtVBChwsYHV0ZacJf(<^GOXC)q#mP%H8O?+me_{%# ztgLBZj5ma@S~wv32PeD|AY*Tu5&EFwH%nS5pi8O=h~BRFgzD|#S_FwBTyW*5@QK%b zT8>NpXu?0&43p$?sZD71dI{TKC~DB^uai@rCyS?n$$WF>5ZXWcIxH2#19`>12`aqt zJ|m9)p=>VNm?`{FH&|YqZm#(9b#mIU-hrJMn!|JUDTZI;ZnLWZU&T()2iWp&T(_CZ zwD4Ug?HQ&>(%(yf{0|F9Zb+w5L|O~OK1%%e`G_UAXCW2I+Y6XMn z_V4H+Ww<&%lW_uHJ0#Kc001tXbLz*T4Y@)Pk($Qe1*Pq5lAgSau6su^qFBPo+*N@V z^HA|bpyVsVd@fd9oZN`6*8b%_^4{ZkN?f)Wv!9*upw!&#Ka6B+_W9}h+wb+o`G5V7 ze|99mhu8TT&i5ikTQt$51 z3eDVa;}qG95aI%yoCRplHvYu_cCNIhL$eP1Auae9;n zc>law0w)fm|Iw8l_4#=ocS-*2`6NaA%pMP5CU1RB`3HFZk@5S@phAm_X@H<+9%Gs_ zSnlGKeL+Do;j)Pa;sRsd$wmYNrr69(WU9$I@ee%xx7~7CKaxZ&CiBmr0CP8GB7I={ zmi=f6Dq6*RFFQ1Fgb5xgbMa@qoS2IMg?KgIR{f{8oW$qt_v!(FSm+Mj3Vu;*`k1uV zkS(jn^GGVX-PD)+(Q{Gn&%4wmg^iuE-WS{WI%3)+RDU%?25`F`6?$pOVR|mw+mPZu zB+AdFyqC>6y@`DK{uWuIF%j6j*!dOPN!NpIug@kI?g|=ZG#!1jMcl0f+V$xCVOPj2#V5*s zs5H)ilNXXmDAg zZgFHk8E>m%@Gq1y-Zgl(lAF7G$foHbOw!s7sS7zkP>I0sEnY`S zV)byqU_mUB3dlQo3FG0Ix>9z>qj)dfep)rsoLp2V0G3qHIPD5 z2QJhciZgFqtJ#drQh9zgUfNbO%P5ISj)k$md^mMnj7Rb59e=x}8AjwPXzoEV6G@7{ zTk$4%R<~d1k($p#N{Xl5V{kSOWbnMOwLC(@eG6|^bLr5e1baCmXK(%)02SxXmI>uod+#CdX2!e#}$#f&scQt$C5wZhqflWR^% zbR+e9;KKPRz(yoWPD_z?OH%m>@5=Q5wXG-*cmlA~{^QrL`)I~SF3o7tI$GBhwIz=t zkBLq&g3K$_x(;*Rqrzn>L8sAa`kux1rueQJ7V@n6l|VQG)~6Jbe!FfXo_+#zgoX+V z(LlpmnKhz-UdmqDxB#*U_mjhWsGM*|;y92iGV&IaR}qKiytr8=xfPVSA{A;`t*M-6 zn%t{mUm8&}o-$$2%6Lv0CUa%s7_UJWTUE@nAO=V&fUf}+YR*b|5anzP$L-aIMptev zBhpYe)EZQH3g2IAygSP!RL-nfl2rCWs5Ai{=lz@eOb*9Zsjqi$^D*T;vY{? zm(NN2)ap;UWjq(|QKv{_BWcbRG7{MT(=6jFlUP+-5HRH`%1S^*^lcS1<{T^f<7pE+ ze&d@n@`QHLb>D)vhgY~9PIHl;SEKDb@d@OW@|xH>7uet%$Ai=vOVlbD`Ct|q!Tn*fPCJxa?gnoAz#qt$J;UA9`u19#5m z1yU|on?d7rgSLx5x_VDrvmsr=47D^Dy+_N>j(Xbh6iUvMG7ulFT4`p6T_m;nk682o z8)S98gm`1$!ucb5wA^N=-yHJaT&3@r;hun)v>mC;UJ{!rn;$1ATZet;218;lylB3R zmr`TrvV^`KKhj32JUE;iKgGGnjYdMu`z**l0a1+!115DvN}NDa2|`FlV7n>mllEtU zhrJZ#0c)-C^=t?4?A#>@Gf+1|0UEH=LMxs)v;}D4&Dqr}CQf+^Hrt_^WQTN~BS&$w zJDJGp3BylvlsN#DYXK%uCneICPDIprvp`0RHPOvTrjP{C%5m$f=+9xq(V3e?8FAo; zIq6!>D#wCLBZ`=4qan@;Q?e{~=Z>>8R0o{+HEd=)&!8FY(_&Fla7fRIzE$5JG1sw5 zRyxfy9^~j|ZnIBqD|Rx^Zn?}!IdZ1<+9{;;>v;heb0$fGmds#=ZnTN=X0nfo!7&m0oY6e=IpMXLoP zB#~@RzpJ`E>+8-cI23SV0B{V?lH1$6joD7SvO9Qb*EPpFpU=;Pq#uuZX!gochsD@9(sTfqwB-e3z_JAoJ=&e7DoaT;A~}FV@MoI~DyL8M zd)?rB*AtRI%if&k;-SfQ_JF6wFO?)yIQ;;7IK8|b9=ZkOUtX4%6kij2&|h@@Kkb)( zyq$Zm^WFws=fr?`?HV<7@7Y`KxgxDs$wQX!n*YW^u-Z6cV=EzFm zk=pB4rLB_N(S-XQBnsstdPiD}B#n{|4!$_M)?S@IdU)t+JEd1E>bSsSPi2+@IRF^FLU8%#7q;s z$vmOd5&&to4NR9SJ>&|w23v%rsVLsAn>f1vx@w-M98id4!B7@v&jIr&cKO?#ALMH` zq>y@rv~d}BCymi=2bdmsN?>KWuupV&*cTe#{kEAwJFzwB9BN)a1J{YZ4h4_8tI1QH z!69dHsx7eUjT$<&M)KF87TEI@9%d~u`6O1|yi?6(a?rHG9TS=H^P^jXZ$Rx(tEcLB zm>W=AC*x`xy*8a-r~*_{o+5LCVZ!3W1~gkMExmr=e4BySTHXak`sWav3GB76%HJ*T zQeMymU?%W{AnneOSY}>3NqVouTMIN8Z#nk6XYHH~&nH`fC~`J9E?rfE*UkYbGu%w^ zV&fb;g0dfv+kpc_94-u;u>Y~gBOC;|MBH+$G2bc8V`+Agq4EPL$!~zln^tQ7dE5eg zvIl?NI!>AOXM#gcJkbmE^81bM_3K=8W&g^k;Ql`stKi>x`Bi|n{$idT6n+!Dy&%)U zZ%uX-`wzNsN9A-<0;UeJ$z9NK#sdcya~P1hp*?S)bxw~}A({uhp4-D_2tGyT+MCR7 zpK(3GejN)^tBp^yE6_NiWxnN$CuKpTw(V9FEkTMCS`?WH&ne?0t90}CSpEzr)4 zil-@mP)roX>4saGJRS|g$&%8n%$ovR_!IBldmP*^;a}KF?Ir67FZk8lz-PVq7#VQ@w+x>f`{nMi zcRRdGa|CoW65ifCl!7U;qQ)zRJk*}WGt*&gp%i>9$K?rl42X##s8vaJ3aKL19=?IO zP&ysDddq?5K`F+pgi(fyRJ>pI?5rB5v&JiP!sJOe$gT~@J>D*Wnkl0?mDb{K+49=t zni!M5Mmc0%uKzISZ!ii~JDL8Uw_DE(sS^}3i8WzQVdcLxi7Xr+xckMpAW#I$$zGdH ztL)^s+zke>b~Vh#$Z|sDe9B*WnJ+}|6i{5=EvG*~Y}mkG&S#0aHgG&_lcx)Q`4iVQ z#Pr}D8Taxs&l2Y$Ew+f9RxQokwYRIoxftB!(PbS%$Spt>pSTUjVpiJEae--Z+2r-tj+t)?RAa2 z@BB2Wloy8ss0xA*pP5PiWNeW~lf3ZlY@*@|wpvb4L+baqW$jDONO8 zJqiacrL4@4HK2SaIz`J(_xv!XT`M+6odLkLTUdTS6hlU^((l#MMJJyQ@9a9)j&4%N zK?)S}_P%d6XPW*fRtndtoxI(S0uqQbtIV|8lUFDci*s*++q~PYZuFZiym=yerQgx(F%-JWiFMedU;k1ID2BsYN4#4{ z6onP+yj`zQ941ENyxD2rrMpQ>oY6!8hkyb`Bzf3^RL93P$Xso^I7HGz(W6230c=_} zU1=q#_9}2CcLQU#s0(v#a=RdkJd(mC?{erZnON&=8S1nQ#W2@og2n}mBv&T;9HAm9 z)#5W*GaT2WZVX%=0Ao(XS~g$nn6Nd%N}l}QH4g7io}@g;a9yUIMrhE(=Fx)ntlGgk zVtPAPn#RCJJmu|>l1-$}nw~lwj|K#3zCmOvlDnR%_s{sw6wG*j=v06d}{{~Ik zo3B6ZUE7{;xxbUVW%HUu?{UH1KQ5EVkj1Mn_3A4d#=@SxU)_G)4>i zPBDmd=i*`_&U+p1mD!$jKNS@3G^*(TNs!vd=ufDo0~8rssdp6#*ip;uq}Wa}WqEFa zGAl5d*_R8I^0uG+T?m`!7JhLR%N&OJ1D$5BzF3*Bl68^+)%Gd`Tx7p^GfaY#!+{)Rk4tz12E=DdZ3jZqK#2I(Mqo8eFSQYuNH?i!HDBfMOj-04DqAeIs8IPBExE|nUh=N(xZJ&mzB31v_&wa-TmJQF6uMAz9i%xw-ahg7v9J&Gr7 zlK0UJCZZF7Jn2!rX%yEi21TB@WA+Wsp|24m(am66b8Q}cc=Erk2i8L-E`tNXK)EFd ztHzzsjZLIN6p}`m;EeYlxLBp#_7O22wU$NvEe-a(9@Ne6zEUXVaLa*8CE@)v4V(xO zGj)7nx98C<;i1iTt+$a!5m|NQYc;9Kb|PV z=cR|4q43_yk3K(p&d4prWz}l%%XrujUlK`aR{84$UL8aUG}3t*k09Q#eC-#I`Qw_) zIZqSBQ0tcWzB!PmYUZS7oXgha4a?`{1DzvCToTbbb-^^n(?HkB%0zGDp(D*1>T+RH z2hlwi#1m-X9Q$GDm1h7y&J@iiF7LN7@AONuf^vVg0Yk8V1A08A{8#1BYE&YDdlYwi^%a0XAcnJK6y|`G;vh%$Ox)09mywr zSm2_8X5IWlWrw{eJR}OMajGQoCJi*FVN)mS8a0c`9cj3RFf`oOHe(g*SgdkQ`$sq= z#z1jFTX0peUo)t|ARS6QVaY^6Nx&2{B@VX_d;%!iG2w8pzw0^3^W*sp~Vg0}5%B7xgZV&^q72MIp1%v7kH^53g1+%Mtd2Fb3bs#}rAS<; zX1r&~Uj;oG$RlvYe0a>zCzFx{$4wv74=A@XP($pJ-2%Nj3^XryDkooElfxh$!&VTVy>8+y+#nD!Hb>;J8X z0#$jh*XLXQU??mcP)+T#92SBO% z7wmk|Th}R#!q@a&%4ciRkTtG1uebDJ8^0sb!{p8w^+ZT2aHvCfQ?;9|w<&Uy28yTY^VRGjm%-*sk_28?j8p0gZEH2#)&u-z9<5;DzS1Jwp^JE2;cfnr`cQ z2-Q|RRw&nMdkWK|faGypF?sk7WKsHatCJ5#pjA}|56DxBF@_8s$BG>*^!Y~j5!6<1 zf2cdpSUrPx)Bkp(yHU51n_GtWA_-lg=i0aO_4Q|mYD=B_gmH%s0v}O-h;-Ca=By`| zQ`kWmD4cMn7y5@r0*3pJ-itHCgOzFzXMcsDU^|$z^}o(&rCTW5+&{jMKdtlAc3QNv z-i{Gb`D<=LLQL`&$4{rk0 z_vk{qZt~CWdG{>Nx9U? z2-UhZ2=am5T)c}Pb#v3SICnSEKgl(e7CZ2*SINF`AC(U$YG~MECnH7Yep#c{aN9v` zktG0YG2&T*`3Y((YZ2TDYy`hHt|tQ&t^RtI)&IJW+-8^Fc&cHob-RStIJ)aujLmdX zM7eLF;u_L}Lo9K1^DKXcTghO;G#PFG<9D<`v9Rw>Q!vSOz z3)L8uJ_t)|24gfW7rHK+lD#QWu3`22`kCUeQ}NKdJTkuRCo2Gj(?oI#ho8l8uiDtd zzibbIyE~FVr~~1P$7+O9ltHF6PenGtPd~k@$L86c-pe#o+%xh1m|{TQd@hZEfOjzZ z*^X`+{(OeMeaE z_q!K4_HeSIl3I)^aIVfh&XR@Gmk>=Ii^#9&BNnO(KE>Crn%Se%d`J|c|KM|t$S+o0*MxD8 zQpmRPT`c$-fLS~ED^{Gr=P_w3k#i|T=U2?KXPjC?ce$ENj=SOqZGvKKm5H(_T2S3^ zonK*eu@MPFs|oj{TV%Bdmd=jD9Egf#T-4d8)`n+FIW{vxevG}C9hs7!ZX-_Xo)vTJac8qwp%o`_X zo##TXN)G4`Y~vK!2~M%bwwaI-zN(MXefXBdPKy>;c`5T|*XCfH)8AvB6@vf-d z`_aGOq5aEYhTmExbanlANER~(Uk)-r`mF0V<8T>2(*^+^w~^J*Et7h#BqPEU{r`Y9B)er z9&#OnDJATWoQ2zXvJ73M8z&G1ahcQnbi={(ZsLN_ zNfSQt)L(Y#)5W3R?QJe#lffqFL;@w&h_R7I2zoYI!*Ro4x*HRO)G(r1<`yZPJ0*gg zkiq^cM&DVJ`9tjtog!4%eaAZx+#9Rn<1h9)z3{*GU%OP~i%XgK^*Uo=_X5yHDn;t? zIrD<{pJXQk-^t!KN-l^3CwGjXX(T)O6QhS^@=iv#Zrw$Y>gBBGop)`3<=KIQTO$-zr zD8SkF>4S5%C_(eLV{{6)Mai%Q}nH=355c+bI0}$VQtx#&1T92 z6gNoMK&Fi}iSpS-_6K{wB>+F81+1j_k`WWcC{lKuk)0miLax~Mwodn1>6O@&I!Oac zmi6UTDHWrGvVpKecKQh2kD{{U>r^gjbf*6R*W3j^?8urnxIL+Tyqs90P;}91Ce|Ku zb%0ez&Y0dEklF($r7lZfn99vZ)bFn$Ciq-`|EQXqf(1$nXk;c0xp{P8FJ zYyLXVjd=Yz-*ivC{>8rsWBm8UDJik|^w>RF3cd4m%nmWRNiA2VvI)8R)C^`B3r4SQ8$7@BMDhB8m$Qk>)mNAX`7??N?Vtbf z=4gw3qt4rMrjOz0xUI3@+GLa|RMDG6C&U1tNr9ipiXCU1nx=8PtElZThY6-Y0;jHD zy(v}Y*=$@Yg(=4@#**uJ5_obN+kB1vJzl z^BJ88&9tUw#Q#cC4>)!BkkC7o?2WdsGasucm#f(QsTxWu>0=#3P$EQRGZPadp?`A9 z=!EOqMtq2iM5XZ+YZ6jJt(ynF70q3o1I+?W8z1FSk*?idiFS5E{5100lk|}4|31v&{Pue4h~Z!oTiZ61);(tL%m;$ z-arzb3}Q+-0!XUZCsy$s8uj$^ON{}FOOeOh)#nCuW3l8`Cn9a*lsPTsa(g`g=&#%D zHrBuqE-&hVBjX%xC{wuN(ClfH5=~1N$-t9njq()GQocrrQ`Y1S5?>w$wvz0~L;rGWCEeLGDHh`AA z^87Dgo&bIh#aPv4Vnp$(h^jKLQ-*qX@9Hdy{_-#zmwo~Hr&K{PGyEq$BPr(vgc6(|H{$lrQLyZH%Tb-9uu;c|9Ht?PQpF&I-bwcF$P({sTnWS|3fSzUTRL z(hcqw?S~K4nZ3!Z9HVf0FQ`f90QlB9>u%c1Ww8{+vwhxdchAF3&E8BNW2E2Aeg4VeW`2Cqc( zr8~21i2uy4J8J!8?KqZ8YNIl|-7F?cMIH8x!O6^4ygs7qg?3QkK8L`T;VA4Eqamz* zk-6B!Z1|2h&rFl-RwRE>YAE>^M3Innzw+Gu_F$cd65Vq`O~kreHL;5E*4Y*Lfi z)|<%NO{)4TX0NI=2tAlkNAIn-{bB~wzYHOUKT|8F$Xw zNvctb+{L<5P{eBMR6{tIVotEZW-ZYmqrf`vLwJ7$otrIdjHeVyx)x?mD=AE9Q~yhQ zgpv)lCv&0Y;YT&(@C*?#Fp=pbtivW>b`E#btTCWbLuNDe`FpyBi-7l+2<>-114Ci@ zmj&-ljE46w&bK?Rzb*UWKXNztvPtt?Uu>QHXr6j7CVsWg9sOX$zUhZ2((;v2yRqT zs6{#tA0FMRis`c;no@CN4o5G;n~4vD2FHyZ*xN$^2&SE-8v}3~!9f-0ZMu@b9`^wH zJe#0vJC7(gn)AcDNs^~u7EH3VAOwoGPA48ptmX{94-MCY|h~10zSsbfcZ>B;$izgBgxYswI!u0{_Ul_P!P|aoy*W!) ze4$gR-zr#B$#NP~LDx0WDW{S4T8nV6!+e+Ov966{pn#`WHtWBnWTfscY-K!~r$yA~ z!w48I5hUhsrc&9pC>dMknsR`%VVHqsvp%5OuM%$aI!}`^j7s z38gSV$v@50I8+frgRfK(mWh`Z**C+!?74)r9+@#PXcAH9RMQ84Js$N@Zk_&hvt;yr zoe)zXtEF#MOW@)q^O*Jech-liKuC6h*oo*7=f@&$!T<#{y=~}Z`}zCeOy{-6h-yo8 zodU2QnajTvtm7Zz7KQ#jVt+5Lp+0-Ct>Mug>5^D`+YKjs{aw8pQCEUGtUqc+j96~! z)KV%>P^=zda3P@+i^&W1&L`p(S6`JWTLbpEl$J3LO0VPBh>X%t->33R!nPy$;s=8#wv0BM(hFT zT)C#@p#D77^3)lZUTdwOGqg$I3TC;E)<)pA-u(=knA_*dMts!DB`4#J%RP)HG*~^D zgP0tmO?Wu^=wl+Bw5oA`l^PsIArg2D9l5wdL6(gx_Eg$QK9I4OXs`$M&)?TS%=-lk zezbb4hK^SURy(_H95zR#zFAa>I_+s9jZsG$2%g$Dg)y`V| zQx*JcQxOMFPYgNTm3SV7()Vr6)-$8}*P2?j9~siWgJS?H+sc^7#rP-extoK86p;^* z*P1ni6XjL8DfWa|1tZxo;}h%H~)5H_P2F_FS{)034i95|59l2y$keK?AUJv z*8k^g32P)8#oo@8MceXNxyO;AT`IAsn6OiM!sWuLey`yy-h`xsX$wVT13~aAE*krm=a~gq0tcYj@1rbm zebRR=c62?9ZECu~%`FhmpY-3G2f4uxZ5-pTcS09?tTW0aLD=B8T?6j%81JocQQt%67$DOFOV zJXj*FwaAlNaagZNNj66Nw>Us;gf9oFhuvAGRPPb*y;g6mh$KLSSb|?T?}SZVQ{=mimC6tlb4&*{n~8!KO+r(Y z3z|N}erVJZ& zWPaWPqN!)lMAZR-7w&R5k6!TUBn_yzMbjxEvRQ&9Od9E$0Sk0#_flxe$J6@~OAqc` zL@A{nhJl^ze;;uwg$k9$RG30VkoJ)~7i->9Tv$Dx`^kul>|cD$HO6}U1r%ZQ{Cuvb zulv`ylgItumkAAKdHM6-x^nc8Y?b19M}5X%O2M%|4e0o&k9(wv$N*E|aM77wMTi~8K-(O+FQgIA6M_?6npgASfu~0^NIr}gXD_SqPp&~Ku z9$5YHRPsD+Jp(w#kU@@N3y__WO&wZe5kvMowh`#kocmi10v+)``biU{GuwNo16h8y zX#is%$>n&c7_3me*>=}#gO@%RLR!ZWDJ9PR>rrwZl`43TLq;GiEPv@*^Y#&{R`w*R zG8rVm1DrG}Tkhpqe#2A^5Bap)jLY->pj;lF{dze8sPQwr0`c&3RyWTmW%C8!#3$D% z#lI;0<1fq<`LNAC`xs}JzL|OWl$^Rec3SI<(eRo#I8o+s(wdH&b@Y>u$W@4y24rVz z021UZx~y+ChZ&x)xMbO-!Y5oVik}v_ram@0TW?gYPE#PE3NJ~;#ui}Tcn*iK{6lqd zAK7Y=S1i6V$8j@F{oKw)4q*x`h~KgatoFKt`rgrkqG=ZBsj>1WqZYM^AzOYmEt5JF z8J3)E{YEdBIhO?aiqO{XLKvoY@qD|%?Aj46YeX1vP;qkvBi~Vua)5Vi{v*2#lGJeG zR^|~DCI{7h^>#aM@&TJEsT2;tjp3&W*@ep(H4J+_tMWuNnj6dQ{_NHU09Vb1 z#Gl4+86`@}I-=ys>3VuVc-2aOO947%Br|1MClu)8VYoj$Ow^osQ_GOF%_GRWXMdAl zo+?iHP4kdz_Rke*SnviN@N7OlR}LDL#B?vAoXo*Zl$?sVUN6e+Aba6tr%C*DDlqaE zB(c8ViDsY8yA&cMDu5G3%s-QKl@%$~3`^)!TYuSgy@rHu)-u(MJ*}Z+DSR(U2jkVYa+B1K74m#klK~7B zOhS|$K(=rOC;v&%R+5~Got4cTjmorfHe>>mgUqUztYi*~Q8p=oL`_mj-D0b4+U}@^a!H zJ1GuQ{lO2#of(J{Fq)HCh4U?SE2A-=Na1Kb+bxLa)z=?Am?%mrfOeeRC z`Hd}?V!MtLmV)wemQ?;2u|sR4^7p`E7hv)a@FBCf>76#?tRZ=Mea_k#NN`?gYX2p* ze}4zIO#6xK{3vA`s}!79MgWAzW=t>b@m;*+;F!klKtTw@M_IXTdxb3ss1bm7Sq6=h-&| zY`jgqX1DatOyELGlXew6^r}mn*VE~MV{3LwFLH_3muOb>!%Sq>u&y7Cz&-bDK=h4= z@y4Q2fa&O<2iIcx9`kdVqf!#hI}@g7yTe#*V+i`_9w@hG&3D{}4QBH&FyrP?7>asH ziZcY8c|;yvNs-38g~cm}BZ6+wNYv-G0HL7u;E*!TR3*1;d}uz7dRM5FX&$c!>Jmmx z(OJdhTYEyv$(+H))7$jW2*6QEFEx*g^zSuk)qFU}4_}Akv1A`hyI$BS8iV^K&}f(MVreMAk=sNFwEa@} zqaK5g%Ywxniy5t zzQT28fXsRREWau9*W8n{L{|9di<0?F1E4s5#2*T49h?axX3KqFF1SuBAMJrAkWXX zmOs+gzMltZkO&v0AviCQ-iN=g-T+KMv%j6c^9uiN?b(MY=+C?*tFo5ocv^$zV#=x4XAU_ON|ZgNxbBCk=P>p6S7Uis>_NLnA+rVF*htY3 zUvs5HD6O7;|WlsFX|H zPJ$lK8m^hCkl`mkbpo;KU;xgV-t zb9zlw_G(2NFCOl!izEW$(2<2Y+*?LUp=|C-jV3Vxk{IrQK6;wSqgGm~a2{;2Vw4GW z54znB*I-OeE&~%LO?6|#FB==8Klm+%4c1{wGfxr64q-WvEeJ3?8|SQDhP5tpg^-K$ zN=KRPAI&}Wof92Z?X)x$+pcRGqyiMSpJ3vTtleNJ+;>OoL|ZKrCX-3}+!iBmhpERC z!%@S9NffES%o;2^F}cf0G@1T3mU1>Ivfm4?dx9j0b~VMxF}&QD38{dgJ*0HN9~_C! zrizrDrFD*nhy6adPeW&aswbR61#_ScF6YAUG)13;!t?9rqkM-7D}%rF5?TF#fJY5Gqz~w8ch;66^fg| zph+~7{lwqt2A74#CD@3|c%Nku4R5`d$}!aK<2OeP^Eg=<|z^l>_u{YYB#Af z&X_=x$ClXKJ-wgbY`3$Vo$HGr!SLL00PyJr0pOB8U!b_~dBjk>_ah)_@v-_e@dv2) zq$1hmOKSJ7_Avu~VcQa@of4S%^?8dDAc8sgBA6zk$z)kq(*&)KMa9{2t?=xE4=tJm zMTn0G7nrEYCdNh+uEwo(LDV1_s*R0L5L0LzmwjW#?&q?-T2~D==TRd$gopBOg+l10 z)pTA2yYcH_R_l%-0cAc@6v&ln8M#)xy5UD!DT~ANN5f*4BY$E62&sr!%gU_vJa)FA zc~yQ3R9dUiVYR0;kXCBpuGf9`aJOGKv7_P~azkFrcfs?68RwUv#doJr$M*>QxJxMY z@}6fZX;6V?BQ8Df0u;4+Ud)?>9xQjPl$QIJkEXnZmN?p7-D49dW|}Pz=7}ZGuf|)G zOqz_)7b(vMF+S%>%KATRT%|*nbRXnRxb@yl5GtF!k)Z42i;gYt_F!%mG=@1rk(GdQ zGI!cc-goXZ$|9kN&P!g!bk2PvL`qb@wqTYSzp3 zQS!t^6E%51{QVlgf1M7NG6Yw)F>A$L%qCVjAuG+zy{|HQE*I@fNYz zqmy~zvBx4~cF|R3N*U5;hSTqA#DLw&6_s?I=ba9r==2A_yP-~K?ySJyy?5@_PQz3w zk;L3#v6iL1@6Me(LfQHa8b`2%*Og=un|mbQk6zp!7lAA#V|wox#Oi*~AKge$e6zcN z^$;J!T5Y-2=(%)+5pf;IMp?o%o;6*VXT@_Br6!;?`8jSW!DYM2o?%EECJfE98hD!0K367;9V zGVO+(`FUTcjP$A}#6JtuSGtNr)~(S2P;M^yjnD?-?=jr{KXd z2W)IU{!e{|PY^}E?kb6B_d5Kte_MPPPR8x$-{4<2PhwqZf7f^6wQzq6OALQ9MidNv z7Ye_<)8g5(shY(a6N_S|E_tieS_|7X@DeQ9kn^2;!Y;lDe+L3=5cf=V z>BBAGI>yIZ-gtLcXomih?A8+&ofcsUXf2W|j`61+P};Riyml5#x%OtaNN537uO$x! z({d9mO|R{? zF+Om&o6gYrDRm7T4Qze3$SJM{koVze2exkg^W58=CO1Xm1t!i^A_vH|l{mt>%{=t2 zM=OQ*M+@vGB|Y27#tY30bilcqsOY3utS2OXWa_m)V!iw&KXop}pnQE0`kFSHY5_LP z+v7l5g3Yai0@#8rNVzUQ=}>Amc;cBHyGwH_*)(rF#H~%7GghjhXfx{winqlzT0#DnwE{7nnmb|b}sqQUXC)?7?S6;BxJd*!ck0SikW%SdRPk0(7 zGkcjqSIr9I^}XL>=C6DL!oGAa^k06{d`}ssyw&UEY10aYIFXMs{e0y#ecTSnPH&8q zXDv$kJc-+jX$x*voBIp9?9~z1T>#}Sh^ttwtRzF{+&t2jLpzA z*r4w{O=cdNPA*$3JF~H}wcx_ee=R)Hc4Roy$RTWE7T=o}ZQuL59%@{>90`#wZOln> zQIs{7X_bYM2S7Z1q(4kX#0hECgET_t?_c*0&K;6_7CdkFLCqU3nBS-;P92%()E;5d zqwKTi`?d2TpMZYOnqv1XJ&)d|LU3m@I_qMy3*V6a#P?+MVXZzrj8ghG2T*XC(aI4$ zbC_U6sPz9s7et?)m#KjDM|I#f$)H&?=4(Y&w%e?CdvwlB|8W`b_m(t9m_F-1zevTF zd^33L#WeK!YlCWav@Wk>3mq{8TJKKH00U4@R?~>E4=JcMo&IX4dZn7+|EYp=nJcIW zJ;_D2uO!TEw#axTQY+$Zk%UhCP*1YL>tLf;&ik}kdNT~!C53aY-WC+{*h3hu9GVy6 ztQYx1?LML@`zsxq4b`7>@eB1-MtMPbFvk7sX5zOo5)w*D>%b^`kZ9|d%Y;@EUhjs? zvWJeOO7*N7(@pBT$ia9hCPS==ggAy_nwtb%$5Exr9XJkYYn$z`W7~8-a}k0=W$&Bk z7K$&=DVn$jd7=Pii<}o1ILX%20;B(EGsP&AinXL)2Wk#q?Hh~lHm@+HMR$5Su6~_m zZ!ZCA(tCwn8hcLADioCYSx+|i+AD(D)ebWpOl>*=sLBE}5a35SS1V;x_}4!($=p`k z(uS_o?!Tcu{408zZ8!C4qjHvbv9!uk@v0TNk@o8~jd<2ZZ{2AdvD>Gs#7 zc#v`Oq>3!xiRs;}jEl$LJOVf`(EY($%EojkzmZDX5su(&8?x>uaime%W;{*pjK@<5$A^@xV%ylUu4Jz39l!z{?P2)ozZU8(S zKdNcHmh7^FEzBjqTAQ7}$_{|?jbk)I!#TEL?07w@`(=6MC$_M_NY>9h{@@X|oY6k< z@ZEIuQz5D6hH~Ra_b(Z-?9Yzk&u61~?^xph{*PnB_qUZm8d%>X>izjT58Qwt?@@Bn zrB_a0I>In zPW$eZh%`{gRv>T|L_4zury3sERBrZdO65X2)Qy!vz`d1AQZ+&JA?T~9Xw>C(8CW6B zo_8JSYPU1{k&AZsh{Q-VFQv(_6|RqKAbe;do7sdrnky1dzg#O}wd4!3@LZ^p zOR9Pa4NFWH;+nho+Vtut#^HZmGqXmIMD*4t_08N^(iXCg4BtUFNPb8*5pEZg(+iKE zJkj74NBSO;_P+n;83=ie>A~eHsT(_8Z=dB2z5zXNtS@K(suHH<%Xy6?(gMI#!B|3g zPCmK*=l-LbK$}LYE`YZ1C`L~S#iBhcoM@I8;Ej8pWn943z~weTFE2YZlhc?9`@M~A z-1Ky#{LEB`64v^_2G~m!`D^IE9wt%4VR-zC%v|pG`?JP>{#|nIt-oSx2x*%iI7y8% z%y{u*C7%FX=7@yiKrWX#dVYa4OBQ;bz=6y!*Us`}Vw@E7E@{{Y5bnUsC5;(Z!y=5N zM>`J`dHQeN7%lcRD?yXQ9_+^^IXwvdQwS6#Cqq5+7A zY31T}{C0yIZ{)+n_<<~0HahKEwi5^~4Pjbr*QipSTgCt64{=E{LfC+!155b=2`9^9QT7s-2ZQKNA1W!3U;&;QI~^e5bBqUJ5UKP?=9(6UB!S(SYvo#uvd< zK&&`yU|ztDH|E-0yc^uTdAmGP->f?6jDEx)zaX95LzmsviFk#S4_TC+t~t%yF5;*E zE5JoR@?~{`!`tSk@Zv_nsYto*_uim>(Qn&L{B20@)i?V0`U*xVH+ZUmv)|dnNQqo} z$-IEv8?dC{4ZVV1!N2FGn$A4QJ%Nvp@u*4=C5An?7XU}wxEFHIRlyPnv4fX3AKNF$ zc>gaX&GNQAyUu~Zl9e!a!G+r+<63?tfcjpN-YI3HKxRTIOQ^}EnGi+L)JZp3R!#Wl z&kw)7ttEpUEN-V4;wU|GW#}mmaSZp%eS;~17ctiHG^It^&WVa!xEL{@E@cy^IAto* zxtEYd7(5HX_W0`M)$Mi&XwVd6v2g2Vpp&~S29gG&yE=H(0jo}^cj{WFSP@#Y2V*5f z;Dr7{Q#4#Zzs_SEA7w^$6NO1<|4zS-o8$Kk@+Y324RAF@U};J=Bu#Z3Zh%6)qhBlP ztVkpCTJF`@MNKYZ^&LFG< zU+qOh&Z~ znn`m3Wz%#%H`pYj(|MTTl+lqC=LF8h^Rd<72CoU=WMM1fpXoV!DW-+oSGt?QNN-&& z^RXE=S{<*`DBFSlJc?a9f&GtLeLmq|vKB$=7NM0R;N7H#dXd=udh&fUuKOIV6lOoP zQ1xOW2uvh<0fP>Z_btu1PCX=~hyAcLD^qVrBkzuv&G4k(q9n!EaSSh3pXZV2G;hxH zsO&`da*?N=fV52;S>7#u6OD#2A}nerCwnh(!!%)duZ`ovh=w4;i8rE)^yxn6u(3tn z&kh_TAD%1ZH{`KSE_mwM*X&w@o7O7V|tqxz*XEVI+-|!VSse``HyB~=klBbgyT0sc`oj6f6wtQsQjhF2H{Ys3MewY$e$54qZ zw#_`j#Su|62Dg+DQLoQ8u~5SaIY!NCF*}wD9p+X?gr0L^2Wnh?4phanx+yn%qojE@ zgm!d;TbnX#=D3JKTzf1_bdx`Qc9hX~9x*~y_-1%}+Snr_(!N@yEh0$2^8ic=kCVBl zCA~#1-~x=TPdWjIauzc=GITTgiXX?^tO<;WIjgMXT|w=ZmJ&phC)mra=fgCCYC6QYQlth7ts9jHgG}|cBcR5lfHG~O)R{X6M-jqqn zMja=2F#xJA;nN1A`KTene+b$;uas$o4Yb&}kufox8*B&$jhm8u;u0 zH(l{v>nPmrqBFN?VWJzw862`IrkK$*y>iJdCtik{TGlDeR7JN_-6K z)A3-a{y02lkyY}1n_C@w!Qh!5UEAkACzGN=vnd{r{<1A12-LN>w3egktQ&2N^HAh! zraVU4g3ul*Ydp_Si1ZUvPlzp6;d0Wn z8GT{npq)UD^R6ie170QpGEnQM99ZU*6E(+e5RiN8gpE(1HX*FpogJTSF6YKK$%-!J zuYO#I%eLNZjAe;dZr5xS=ssF+>S{~>_1eJKZOplQ-f7YI9=r7fPb#NQju({62?+iX zb>*iV(ckE|F&BxZUgd9}3;1ZCd{11$B{t6{O*#>tZPTWTlOYQxeDwT8FK-w4ysRp} znh?oeXtT#;5RXl3m$jy#XDY~9FYddS%HYndX{~{OvfCiPwL*-C=ykrz@>Pp5;&9Tf z$qy;VAlvwG$&e}HfAw9;+9%KFUKR!Kx6(ip8Kp3{It<&HYl6pW;p5S0 z`(2-nR9P^z(8l0i$$}a?Sy;yua%?1Q6)Wb1G>|Gq)o7l*1oy-b3bJe$o)CCER@wenB14o-fd2fidy4~y z$aRy0`3ZbY|DwNs-JX4I=Zf^3(8kn8Twhg(?AP%zjD!#Uj*6i7cJJBB-&;9q42N~h z6HlvLO_W$$8-}O9DKsJJ16?}sNSVCmPkkNLU8ap0XF;y5WGc!gIjHrvVbw$CSa|@; z%=3Kj^g!0kyKG#MpU}N~>I9(KEsmWb^OR>4m1v7LZhJzDa)Ilq+GaW{LODNE6s%Dw znNnuwf@`jZtGjFBpKT`&og}yrJUB;em24va)@T+qW$z$n$%o>3;If%K-zyVxELUXF%vX& zCI>)WSJhN#ne^i%X$vz_DW76Jyrl>zvuP^6jfPd#?Yyzc(YyK}?bWD9UAkJ964+hd z@-6=V*?Si)H*zFP5C*5J=l?%-cFsg>XoN2|aG=82TndD;~PqA==oQFSn&Ws|5irl}E z;+E!TfWeEMj7Tf5aG>ucke`zo6OOc+(+W3xrD(d?19`}YR1#WK>r->WhYuqI0#lsajU&Dx7x z8SEo*TIS(AOlX?gnD~=F4lEQUSFt(K;Ct#(+U6xwJyjy40$nV?0w-2_sBaYw&noek zDQbEo92Jad~li)ep|=kLiOe{KtCB z^A|t0yh;)PyV%I`0`k)YN{PfA@2of{7uV}vYEhf%cL|=W5bPc%J;Bser zb62^Ib|9$JZi$31()85U8@)NG&wr7>P*Blp*&J<$hH2?Nnit{R0@ z)?*tI2(!6oF0>Q`1Y~BNi)eCmlAzvd? z&3ZV7jWYV*Kl;n|(TQNUa=JIMs~jowjqe9z+eX=MsxpIN32ZnFc1IldJkp^ZC+5XD z%!>e&4?0OchL;R_-)Ku2>XpWp#2Bt_5OK#?mAnpGQqVm8t#Zh zCudi%;)$+KSNS@%Qfy3|=;l|oD4{LFNGNJ*i(mD?!5eT08xlGU8t#%-#vi4uNksa^JNI$|j(Ao!L%iF3}H828-Cm04f!3 z-!#8YK?6a)4!B=;fenM67mp&l7qwg^LK7kkUN4SEIi|kpUnm+%+AM6D4QPxdmI^@+P(4hiRwn{-n%Uu52>U@8;SBX$F~=DsW9cp*(p;{_A;+&05o@T4Acr;$U1n*MUVIgzk|MyJUlMEG^1eYZ)!hV_0Fh3PAP)T5G1j)d3FocQU$t0Y&qtK!brH4 zE#INa%@jlS8a}_83gw+{Z%pVmEaV@q5$gb^5EUL4b-`1dm&V%}%oW^<5^i8P2}oHc z(2u49a^jjM1sDmcY`0M`4!c@yMAlkEm{!85p3E_W-P0%ir;M$iBz}ISH_5> z)^pKt`I|pPY&1TloZ|lAyaD2{Abf&O0CeUaU^9P!^|mB+QXVW{FFesttL%H z3DU+H9@aW##3%GU$E{>hK2ba^@{PdU3DX#lx9%;rGtgAUyjf_KEu6JX!Y@0aY6=2w zNp}093=OXxiFRz>G)HhKlun)}L?7HRO${`BZjuX62o)1=>HAS8pA98TyAXFn_60 z{JYjdn|J1HmvDm3<~`P|yYn5S&HkC>aD+^^SKczGe@!@gggz@J&`bo*g~dm5q$=Z# zBNFf$1Vba4i5+@2eZbt8Z0aWy;!Va~Qbcig@!kUrC)1$x@a~YxiC6&xf|i(qa*O|; z|N1#Ce6qYaW3d#8fsUp>=FT8-C2^~u`A`>X&`jQtPv25#LW<`;8Zm&U(EjF<_WUF;hSn~*rafk5~L2F)1EWNLJ*bqxHlxvBKq$9?;-^;>$9qW z_$a2y#c@5^vAQj5PEd(_s%Dj6N5Dq1pCDkxNK;g602Y-?RGq`Z`D?? zw<7(&dWC(|lHrHmhuk~8`N!=?f2mel)xxWRa*t_3jnOw*sJ{*#Mhr!n{7$7CZVfji z&U^(qdPc%z&DakU1XBv+Dg~iWFx-x)dT$DvTc<5!M?p&-6pC-XnbdGUyUEiwStpg! z3^GZMP=jMbuJ7X*KGA-X>9y*ydmf=##;xm}|9{+S(b|3N{U&c8Mn}D~z1?~z_Vcg5 zdZ$cAjfYrx+GzZyk>K6Z2?g z8HmTtmPhea(%u;N4!5J(&wg-x!DBQ_k5cHoyGYSb>Z(eOEPb&|k6*Ekw6EbRVNH_z zR1C6PnJs(Im5G;`JXC-Ip4V6^9BXw-l4bOjw1bI^q?)r?nSxoEcN(6ZQD-tqd))8G ztxpexv|_zh^CM;`FxjmO+E!Q}Q`K4Ch-`TR#~_ymlw^=#?Bemis&4Fk-B!|d)gPI@ zhd)2~&*zgMp^NY1EOVHmm6r6of(GZRl-}s`ahEPhFEXo`eo>LQARbgsAigOEu&ipp zDbJF#TZu6hNk?#NKGr4+L_+OXaFurx@3ZQBy<8`g41V*HeR%sng#PHHlI7Ph+QN$1 z{yeZ?obuyQ?{FWlM`O-@EU6!yZ_sQmy3C|v? z`_S!tQ>g4n)VdBQr()A6SFam4v=Taj-%ZeOt{oVM!}hZB>6$<|g)CUOmw6*ZvE6_E z9RGOJlO3!-mTyO#n6&92@=nIaqM9Lr*Kiopfp&?IDP~7)N{l4Q~9d`{ejF?Z1Whnmnogu+)$aRWJmEJaN|}2@!}NDgeD~FzjDZxeT*Xa(XDRk zxX2HADkLnR4m({c;2@Itc6rXn}R)_q$Ay6`UCC1wbwW zna{!^t92!-_|kI{vG!hl4;poGQFJ%r0=Y^yyPB#z>t~j{!CtYAB)3uCY295%+CY#Z zum|hbl$8i0=1NH!9YfvK`%U!r!mNmh$jF-_Zx9-$Y==nMP1l~W-sjLn86gZA>Cp1H zp4E{jfB(*$8-6Tqt(k0*{bsUuk7ezFiV%O*X2u3@ruPG^xS&t$;t32jy(}m&F6!|T z8psLpxH>>+_I^WNdB7CSaiR1VN`+?glVo)jH}Qi-Uo=h4gllD+P&F-nG=adF2)IZ) zWGEy!{68y|^}$fzT|>o3TcOKrP~r&)*~Lk*I0Xw4GQB_16q;?_ojmW!k#@3rWXrf7 zpe$zI@6?BRA0{lTf*)cwnxNBzjW&rcY)x#uvr0~i%No22$*yqpnt8+u3cXndr+C2l z7-i9@)8?=xoJx@!sp_*nMlAiwxE)_@gh3=a*qk;&O7jOr&G>CM;ACr=iDHZj z!`DF0(s?l;9E@G72Yl03`UZ-6UO2&gwu`I|u zw`+z_6+a7pGZ2`y#I0>jS8r_NZvWsS52Jq9D?=CzJ(M3MNFb*$dTApkK00ov=-;7#m9wFX@ z5Lon#K(*3not3irs{v}we zv0ZRd8nI1)*oBbBj4H_^F){#{v8|GjyQXqY(OrtxQ7C-H)vAVA3dU$L2*#NNX<@-I zO)4-%BrE#e&Sw)rmIevt1tAl)-OQ(>pwCC-Vb`H?PWGEkN9*n8t8;wyq(oy4R;h)w zHxivDfZ7E@7f$mFxXh4G^c+A6#Q%zpkkRaEQV=}vOI;gL0kXsxX25oJB!*URJQ#95 z{HyAz&?HoF2usyhl3WkW>Mz=GkiuL_N=0HzU#gLAij)2&kjnLqgwvmm$+451)j7vM zezntF_CIh;38>x(^Zeei5xygwLne3^!~DXHY^vt6cQ$J9uGDc(J-9M>foM#vgb0|_ z>F7P-!w8r+UyWSNBR!!njn0x-%Hpx~0Mg?mlYSo%p~x74_l3dYBoQIP06IBe$5@bb zGAPC6pm>)M!z8^Sk{@Pq+el4vepT9Y=2Dz60cmu=@*C*bje~&pT3H>R<{O zYAHf5G(6w^nRi$If9jtw5fN22l>`sQUKjuM8pt7JBbY6B>IjJozpClyOsBQE0v1B- zjhpxbNW_Ay<9vL>Q?v2`%YqxCO&9OJ!uKao;!FW^?{&c^|40 zsdYwjU}4E&sp`ZdEcGCmwCCD+`k7#L%_?7EXBA^%Y0$Y9%9JT9YYi2H`V3hx-0sQ1 zT~7Wmd^R|dYBHq&~WB06+pSum5iU72W2TTdoURuroj{9?Ctqa z$Cz%wBisZ|cz8Z#IF8$SlcT|!1TGi$nR4#IYJtr=$i zN<3}M$?{uwE+I>gz?l9mY6DjcpHsI}+qsuW1lrpFktm3*B#Ga}`uG<+qK`hRfy=+Wf;zn%WoMt3m5Yg3;+h zNt6-vJ@>&<+nH=~pe~=Z95M7HWKRiLw`Dt*6HR6_pMp`~MH68T8jPwIx8RmpzpI}H zTByF|gGxQwSRPvgsV zx@3PdgTgoLD4H}C793eDtj zAf{n{I4LSb<8`}IUjme_Lui4HnuBP>ZBsHC7%M}m*)ddm@9<8Q6|6j8~-YBI*h=sbpT3A2SF zD1^zpd$czE1D>bjIQp|;hz3!T1+6N$s^83$Rvbr@@Jh1rSnAd^U80p@!GjD+9`H(c z?So|~Hbl4*O#+euFo?sYx2_0JM$!a}G4&AhwG#qg)7FFr9MaXK8Zu+Ul?mG#8bup4 zr~>m5H@C>KvAT4`2k7#=;IzuFMaVn%%OmBK%QGyOqHtM^yt(*Zp)g-dvbGhS0o@(D zO7c9^`m~rNhrZjL2?wM!nQtyoAx}qX=whbs_Zk+opXWCyD|A8&ZoXoj-Z5KYo9A#bA-N8xz!GMh0*`xoXR(I*{Nv2+ zeQHeqh~-Rnk0|Gf?R?n>{pmkjZ0$_JnA{Yzq72~Z*TIAN?P%?aKk5Dc#uh9u|G)co zP8;NyNL&kaX#TrA(L!6l45n6I>5m(cZ4AW1IxvlmK;yy6yI_FAU4^uWxI8g$e zQUqHG3kNp?4+Z{82T1O-H2YWofv)cDjqK#OCCWEb&ZSXiPb*P#rU6aOR&ooI=VtBx zuuLduluqj8$n)hf^7F5l)Pmn?d6w%l}QxHU*MA2115Pn(u{ zOrHEUs6^fpYVROdQ!v2s)gIFRRfP^!xmV3-g)|?$N24$VIu@q%3_Axis2ctkCE=M| zCPU#&I>kf*>D)?6-v-zk{1w?_M+&kn_5VM6+rt7a`-OQt6v%23(p?2U6hmE?z3mPp zf2fTF&U7+I_q@PU(nTJfvY6AWz?zOqqb0 zO~a6XERW>)JzWg3@SxcO&P9WEJ`HHOOMW?WcL}C0BOTGdrT5(&gAJ_1$)M)JoiLg- zL}JWm>qm7Mjf7OnT#lEm!b$G{oKHmOqO+sT&wSRT6x0f``@;guU^SCjUW8YCGjo1W z>|qPc&KF+WRX_7ij+QmOm@?OT*G~4F!yaani@zZpIKWDlfS8@m!4ESM-k4aa`03Bt zkj4yiyOT_wA~ZLV--6>!SLJ2EDvXq= zXtRiI)MU2VlQlTX!UK=swOz{!GhQ~)Q}0<^S__M4I{s2~>l~}escRdhu*ph!)u+S< zTj&p$D>G+qps3CA)DLpFz|`FygtA~!F zOcovlIc2vs)ergdVYtOcN(O@A&co+wCyHr3*@-g~8)!VK|2|e6=YT%l3 zH!x2eAp#2-4E%^Gi^f5cOhgP*8cBZDyBWvC#ucmRhE%f?>VUZ{5XD4M)0tpV6X?dP z6k>P6j5c*R97UHK*1`q8lkiJ6&6GpAo3_7%SaJ=?I zT!J|c5kqwSTuCh-9S0?&WI#hhC|Tn)m>_^$=8_v<17pzNvEsM~f4jo57rvF5SNUEI zT*{fPtegAy!`8sikX8?c zGGRsJ)vq8vZn>8`|9Fu)LC7|g>~g|~1)MWxx%us`74&A{6X_t(Aohue;)4sYU3DGE zQsc+{5fc#;C=IIKNR=QfLPq{xM~NyK20fk`&q;6L?;xFp6u`IcrS6AHxQ85CG#AZW zv^Qqj4P>zH&we}uBplkVa0sdr$e2+Cy9U{((ajqvn$^*2jHH1&g658+SODF)O1IvI z-XHe?a%x{Pyi)pOcROzPau;2q0E`l?{`3B`ij`>9i#amT3|?778sd@781Vgj44@l? zCPoCM0mrKcD4E*?Pjq!&(GNd>On`}P*SL?*6CsxN3hHPId%JR~$3l;ZT!8B=)ZjrX zslzU4RNEYrId4tnCuHcPfQSw+>T`uW29jvsLL8r33(BHt>t; zb^mmjSr;iwHx>7xQ04b$flzG|4wA><1fxe_s&ZZ(EU^-9K^>fJ>z^c>VUC@EmW{6% zD@#?Pivx*-KpNduDOuIP%eb4+6F2q7Q!})TwSAY09q*HncYop5fQjJ*Q7(%cyTRPt zT!pMcCwXvMQzNoWTKQ64u_BAm5d(CZXbBUe z?@u>V{2Sv&-tJZZo+i@yS((raEmydry^$(MAyN-j(;3}#SplAMHM`cAU?`~Bgc;0J zI&jB92w@1A=YILVdsCT7CQp}h$IVC#NpsnkM1!VFu?Xa$kUAvmh$&>$;xj-tI3qt6 z<&+!YQemv&#v*UdB#HK-!FT-Ca!2X*L^+xXZ4XKFt55LLHy_pM!@MXD=+*p$HeZm+ zCN_z|B?&_{l41S{_00bL|NFC(s-!)5bbr`Utl2PszAhF%)d9i>0z&?DhBU5JuHeWa+$k_ng0p*m9b2$r zk4>WiqZM~ml5cHny!)_k(p{-)7o_=r=z8H|KSA(oAcP8(*@NwCppCWBwC&Zk@%+ob z{`%|Z&%bK@(OI2^Za;oJf9d|$&*!CiUY>g^kXauYjNNoIcF6wq9zCk-e*EZhcm7#L zE7&`_QY9VFJ}jw=15HbxjL+=W#>ndvC(ocj#P+u31#AgeB7hnluW9wYxd|X(mMxmjqbZr0U_x4w-1je0mH75 zKbfY3fvGPIvug$x(gs{aiw*Sg!t?by{#!2VeW{__}70X;YA{`2SfV24Q9ieQww%w~sL z+jGm-$0Y7Q{F=h5@<5LLBv9>aKb~}lpuigeQ^n#INm$}+!yx_LF%UV)1AN8NSpqGl z4=|n*j|aqLEgGLqI&Il4i-YAH4lx8SP%Zf#=cBu5G@AKufk)5M}OX?sfFWu#_;dRnVbMa6XqVBxOJ^%71 zb#1@rhf?N7m}IZZA45|Fe!1*Zf5|rNq(qU&^*hx=;v|DkvaNg92E@k_&Tr(%)hwCy znayt2VY3K%#h@XU?Da>y9Z=%_>B$s0TlWd?^sqq;g9H7LeIe|$<_zf(oLtpAgxvFk z?BdAvA>)M}$1w`2Xoh|0!w~&YD)=X8#pza$Jt0w1Zy((>O&E`uwqUmKG-a5^z5VV} z0UODVE$SEVZMX)nKZ}!|5nnpG$`a!(I=E3D_Cmj>kuGCoS_UQpU{tydoYA*|4jv*R zD!Tv>^)4uI5&q7e?ZYWQhvS}0V5>uZ&Vu&l#JRZs*;-RTX+*EeKR$fc;sP?yIWOSK z?>nG(gW0UnOoQTX>kv{mxRE2`ywtG{kKM+kXb_=kyO>oD`{ov0B*K>Q-WXBQXY)&{2}$Ap3o^|DR?~U5qJ|R!)ac*CX!!v z3%pQItQ%C{BZ8x$xT2dQbLU+Hz^^uHjDFfOXKX%yoUAP959$eb1}$fQ|IKL8?**gi z&%DzwckJ}%AUgb#`4D>^W4s6S}wA(ZBDvw z3I~*^ZN|i1rnQ5GCx}@ms=Cdll~?xxXn@z26zC0>#r|BuL|3cIsF{|liHnAi>O`wH zzAf49`q!O{IXQ%w zY9mi9vcfogt8?Tlf(}57FP5ps#NnDa$);J=l#kvM^?QAce{2rNK^A+-s!9xRD8b3i z<{JQ6K&HQG8~}B>jmAp_8E9W8;+|r`?NP>8^7wd}umzE$c=q4CNXue2L-9ztA6LK6 z_kmP6S1-_o&QqrNZnJRJ(%LBRu;;-2szhgV zKDR3AJ0MqQK}uAqBHO(XrvY-n$FPt16$KP!Z0o@(UcgmSr4;?-c%S8 zyr@lr!uuhbRQ1Xg_oq0ce24TJ=v6sR8?oD$EDGQv=9`zI`?726e;m($5Is**V}{=D zjFzxUKti)D60!&jAGm{I_hffW>%QP0dh6N;8@p<$4uuVcE2W&*HQdQ$^}) zQYk4Q#UF6>|4>xDft+nrt&@3t|MpU!7Ke-|G6^A@*!^}O)*QXvdvozqKBq=?H?^kO z%(nGuJ`DMEj7`jds=-q$-i+-*G2iWn!I-1?8+f6ZDm0|a+m@sRf!^u5+2EtA2Q^|2 zKW7Yd$WStIGm@ULHKb^=O_TK7S|lSzMa|o#>&xvg8<5DzZv{i$NfI>9y1gp5EzFY8 z+9FywBfULF@T#MH0^wD~9u;yg7PH}BMgH+N-m4N#M~eax1cj&*tV&Ku@I)nM*;0vSkJ>oUDl7ycqq1~Rf@3IHy?n4{I#mt*d-ht-|CCnBmMSy( zKyAsZM&ZRV6dr1*4$8V}!o-dcl*$EeCA`ZBXc#3^Sc&@)S+5z9j=9zB(Ga5=E+q*GY{MZ> zV$HU>ymp)+eavGT`5Ij`;U-~`4@nxr#E~vY<(imC>)FmW zHwN$}q4A`g{rb=BIAqkzAqxq=9SYK9aP$l>FDFVBlo-9mPl ziw>mJZM)1pWN0jJ$JyvFrSX~NQ2MDUody7(5s9G{VDtkCzb|5ZBaBD6#dU~Fwc+WQ zF5!1i4vcdeOqoseAvj3ayqgUM^`u|~Ur;rn8EAv@4LRxB=Zo*MNGh8&(BX+8!G>4I zwp)}|)zh-2Ca?yMD1)Ht@Y1FzeelE&xvd(qK?5+4jx(%8PxucA;LBI829+--wrMIX z4}}Zwhpz6I!|nu-hmPz_*NpShm&3!R4>9qD_+=s%tXqmsl{w1-w};$EF#rW-2{E&3 z{-mIc4La2<((oKEcz=5TQsfRMP$j)C^|PDkl>LQq z_7^vf7LVp_mpHLWSK7AK1v~+LiU+E_MR2SzNbv02RUEU^^&RBIc?wI8GTAcir!U~m zGyVI32X@`yW`Tt{}T97+os*cC|NG_x9e(Y#tu zK4bT{JdRs=++}Pathc5)dRFTZM@vo}9 zM6Y^{0zd_V6%7C-wuRm zw5k4;Cw@21EbKG*^kl zNr7Mi{hdSN>}h@gPb!Of5TvX9a$3BjC)72lrbp=kmuqrty9Bjk&B3-j+GrtDRzNw2 z0C;mSlw|I_09X*iIT)o^{BX9vg%DuND=L=9LTET*S?~= zl(50U)-)t=jn?rNlyWjfOI%U+Hq}9qyUn03w1D%9KFH>xhTEzSK~PenPxRQGKTqb4 zDuFHVGyH<*eq_J*yFW@a%svJy8&YP0Sd90Y!>sx9R!8zGsb)nZjw}iY${5BSuo~Ox z$P)@Ho{mnQY1d^}mI>La zjf_^U#&X)Ru086vXPtlJ>rZ9zn_+!m2K2=b|HcRY@)et0R%mrw`QDzeU=HNp(Da=k zpY-xF@q1Tu%vC+9SA%{_UN#Ory#cFYJYAHC5VdPP;S^#)scxFI*fP!~DBUeM`aRT_ z=!&8Ycp&evGb2XuTYf(LM2^0GwI?55Q5qMg6(C=cq+9S~pe0M}-QA zfS=VX&qHjWguEx_iqaTlyOW{~ksvK>n#BQB3M-(y_>gL{G!}f^K(qSb;Q&MGo>wi+ z5NFn&F4zMo$%JOr(l>gd1Yrc9al}(8G&bsEGP?RfP7?vw=SHX%pl$XX4g{r+f^zW zHrK(eh9a(Jl8{jXm$*!AyGLC4$w*j2ChV(6v6msOj^k#u|9|6EW$MRcjasRPEVJeHRzv5sJ3LhU%(qL zS{eSOPr28p{S$CH{?0p5zQ4;&NI>x1**xGXn0itx?dn-9vu?#RMfL?Y0edpzER@AE z*H;sxawwZxUXU9-l+BC1wbu!H2%S56J+fqPHEr-BkP?G=q7(W1;(ZrP(Y9^bI5O{a? zYl}&)PKu4382`>=S^ys*xOI&qcgP;1;~34LJPDnT`dmw$7cE==!{S?j#KGp^udB~4 zw=DHrRu3A5uqMDnR*OO6A+3)GR%8`KHP3C$1iS~0Hj4nU%d0`1zWYJTb#{)tD?MVE zC~`rMfyy3@84R3?jbul+BQrpe9Cp^=S3?!(a&D%gw}5mj!jAoz_0=a z0zhWtO};FPq_E_8(;7M+A2M!$FE#PnJzwUI4)sj3bfM)lr12VNOGusN`=XqTDjrds z1L%uDN%c$)14Vf{3OUTrY#ZM(zG)V8Mn=+#K3|iiy{?OnA#+uEw|$lJD=c*SV1u~) zMb4>%Q0Osq(UhxMJ*XL6MrI;j(hJ7uV4}J7JNdu=^H~ht4GJR%+1TtZ{B+dfY4G<> zE=x6c%El1E^sgWPG9h9MF;iFwo}IK-9wRqhEA5pWpd^je?jWuuIXu2ZzcTr#@YSCv)XW-UG0x;Ur=Ub`6PZpQl;0*anYZS88iL za4Q6_|3|(3c>cg&-E8z(UH30`yZ!k2^InfJIM8;vjeA$`O>F$y=pSx_RrnBkrfm0R zm`@)4+8pLc;ZV1njtzD&o<2cf^~y zjt|C4m;+&#l>KGbBC^Lb4uh^_k0wIReIy`XxwJ7xp%y23Bp%I-s+}KJlMKUy1pT)y z(*^Tzb-E=#ype_)R-BUGd6I)hN}-ifycnc0#29kOEtt&mSO24Py>syiqF$39!>hUM zQp~N1JFm*;1i=wyW?1uc(Q`-hOR~5Ng1=N`b$)Sp{Y{ACr*F4#JI}6L4j_p&wQ}F3 z`Vb>%V&6__7Qt>-J=O_)WfgOt2S^jFz41kS3&Er775I(S>+eAPK2Zz(j0WWETk-<+HdF z=)_XGpv5m2-sA(9ES4SrWohU8vEGzncpD5yS}6`l_|q0o!IL`s2uFI8wIJKqZCm2hjXV{aEG$O_#SmYFxad}{^Zs)*} z`lCpi!x!_A##!>1e15wqFvF1w&QK6zJ5V^z`TM;Z^%HGNYlMf!J<3kcNoi2Q1jpOr3z7?T7I zpcgrn25bzITd3^Zu2w#_AE<>v6YYjsgCBg*(Z-t8Ai{(e(W!`iP17!+7XGLnV{Y#& z+TR$>&rF%DPkj9ryz^qa9HR!@-R#K~E%AFO!<7f!WQhihr*iElkuoZO5B9QI8T8_g zOi}E~_`#X{{Lty7QHO0OuDh#6J2K5b9%1^0=5uQsMCXFdz)T3^kgrVw>E&bh8DUcW z!T4LHo#lI#qW?JH(c}e$nexQb(SOm9ZvYyi>yDXsCeKvp&ECD_F(^jSum(+@v zKE~Uo(Mz3ILx(8c-lO!;eC80X=iP40;Fl|8U5Zl{D(^!w`Slt$Ya{V`2jldpA*X^x zx~rQLAW_3+b2fnq$Nv(j3?L=~0RyH7D+zS4zNG0IQDdT?t5~vntu<2Z%cIm`RMSF0 zOUYix={HSDfn+I$On~tx@Tw3eo7K!G*Zz-GAbE?i4 zee4_^oZBX%4y|{@-sOVc7Ik%&^H^8{NP18!q8-5n%azVXN zI_7n>LtC?|@j4Hq)U$~}AucWlGdkGEK_#(?nAW!09iOP>NkHDY%f^@`e@NS<-TVW> z82I}Fw@?-vECiH? z9Fu_Q9zC4IlTD5RCB~4_R1nap4|4UG$EeTTKzSQuV#_?|LNQh}oCekf(@bGT zgZ&^+{;0ZHwG`pF?Fla*y)*R$T{l^0xaM04_t8sG;t1`Y-U-UYIu%#Oq{ZBQm@*xypG)-r*VE0_OgZ=@!JCM;1&V6FN(2K_4Y*5E%4Igh8 z!dX3RW>JKOA=@AJjfxU9dwodJhMw|4DTy#j{`L&7PRS2ufoo>2rw{z`%(H1_TaP&1 z!6Q=8+-lB_eQb_i^+?-BMe|vLe#xH~<+EkZ8`oJpynZ#4}8E^;ZTGmP@s_5Bz^!Y0{j`2ZBrZZ}k(xN96wB=ccA2dO{g-v-6 zrD{Zg*9r!25546y4F$^8CwBLEuf?5UCa#;-PqVYSIZd_kZq|k4!QLlk1%`tI!ajRm zjOTmZA97T8B4nY8;CzGR`uPqtK|4IOLx7Hc(QZF(1imOn)o#VA+JIy=1EB4w3^e)U za1MAb=qOdfnUyE3{^S888}_W)$imtU&`4O2dnT@<-Bh*KhX}BIfgZ<{JJN4;pe#=DRxQJPK-AHTvZ-T>ql40?@8*i;V~-$Uy;Q5LH) z{)!hN?P}bR;0=4^>f7F#8j;RA!dGV~*=fvaa3n9A3O4}V{}Cw4N3EBN+yE6p1aUk3xli0MvRD``x?Xz;rdqrwg47G5#%@x_=vInsd(WWtw3;?{9GDu>j=4Z)`N# zrmLJnftT+;n-kOP2J&pV;_L>4=1bYoET_{jw0TSZ1V$Y()b7QFYJReG;BmK zYArC{?Y;||D(czsGM{3pe^WL8a<|CFnXpjki<{5OmAI#d{;v56SZ|=uOLP;Bo#eO= zDH|Q#bmhnGV0C5aoX>XeZ8vdeU&d}iG1~xFj6{gi5wh9Lk?G}1qS7&8%;>g#S&VC? zm#9SzC$UAWqcipzn53ZM;bls~CDbgruk` ztKQq1J7ul6VK~yt#KPjKBQPw2lK-mVb&P>yuB7&y69cManK+WL6l6K^!uf0ccI!_z z=$vSoWXkq|E-7egvJSBH^H$fz5&Po^I+AE?Z{5w6&Vx)DbyqYkTz`ucY^ECK7#N-bJY( z=Hi6pwte}W)$9Pd4&Q1_uXpGuiRd-nmMejmd_m>=e%S;ds9} zti1X;LF6pLbW`9m9yVpusPfguc)lsjl!VC`b)oF>SvQ=w{^dzt;Ii{0W?k42r{q0h z?Dv#Kp11jx;3Ei<;1@rJ6Qx1EFS#MO^~n;YYk^MPvZ2~@8m4N22Er z2~}nx--8x6C~{)1ig&jrAWK$x)A zaTppE?R7YcS~Cy<4;~CU-1{9h;z)Xl3~<7-01G}vwDD+KhntskUpcpvPzz#HhnJq-EAq1-s|r>e@sNm_d2E5Y24+FE1lV%ZN(y1 zIK`(PInepDD3pK4KlH-6hfb>{cy2<;k&*OMJV%KZx%lJj1(q|;!(Zh8|MQi{DvSyF z9hk9fd6k9EpIK)wkc0D%6+^%UI+tDmnLdjzS3W4qq)|=mwcoR?kNI>I;qYB*$57kF zDgAhcXYzna#<>0lA=B3qJr(KlTOBO{^TKI(y?b~mVJ5F8E8A{xGlV|x^j1au>^@39 z8sl}9@Up+WUbtQc?Cg(=x~A_}JQzf0@~gWWygkrllqCgGu34!PSKvmHJ|2N=M6e?o zFi7t9Jxn5@#ZJCmDEJj4*D%{9wm|P;Q#eZYn|7Vi6Ox88Cp^=J`N`N79HT2O$bSFf z`{RB*zo4?hg5W&yZ`D78P?XO>N|5Hwj7r65MizcGD<;7fq!{g#!uzA3)v=L1rJ!L8 zOtXiy-Fr~Oz%#2TEeGeSCoVjK#v5S?7RX-V!KBoNvgvu`hHM9Kq#938iG(`|w1hP2 z(T^S`BDQm=tgB)=suS?UufIcl&!s~@?>~EEDG_ijFgCIMm3JgZoE7rySGW#0pPmUR zUXxvZmM%MiRcJ~r;ewq8VL*`*!CN( zl1T4CZE7-R03Sn7TU!)DChQ5TeS5>-&{zKMKkxCx7%Mgc%?M$7SSzznlsX*`i9WmWX(dW09%0L~!LJzBpHs;Hr@({PP2J`%r;E>} zTzbR^Qzn}q&!bBYr*H~vbDlUTb01BrZ zE;}faH(M-6Gg&q8%uGld@A6<^ck<_m6vr8wj(J<7B(>x9VZ4Ulw~y7C*t^WKi_ikgWHW%De|!#xVE@RAMMkuh$~6TK`_yNz78dOMU) zHlURjPXk+OUTc4`#6~#%u(zjTlQd%9Jvd$&vHq}4GYkUmHIkK^`jJTbc@wovt;CA} zT3)JUD>)5*VZINP<~II}WQ87lC=Zfy1B@%!wRZRW|N4*rpvgVk2RW1;UH_cVmwA@p z_xpWtFD;5q8q4hlIzM!(unVZzNQvY*CsecZPq{m&JAD5~Y1p3K^2m%E^?j4%fNG7| zgEio!uciJUGtiVCw8fmPNqAr>NzJ(O4N|1fd53^;L%z>8W7x{&dh{knP^|-lq?+mC zPKRL3k=v6u0%18;OUEK@X=+MlE8_pGiAF{jVefk(Wj;sZ7z;Dg?|Ak2k0vSjB*mlo z?rL>C^Gs+OC7@NejU|TbZ0#e1-pTb^w%^sGJ}>3Zzn(ALg^|y1Hj)F&h_Ga{GJan1 z|9U_UX+-OAYsak z7Y(_YF$FhAxl$}Gx4m_p-V{T|&==Z=OTq|PUAiz?dR2#!dsxeB_PN!gf%(ucBUBq# zgNvRjls{QCI|h6UQz!-lIcaU2VkP-w+3O7Yw0zk?_p;}f>10g)8qwN&8qsF0ZRM{Y zHZ8C0b|@iuu*uu5sIz7|D=VEp$D1=@jt2wc5B#;{xbi#{xo8Mq7P#xykh7<~Uj3Ep zd6u-ev-p2+Eb!th(|izLJsPt$NwLADNQIQs{)qNz3MLF_TqHN$L+O zz=<=Dj;rhJ@4f~M;?aC!LbotEv)FH2&uqL#p~9D9=@?no^1~uIfnc-hjMH9lY5}HI z!$W>u&(Vyyu$pyO(KL;HwR=FU2~2UkP1zXcImQVdoHo+=GIBg^Y+s>&ul5*&X8HVF zEniebSQgwG5!M$2eZ1h109jI=Fcd(Q2ts$8n|y~up8oQMKV9lb6LW?(^QLznqNOFI zIfQus^EdHqRK}(w8ffVkC zC!jDiI2$KoerYYu>HJ9hQB9Wv+QmdC8@F@tHeF(w$OY!TLc6opNEm=2x*E*%mv2?4DN^2f4M5nX8{y=d$he@1l9#Z~x zWKPfwSrsDnZ8l@FaYh?CqP4hAJyOe?P2QTXF)p!vRp1W~r{0IZm(Md4031m&)+P~` zV8D7GXkV;;0Y16TDDxoI7uXD{^~-x2tc){q68No0;6=cFnNo2})ID$IN)cKD>Tk9Z zK4ve$9~&PAQ3Z|#y88A?J~yPT!4TnZ!m{|q-1f~0FI{YAL6Ny~#Qy})+(}taLC!x4 zC=sRM7MUOUgy+y{sYDTM^`hy0`S;(7>QT9J&i&;#_+xf6UsmjTAQm=lg;tF2tQw|8 z|H&KX0KpEJHPRDz1_ngV)T@ z{rNZl@vr|V5#ue<17S4}W-$AGb;@!(+pxaGN}vK$E_7EO1qw^z_Zp+i@9ipYuHnUl zvds9*X{Ywq=6{xQ^yw8t9o)+uhBTx?2fjW%LVP=tyZcD)fMcDKl?p$Pay@-Gv zDQI@bM$l3y_QX`9n2RJOe!of$dx6-Tg+Bt-*lHajFphct_m3a7387P5J;gT_EG44_ zMMOCIf7=a?^*ko##MqP|a4dp1DJdlsGVX{o3gVk^Xk9lfvDXQuuG0oo7ibb8SA$6zb|)u27K~05xJfioHG#ERB+ZQqw3HW#V96YnAX0 zH-X`iQRq=M)umkm2oDaza42nl{6j=R!_Cf}2?8ogJ=i#%nCHg_u4ky|x1j};@;Z>Zik zwyf2hmYYGQ^KF&C5BqOF(M&h#If8P|_zE7YkT~N8eA#-vF7#s6kzGT&KCMpbH=n_;YwT_}%rlC;tR0RdtB7I>#g6Y+blJ@o z1U}^;xHwQhJ!rq91p=7s`qg+QE7Gf)c+kZgeMTC+?tkb^75nG9!MzUL+CQH4;?O^= z&O%Gu%xCAtdBimRmKJL^mH6JPo4MH66RSVg9L}8{r=D#E{ivh_5d`8;KgXwFDN&fN z@hm-?z}S-~)a(yZ;h%G^_x~I6b?9P*?Xz0+M@0vkm8M~benI`bgMQm#xc8ivk}bem z*QbmK*~U4E5wj?YQNYxyuzcF6@V1dYe;u$)o0I%DD#Uz!=pBSW78s5WIb1u|r$RU z5*1QUm?s2;b6uQ`EU_k}Oi|b|O=1&wZ=gsd%w{qeQvqDZuJkJyF^mx4qfZmnaZFk~M0e50Ib zqn+%_*+6?GG;kORmGTx@^i^ZX+k<7cdbxUWddp`&LwHFPJMl+y@aW>z>2DTCGN1G} zr6lYxJbLk26EJ5oY1mEl$tK+7VbB!SQsJ##Ac1#!@TB6y-7{-Nm{(snPeJhu^|{mO0+abxImE-Xx{3uGo5B z8>=fl6l;Mb^R>p0PBrg>o!gJwkNyvjW@3suk5(vCb_raXJ||)Xtolj|QIa=DNeQiS zx~HHi^w_`NjRRHP+~PHM27EiyU@=uhG|vIvZ41Bcph=HX@RGJy0dW)8SgAFnA*d2#OE$#9%x#-61qfM&7`4c?)gw>Ai)owt45 zj&@i3N2(a=1v4x{4Qp?x1hqA(8_D-APPzjNyA>H{bJwYP4&dKWq;6YCgmX=+JzeAJ zZK(H7{S6wcwzIQ_xlFDvRCxM?rt%^bAVE%mf5*n|d`mQMH*w2g7qmE|Br0$O#BDiI z{ApgegZm1C(G&{-!vGHkr6e?V&6|QSYEdF?wn}*~c?39`z&cR~oyQtUa7KsruhL2! z`_>7Zf7)9PM zIl?UlLXHO8Ugnsv`^Qi<>PR!PXVuP9_>YR50X+!ul_&plM$9V~sl8!W&2;je#-7uC!K+mm0Yp_w!Mbz&hC zal6U?lP}CfQS(kOT|+Or!61wAYfzU@Ce9Sm7@?5d5ysCaLKk-gK)kHtF)U%Uf1Suv zZ(aXF>L4(F@^FhmP9!)9l;Rn}`e@YjHbrw9Q2T+=AeY;W)YVWi_i6mrC*jxQ4Vudx zv}r@+kWR-iZ)}cUum0)=9&xJZ*0!%=ZrWn$=W8zhN=w%JN>&3EZFp+Bp-9wa)AYkw z9YQgCo~t`d^DOREH$Cb0$lc*c!F(xi)B6$mm&FBb}5cu0UHH z{$X0LMT&e1@g%GXGg|z$>R%+HglQS|$5lj{r z$Y3I6QSa%7A-l%Su$hCj7|+Pyeb!teN9=^!XraBbcK_gs$J+FEm)h~X9(0r%pRzSq zKSFswK2YDFEZNMY&xA%GPOIG6yMQ*I@+b}b>tml)8ZntUr#a=CVFNkL*ChR-kc}T` zpA}P<;UCEYl53`j7xcBo@zgST>*^gDtlP$*SH|QbeI+1Bu5xO!oD{WnC1+d*(umVa z2@*hni3gxPHx**;5}h><25-MBKaTF1)V+De!BaE=vUIks5n!E?D#;~nT%3pZ`MQwd zpL|l2bpi4x{w!|1$ox-k6#Rwi z>12sf2#sG3U;a4G_SOyIKYOD-nG8%gA#eRR=nP^()AX24uG%@McJlVBrYSs8QWtoN zUS7X#2Xfw)1U?2#MHpwy5oY*Oj7jk(-J(@$#uY&BKDSv;Y$Os30>q9dL?cX0iewSLI!=~?h$;+ z8-*N%>(BG{)9(=!`XXanh1A%Grxx)!H-tj%X4TwAn1-kXc=FLP$Zi9xS}cb;bQpH$ zF)9U2r-}-U^(<{l-yGlS@Wz2$jQac{M?K;**b^yPcAhAz_P`Q)Qln*Ufg07Neo$#E zw})a%oVF>8>s0zRBInFG5O;1(zKniMGtO&CJx%=?m}S57YqKbNe?Xi}XnE_S;GBn$ z1r!fAq}124_mIi-fFnf%DW48m@-C5H;a(zibu5KnG-<5E1ose1<-(tb%!ocLkn@Fj z1IW=Qox=6!OO_3*sFmUGOZmh6(UUyD=pA@^=%*_}Qz$B&FgYM@ifYMH`L`dnuY-hC z=&~22@3@wFBJ5t4f8+i7tP76NcQR%Kri-s0A-OZBrBk1KVqTg_!k%PJSS@Kk+)mXO zPTgtL=ePg%yX>f+{M zziT+^?)zTg%FvD)!^T<=0gtRr5M+qx7{W_>>}52 zvhf~^Za4AP#z^g9!7X4{ALjKKZad}(Y7olXiK9#@|EfI-)XE4cfPu~{9+`%Ff|AF> zDxMFa!iAFhl; z_B@eNs%>dV+o&G0k~bk~W=6}X`)M1u&&)xXGqq!y8^s>=yvw7=t(vB3l0>>q*b8jm z4db~9Oe(|k+Qr}EmxL5faj6xoFZM+J-wmb=?$-joGR442w&CDJq4w8r!S5wmI{j)s z1~@34bJvTGv4t9tj+b+yEQ*UcJZr0n-^JAS`SZ_1OO0eSIfmn20wgx%f7LxywL{1J z4Cs!ZJ*U`nPlM`(ukiJ%2iAAVp2P1plg2ToFTOjL_~#WNV#Q9!***|uUmP(QO)0U$ zQZcJe4KM_}M1pEyF=>*5Ej>endU~Gf>`BNR#j`P9b&{Xpe!o~U>+$g>_PEQxTN?bB zs`0zldfN|3?@%TGK6r4#Op#p2%Yc2hCK{`dZ4_FTTs1C#(s1Rsz=Hc0W$L(g-BkHa z@BX#>8#kCZi7rm#|6cs{EE-%k#fY5%@NR7tX6n}Gdqw6bMJ5pMl={vz+b@<`Wku;l zbnl%zC9k^#Ka{|R$H^|(k6&v^=7js9~t%@l%klhmW?G!L3AL+`jJ4R*yoDP zeoCv4T1xVx98&Xr;TRSiR;{9kLim%Ao9uU#(6Vmk#SS|l2yrcEucvZ|7OMoMcHwWB z0X(9j;0&4eRc$a@%dsj!=)3nqO>HC}Y9d2Te#FP*vk|+5{Z4~kcp@=vBxCGKCl!~+uSF~a| zD9@Fkp<%i_4!!1uylZtzj7>~fk=~RgI%TpYBhyQ`jsnR_;SH4fAIQ-3E?YxAV=@yb zK2-U2{s5S|2{v>B)5?v|gTKU1>(4cC`gMp<-Og7-pW=Hp&K@3|si6f&@N;m5ypH#T zjNW-3a~S7^_LTJifinJdHESBngVMFf<$nLkmPRET|Ji8tESs45b%{A)u3(XkJx!)?ig_yZ%R*3-_SV-;m z5`ue7Q;sXI$%jTGYEzQrzK^?TxLy!h>1%f;fh||-cIUg}W0!JL0F=pj%h5%fYrDv=cX3LH zFQ1(Yyo-0GSFPh>1O7f^pU)-+OM|Un{r0@8w~+wuISf+8AN6QrFwQ{5hta^aYm_TJ zt8N~_1nAjgQrYKnA-CrG99XIWC7&7G&8;$y)JuK9!Op`wH;-iS4X+`)Cb!P|?2UGB zbR%s#)vWp&OwgEU0TEngEyvN32awuZ$f{zl^`p^ooBe-{QY!La zRX-|rQ$?axRF1d-DOS^G*Cp<`82QW%>@=!Pb>fsvNYY#lz5Y$TA6zUJIYCl>GJ)$! zHwW4TYDdbVwS|UucOtBIXvQwpX24`)(g>DZb(VyrQLvH*vzEaD&6xTXH^OIYQz