From dd4192cf19ea991a7e9e2c763ca1adebc373aabf Mon Sep 17 00:00:00 2001 From: Gautier DELEGLISE Date: Wed, 16 Apr 2025 18:37:38 +0200 Subject: [PATCH 1/4] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactored=20the=20pro?= =?UTF-8?q?cess?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/FUNDING.yml | 1 + .github/workflows/packagist-deploy.yml | 20 +++ README.md | 68 +++++++++- src/Controls/Control.php | 8 +- src/Perimeters/Perimeter.php | 12 +- tests/Feature/ControlsQueryTest.php | 79 ++++++++---- tests/Feature/ControlsScoutQueryTest.php | 84 ++++--------- tests/Feature/ControlsShouldTest.php | 118 +++++++++++------- tests/Feature/PerimetersTest.php | 53 +------- tests/Feature/TestCase.php | 7 +- .../Support/Access/Controls/ModelControl.php | 54 ++++---- .../Database/Factories/ClientFactory.php | 28 +++++ .../Database/Factories/ModelFactory.php | 22 +++- .../Database/Factories/UserFactory.php | 4 - .../2014_00_00_000000_create_users_table.php | 5 +- ...2023_04_00_000000_create_clients_table.php | 21 ++++ .../2023_04_00_000000_create_models_table.php | 7 +- ..._create_models_shared_with_users_table.php | 21 ++++ tests/Support/Models/Client.php | 36 ++++++ tests/Support/Models/Model.php | 12 ++ tests/Support/Models/User.php | 20 +-- 21 files changed, 435 insertions(+), 245 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 .github/workflows/packagist-deploy.yml create mode 100644 tests/Support/Database/Factories/ClientFactory.php create mode 100644 tests/Support/Database/migrations/2023_04_00_000000_create_clients_table.php create mode 100644 tests/Support/Database/migrations/2023_05_00_000000_create_models_shared_with_users_table.php create mode 100644 tests/Support/Models/Client.php diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..d262752 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: GautierDele \ No newline at end of file diff --git a/.github/workflows/packagist-deploy.yml b/.github/workflows/packagist-deploy.yml new file mode 100644 index 0000000..6fea6b5 --- /dev/null +++ b/.github/workflows/packagist-deploy.yml @@ -0,0 +1,20 @@ +name: Packagist Deploy + +on: + release: + types: [created] + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + - uses: mnavarrocarter/packagist-update@v1.0.0 + with: + username: "GautierDele" + api_token: ${{ secrets.PACKAGIST_TOKEN }} \ No newline at end of file diff --git a/README.md b/README.md index 16874cf..300c644 100644 --- a/README.md +++ b/README.md @@ -2,5 +2,69 @@ # Laravel Access Control -# BETA -Please note that this package is under beta and is not recommended to use for production environment for now. End of beta should be by summer 2024. \ No newline at end of file +Laravel Access Control allows you to fully secure your application in two key areas: Policies and Queries. Manage everything in one place! +## Requirements + +PHP 8.2+ and Laravel 11+ + +## Documentation, Installation, and Usage Instructions + +See the [documentation](https://laravel-access-control.lomkit.com) for detailed installation and usage instructions. + +## What it does + +You first need to define the perimeters concerned by your applications. + +Create the model control: + +```php +class PostControl extends Control +{ + protected function perimeters(): array + { + return [ + GlobalPerimeter::new() + ->allowed(function (Model $user, string $method) { + return $user->can(sprintf('%s global models', $method)); + }) + ->should(function (Model $user, Model $model) { + return true; + }) + ->scoutQuery(function (\Laravel\Scout\Builder $query, Model $user) { + return $query; + }) + ->query(function (Builder $query, Model $user) { + return $query; + }), + ClientPerimeter::new() + ->allowed(function (Model $user, string $method) { + return $user->can(sprintf('%s client models', $method)); + }) + ->should(function (Model $user, Model $model) { + return $model->client()->is($user->client); + }) + ->scoutQuery(function (\Laravel\Scout\Builder $query, Model $user) { + return $query->where('client_id', $user->client->getKey()); + }) + ->query(function (Builder $query, Model $user) { + return $query->orWhere('client_id', $user->client->getKey()); + }), + // ... +``` + +Then setup your policy: + +```php + class PostPolicy extends ControlledPolicy +{ + protected string $model = Post::class; +} +``` + +and you are ready to go ! + +```php +App\Models\Post::controlled()->get() // Apply the Control to the query + +$user->can('view', App\Models\Post::first()) // Check if the user can view the post according to the policy +``` \ No newline at end of file diff --git a/src/Controls/Control.php b/src/Controls/Control.php index 8a869fc..d8beb13 100644 --- a/src/Controls/Control.php +++ b/src/Controls/Control.php @@ -49,14 +49,14 @@ protected function perimeters(): array public function applies(Model $user, string $method, Model $model): bool { foreach ($this->perimeters() as $perimeter) { - if ($perimeter->applyAllowedCallback($user)) { + if ($perimeter->applyAllowedCallback($user, $method)) { // If the model doesn't exists, it means the method is not related to a model // so we don't need to activate the should result since we can't compare an existing model if (!$model->exists) { return true; } - $should = $perimeter->applyShouldCallback($user, $method, $model); + $should = $perimeter->applyShouldCallback($user, $model); if (!$perimeter->overlays() || $should) { return $should; @@ -118,7 +118,7 @@ protected function applyQueryControl(Builder $query, Model $user): Builder }; foreach ($this->perimeters() as $perimeter) { - if ($perimeter->applyAllowedCallback($user)) { + if ($perimeter->applyAllowedCallback($user, 'view')) { $query = $perimeter->applyQueryCallback($query, $user); $noResultCallback = function ($query) {return $query; }; @@ -147,7 +147,7 @@ protected function applyScoutQueryControl(\Laravel\Scout\Builder $query, Model $ }; foreach ($this->perimeters() as $perimeter) { - if ($perimeter->applyAllowedCallback($user)) { + if ($perimeter->applyAllowedCallback($user, 'view')) { $query = $perimeter->applyScoutQueryCallback($query, $user); $noResultCallback = function ($query) {return $query; }; diff --git a/src/Perimeters/Perimeter.php b/src/Perimeters/Perimeter.php index 7461106..0a63841 100644 --- a/src/Perimeters/Perimeter.php +++ b/src/Perimeters/Perimeter.php @@ -22,8 +22,8 @@ public function __construct() // Default implementations that can be overridden $this->scoutQueryCallback = function (\Laravel\Scout\Builder $query, Model $user) { return $query; }; $this->queryCallback = function (Builder $query, Model $user) { return $query; }; - $this->shouldCallback = function (Model $user, string $method, Model $model) { return true; }; - $this->allowedCallback = function (Model $user) { return true; }; + $this->shouldCallback = function (Model $user, Model $model) { return true; }; + $this->allowedCallback = function (Model $user, string $method, Model $model) { return true; }; } /** @@ -35,9 +35,9 @@ public function __construct() * * @return bool True if the condition applies; false otherwise. */ - public function applyShouldCallback(Model $user, string $method, Model $model): bool + public function applyShouldCallback(Model $user, Model $model): bool { - return ($this->shouldCallback)($user, $method, $model); + return ($this->shouldCallback)($user, $model); } /** @@ -73,9 +73,9 @@ public function applyQueryCallback(Builder $query, Model $user): Builder * * @return bool True if the user is allowed; false otherwise. */ - public function applyAllowedCallback(Model $user): bool + public function applyAllowedCallback(Model $user, string $method): bool { - return ($this->allowedCallback)($user); + return ($this->allowedCallback)($user, $method); } /** diff --git a/tests/Feature/ControlsQueryTest.php b/tests/Feature/ControlsQueryTest.php index 4edf236..505320f 100644 --- a/tests/Feature/ControlsQueryTest.php +++ b/tests/Feature/ControlsQueryTest.php @@ -3,7 +3,10 @@ namespace Lomkit\Access\Tests\Feature; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Gate; +use Lomkit\Access\Tests\Support\Models\Client; use Lomkit\Access\Tests\Support\Models\Model; +use Lomkit\Access\Tests\Support\Models\User; class ControlsQueryTest extends \Lomkit\Access\Tests\Feature\TestCase { @@ -21,13 +24,15 @@ public function test_control_with_no_perimeter_passing(): void public function test_control_queried_using_client_perimeter(): void { - Auth::user()->update(['should_client' => true]); + Gate::define('view client models', function (User $user) { + return true; + }); Model::factory() ->count(50) ->create(); Model::factory() - ->state(['is_client' => true]) + ->clientPerimeter() ->count(50) ->create(); @@ -39,18 +44,22 @@ public function test_control_queried_using_client_perimeter(): void public function test_control_queried_using_shared_overlayed_perimeter(): void { - Auth::user()->update(['should_shared' => true]); - Auth::user()->update(['should_client' => true]); + Gate::define('view client models', function (User $user) { + return true; + }); + Gate::define('view shared models', function (User $user) { + return true; + }); Model::factory() ->count(50) ->create(); Model::factory() - ->state(['is_shared' => true]) + ->sharedPerimeter() ->count(50) ->create(); Model::factory() - ->state(['is_client' => true]) + ->clientPerimeter() ->count(50) ->create(); @@ -62,19 +71,23 @@ public function test_control_queried_using_shared_overlayed_perimeter(): void public function test_control_queried_using_shared_overlayed_perimeter_with_distant_perimeter(): void { - Auth::user()->update(['should_shared' => true]); - Auth::user()->update(['should_own' => true]); + Gate::define('view shared models', function (User $user) { + return true; + }); + Gate::define('view own models', function (User $user) { + return true; + }); Model::factory() - ->state(['is_client' => true]) + ->clientPerimeter() ->count(50) ->create(); Model::factory() - ->state(['is_shared' => true]) + ->sharedPerimeter() ->count(50) ->create(); Model::factory() - ->state(['is_own' => true]) + ->ownPerimeter() ->count(50) ->create(); @@ -86,18 +99,20 @@ public function test_control_queried_using_shared_overlayed_perimeter_with_dista public function test_control_queried_using_only_shared_overlayed_perimeter(): void { - Auth::user()->update(['should_shared' => true]); + Gate::define('view shared models', function (User $user) { + return true; + }); Model::factory() - ->state(['is_client' => true]) + ->clientPerimeter() ->count(50) ->create(); Model::factory() - ->state(['is_shared' => true]) + ->sharedPerimeter() ->count(50) ->create(); Model::factory() - ->state(['is_own' => true]) + ->ownPerimeter() ->count(50) ->create(); @@ -109,23 +124,28 @@ public function test_control_queried_using_only_shared_overlayed_perimeter(): vo public function test_control_queried_isolated(): void { - Auth::user()->update(['should_shared' => true]); - Auth::user()->update(['should_own' => true]); + Gate::define('view shared models', function (User $user) { + return true; + }); + Gate::define('view own models', function (User $user) { + return true; + }); Model::factory() - ->state(['is_client' => true]) + ->clientPerimeter() ->count(50) ->create(); Model::factory() - ->state(['is_shared' => true, 'is_client' => true]) + ->clientPerimeter() + ->sharedPerimeter() ->count(50) ->create(); Model::factory() - ->state(['is_own' => true]) + ->ownPerimeter() ->count(50) ->create(); - $query = Model::query()->where('is_client', true); + $query = Model::query()->where('client_id', Auth::user()->client->getKey()); $query = (new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->queried($query, Auth::user()); $this->assertEquals(50, $query->count()); @@ -135,23 +155,28 @@ public function test_control_queried_not_isolated(): void { config(['access-control.queries.isolated' => false]); - Auth::user()->update(['should_shared' => true]); - Auth::user()->update(['should_own' => true]); + Gate::define('view shared models', function (User $user) { + return true; + }); + Gate::define('view own models', function (User $user) { + return true; + }); Model::factory() - ->state(['is_client' => true]) + ->clientPerimeter() ->count(50) ->create(); Model::factory() - ->state(['is_shared' => true, 'is_client' => true]) + ->clientPerimeter() + ->sharedPerimeter() ->count(50) ->create(); Model::factory() - ->state(['is_own' => true]) + ->ownPerimeter() ->count(50) ->create(); - $query = Model::query()->where('is_client', true); + $query = Model::query()->where('client_id', Auth::user()->client->getKey()); $query = (new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->queried($query, Auth::user()); $this->assertEquals(150, $query->count()); diff --git a/tests/Feature/ControlsScoutQueryTest.php b/tests/Feature/ControlsScoutQueryTest.php index 0aa3540..878f86b 100644 --- a/tests/Feature/ControlsScoutQueryTest.php +++ b/tests/Feature/ControlsScoutQueryTest.php @@ -1,16 +1,14 @@ count(50) - ->create(); - $query = Model::search(); $query = (new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->scoutQueried($query, Auth::user()); @@ -19,89 +17,55 @@ public function test_control_scout_query_with_no_perimeter_passing(): void public function test_control_scout_queried_using_client_perimeter(): void { - Auth::user()->update(['should_client' => true]); - - Model::factory() - ->count(50) - ->create(); - Model::factory() - ->state(['is_client' => true]) - ->count(50) - ->create(); + Gate::define('view client models', function (User $user) { + return true; + }); $query = Model::search(); $query = (new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->scoutQueried($query, Auth::user()); - $this->assertEquals(['is_client' => true], $query->wheres); + $this->assertEquals(['client_id' => Auth::user()->client->getKey()], $query->wheres); } public function test_control_scout_queried_using_shared_overlayed_perimeter(): void { - Auth::user()->update(['should_shared' => true]); - Auth::user()->update(['should_client' => true]); - - Model::factory() - ->count(50) - ->create(); - Model::factory() - ->state(['is_shared' => true]) - ->count(50) - ->create(); - Model::factory() - ->state(['is_client' => true]) - ->count(50) - ->create(); + Gate::define('view client models', function (User $user) { + return true; + }); + Gate::define('view shared models', function (User $user) { + return true; + }); $query = Model::search(); $query = (new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->scoutQueried($query, Auth::user()); - $this->assertEquals(['is_shared' => true, 'is_client' => true], $query->wheres); + $this->assertEquals(['client_id' => Auth::user()->client->getKey(), 'shared_with_users' => Auth::user()->getKey()], $query->wheres); } public function test_control_scout_queried_using_shared_overlayed_perimeter_with_distant_perimeter(): void { - Auth::user()->update(['should_shared' => true]); - Auth::user()->update(['should_own' => true]); - - Model::factory() - ->state(['is_client' => true]) - ->count(50) - ->create(); - Model::factory() - ->state(['is_shared' => true]) - ->count(50) - ->create(); - Model::factory() - ->state(['is_own' => true]) - ->count(50) - ->create(); + Gate::define('view own models', function (User $user) { + return true; + }); + Gate::define('view shared models', function (User $user) { + return true; + }); $query = Model::search(); $query = (new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->scoutQueried($query, Auth::user()); - $this->assertEquals(['is_shared' => true, 'is_own' => true], $query->wheres); + $this->assertEquals(['shared_with_users' => Auth::user()->getKey(), 'author_id' => Auth::user()->getKey()], $query->wheres); } public function test_control_scout_queried_using_only_shared_overlayed_perimeter(): void { - Auth::user()->update(['should_shared' => true]); - - Model::factory() - ->state(['is_client' => true]) - ->count(50) - ->create(); - Model::factory() - ->state(['is_shared' => true]) - ->count(50) - ->create(); - Model::factory() - ->state(['is_own' => true]) - ->count(50) - ->create(); + Gate::define('view shared models', function (User $user) { + return true; + }); $query = Model::search(); $query = (new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->scoutQueried($query, Auth::user()); - $this->assertEquals(['is_shared' => true], $query->wheres); + $this->assertEquals(['shared_with_users' => Auth::user()->getKey()], $query->wheres); } } diff --git a/tests/Feature/ControlsShouldTest.php b/tests/Feature/ControlsShouldTest.php index af6c883..cb6a9de 100644 --- a/tests/Feature/ControlsShouldTest.php +++ b/tests/Feature/ControlsShouldTest.php @@ -3,7 +3,9 @@ namespace Lomkit\Access\Tests\Feature; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Gate; use Lomkit\Access\Tests\Support\Models\Model; +use Lomkit\Access\Tests\Support\Models\User; class ControlsShouldTest extends \Lomkit\Access\Tests\Feature\TestCase { @@ -14,140 +16,164 @@ public function test_control_with_no_perimeter_passing(): void public function test_control_should_view_any_using_client_perimeter(): void { - Auth::user()->update(['should_client' => true]); + Gate::define('viewAny client models', function (User $user) { + return true; + }); $this->assertTrue((new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->applies(Auth::user(), 'viewAny', new Model())); } public function test_control_should_view_using_client_perimeter(): void { - Auth::user()->update(['should_client' => true]); + Gate::define('view client models', function (User $user) { + return true; + }); + $model = Model::factory() - ->create([ - 'allowed_methods' => 'view', - ]); + ->clientPerimeter() + ->create(); $this->assertTrue((new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->applies(Auth::user(), 'view', $model)); } public function test_control_should_not_view_using_client_perimeter(): void { - Auth::user()->update(['should_client' => true]); + Gate::define('update client models', function (User $user) { + return true; + }); + $model = Model::factory() - ->create([ - 'allowed_methods' => 'create', - ]); + ->create(); $this->assertFalse((new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->applies(Auth::user(), 'view', $model)); } public function test_control_should_create_using_client_perimeter(): void { - Auth::user()->update(['should_client' => true]); + Gate::define('create global models', function (User $user) { + return true; + }); $this->assertTrue((new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->applies(Auth::user(), 'create', new Model())); } public function test_control_should_update_using_client_perimeter(): void { - Auth::user()->update(['should_client' => true]); + Gate::define('update client models', function (User $user) { + return true; + }); + $model = Model::factory() - ->create([ - 'allowed_methods' => 'update', - ]); + ->clientPerimeter() + ->create(); $this->assertTrue((new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->applies(Auth::user(), 'update', $model)); } public function test_control_should_delete_using_client_perimeter(): void { - Auth::user()->update(['should_client' => true]); + Gate::define('delete client models', function (User $user) { + return true; + }); + $model = Model::factory() - ->create([ - 'allowed_methods' => 'delete', - ]); + ->clientPerimeter() + ->create(); $this->assertTrue((new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->applies(Auth::user(), 'delete', $model)); } public function test_control_should_view_any_using_global_perimeter(): void { - Auth::user()->update(['should_global' => true]); + Gate::define('viewAny global models', function (User $user) { + return true; + }); $this->assertTrue((new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->applies(Auth::user(), 'viewAny', new Model())); } public function test_control_should_view_using_global_perimeter(): void { - Auth::user()->update(['should_global' => true]); + Gate::define('view global models', function (User $user) { + return true; + }); + $model = Model::factory() - ->create([ - 'allowed_methods' => 'view', - ]); + ->create(); $this->assertTrue((new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->applies(Auth::user(), 'view', $model)); } public function test_control_should_not_view_using_global_perimeter(): void { - Auth::user()->update(['should_global' => true]); + Gate::define('view client models', function (User $user) { + return true; + }); + $model = Model::factory() - ->create([ - 'allowed_methods' => 'create', - ]); + ->create(); $this->assertFalse((new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->applies(Auth::user(), 'view', $model)); } public function test_control_should_create_using_global_perimeter(): void { - Auth::user()->update(['should_global' => true]); + Gate::define('create global models', function (User $user) { + return true; + }); $this->assertTrue((new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->applies(Auth::user(), 'create', new Model())); } public function test_control_should_update_using_global_perimeter(): void { - Auth::user()->update(['should_global' => true]); + Gate::define('update global models', function (User $user) { + return true; + }); + $model = Model::factory() - ->create([ - 'allowed_methods' => 'update', - ]); + ->create(); $this->assertTrue((new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->applies(Auth::user(), 'update', $model)); } public function test_control_should_delete_using_global_perimeter(): void { - Auth::user()->update(['should_global' => true]); + Gate::define('delete global models', function (User $user) { + return true; + }); + $model = Model::factory() - ->create([ - 'allowed_methods' => 'delete', - ]); + ->create(); $this->assertTrue((new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->applies(Auth::user(), 'delete', $model)); } public function test_control_should_delete_global_using_shared_overlayed_perimeter(): void { - Auth::user()->update(['should_shared' => true]); - Auth::user()->update(['should_global' => true]); + Gate::define('delete shared models', function (User $user) { + return true; + }); + Gate::define('delete global models', function (User $user) { + return true; + }); $model = Model::factory() - ->create([ - 'allowed_methods' => 'delete', - ]); + ->create(); $this->assertTrue((new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->applies(Auth::user(), 'delete', $model)); } public function test_control_should_delete_using_shared_overlayed_perimeter(): void { - Auth::user()->update(['should_shared' => true]); - Auth::user()->update(['should_global' => true]); + Gate::define('delete shared models', function (User $user) { + return true; + }); + Gate::define('delete global models', function (User $user) { + return true; + }); + $model = Model::factory() - ->create([ - 'allowed_methods' => 'delete_shared', - ]); + ->create(); $this->assertTrue((new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->applies(Auth::user(), 'delete', $model)); } diff --git a/tests/Feature/PerimetersTest.php b/tests/Feature/PerimetersTest.php index dbc582d..ea4527a 100644 --- a/tests/Feature/PerimetersTest.php +++ b/tests/Feature/PerimetersTest.php @@ -4,66 +4,21 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Gate; use Lomkit\Access\Tests\Support\Access\Perimeters\ClientPerimeter; use Lomkit\Access\Tests\Support\Access\Perimeters\GlobalPerimeter; use Lomkit\Access\Tests\Support\Access\Perimeters\OwnPerimeter; use Lomkit\Access\Tests\Support\Access\Perimeters\SharedPerimeter; +use Lomkit\Access\Tests\Support\Models\User; class PerimetersTest extends TestCase { public function test_should_client_perimeter(): void { - Auth::user()->update(['should_client' => true]); - - $this->assertTrue((new ClientPerimeter())->allowed(function (Model $user) { return $user->should_client; })->applyAllowedCallback(Auth::user())); - } - - public function test_should_not_client_perimeter(): void - { - Auth::user()->update(['should_client' => false]); - - $this->assertFalse((new ClientPerimeter())->allowed(function (Model $user) { return $user->should_client; })->applyAllowedCallback(Auth::user())); - } - - public function test_should_global_perimeter(): void - { - Auth::user()->update(['should_global' => true]); - - $this->assertTrue((new GlobalPerimeter())->allowed(function (Model $user) { return $user->should_global; })->applyAllowedCallback(Auth::user())); + $this->assertTrue((new ClientPerimeter())->allowed(function (Model $user, string $method) { return true; })->applyAllowedCallback(Auth::user(), '')); } - - public function test_should_not_global_perimeter(): void - { - Auth::user()->update(['should_global' => false]); - - $this->assertFalse((new GlobalPerimeter())->allowed(function (Model $user) { return $user->should_global; })->applyAllowedCallback(Auth::user())); - } - - public function test_should_own_perimeter(): void - { - Auth::user()->update(['should_own' => true]); - - $this->assertTrue((new OwnPerimeter())->allowed(function (Model $user) { return $user->should_own; })->applyAllowedCallback(Auth::user())); - } - - public function test_should_not_own_perimeter(): void - { - Auth::user()->update(['should_own' => false]); - - $this->assertFalse((new OwnPerimeter())->allowed(function (Model $user) { return $user->should_own; })->applyAllowedCallback(Auth::user())); - } - - public function test_should_shared_perimeter(): void - { - Auth::user()->update(['should_shared' => true]); - - $this->assertTrue((new SharedPerimeter())->allowed(function (Model $user) { return $user->should_shared; })->applyAllowedCallback(Auth::user())); - } - public function test_should_not_shared_perimeter(): void { - Auth::user()->update(['should_shared' => false]); - - $this->assertFalse((new SharedPerimeter())->allowed(function (Model $user) { return $user->should_shared; })->applyAllowedCallback(Auth::user())); + $this->assertFalse((new SharedPerimeter())->allowed(function (Model $user, string $method) { return false; })->applyAllowedCallback(Auth::user(), '')); } } diff --git a/tests/Feature/TestCase.php b/tests/Feature/TestCase.php index ccad5cc..98c5f94 100644 --- a/tests/Feature/TestCase.php +++ b/tests/Feature/TestCase.php @@ -3,6 +3,8 @@ namespace Lomkit\Access\Tests\Feature; use Lomkit\Access\Tests\Support\Database\Factories\UserFactory; +use Lomkit\Access\Tests\Support\Models\Client; +use Lomkit\Access\Tests\Support\Models\User; use Lomkit\Access\Tests\TestCase as BaseTestCase; class TestCase extends BaseTestCase @@ -11,7 +13,10 @@ protected function setUp(): void { parent::setUp(); - $this->withAuthenticatedUser(); + $user = UserFactory::new() + ->for(Client::factory()) + ->createOne(); + $this->withAuthenticatedUser($user); } protected function resolveAuthFactoryClass() diff --git a/tests/Support/Access/Controls/ModelControl.php b/tests/Support/Access/Controls/ModelControl.php index 56da960..65314d1 100644 --- a/tests/Support/Access/Controls/ModelControl.php +++ b/tests/Support/Access/Controls/ModelControl.php @@ -14,56 +14,60 @@ class ModelControl extends Control { protected function perimeters(): array { - $shouldCallback = function (Model $user, string $method, Model $model) { - return in_array($method, explode(',', $model->allowed_methods)); - }; - return [ SharedPerimeter::new() - ->allowed(function (Model $user) { - return $user->should_shared; + ->allowed(function (Model $user, string $method) { + return $user->can(sprintf('%s shared models', $method)); }) - ->should(function (Model $user, string $method, Model $model) { - return in_array($method.'_shared', explode(',', $model->allowed_methods)); + ->should(function (Model $user, Model $model) { + return $model->sharedWithUsers()->where('id', $user->getKey())->exists(); }) ->scoutQuery(function (\Laravel\Scout\Builder $query, Model $user) { - return $query->where('is_shared', true); + return $query->where('shared_with_users', $user->getKey()); }) ->query(function (Builder $query, Model $user) { - return $query->orWhere('is_shared', true); + return $query->orWhereHas('sharedWithUsers', function (Builder $query) use ($user) { + return $query->where('id', $user->getKey()); + }); }), GlobalPerimeter::new() - ->allowed(function (Model $user) { - return $user->should_global; + ->allowed(function (Model $user, string $method) { + return $user->can(sprintf('%s global models', $method)); + }) + ->should(function (Model $user, Model $model) { + return true; }) - ->should($shouldCallback) ->scoutQuery(function (\Laravel\Scout\Builder $query, Model $user) { - return $query->where('is_global', true); + return $query; }) ->query(function (Builder $query, Model $user) { - return $query->orWhere('is_global', true); + return $query; }), ClientPerimeter::new() - ->allowed(function (Model $user) { - return $user->should_client; + ->allowed(function (Model $user, string $method) { + return $user->can(sprintf('%s client models', $method)); + }) + ->should(function (Model $user, Model $model) { + return $model->client()->is($user->client); }) - ->should($shouldCallback) ->scoutQuery(function (\Laravel\Scout\Builder $query, Model $user) { - return $query->where('is_client', true); + return $query->where('client_id', $user->client->getKey()); }) ->query(function (Builder $query, Model $user) { - return $query->orWhere('is_client', true); + return $query->orWhere('client_id', $user->client->getKey()); }), OwnPerimeter::new() - ->allowed(function (Model $user) { - return $user->should_own; + ->allowed(function (Model $user, string $method) { + return $user->can(sprintf('%s own models', $method)); + }) + ->should(function (Model $user, Model $model) { + return $model->user()->is($user); }) - ->should($shouldCallback) ->scoutQuery(function (\Laravel\Scout\Builder $query, Model $user) { - return $query->where('is_own', true); + return $query->where('author_id', $user->getKey()); }) ->query(function (Builder $query, Model $user) { - return $query->orWhere('is_own', true); + return $query->orWhere('author_id', $user->getKey()); }), ]; } diff --git a/tests/Support/Database/Factories/ClientFactory.php b/tests/Support/Database/Factories/ClientFactory.php new file mode 100644 index 0000000..7b9350c --- /dev/null +++ b/tests/Support/Database/Factories/ClientFactory.php @@ -0,0 +1,28 @@ + + */ +class ClientFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var class-string<\Illuminate\Database\Eloquent\Model|TModel> + */ + protected $model = Client::class; + + public function definition(): array + { + return [ + 'name' => fake()->name(), + ]; + } +} diff --git a/tests/Support/Database/Factories/ModelFactory.php b/tests/Support/Database/Factories/ModelFactory.php index 88409a3..3d1f6a5 100644 --- a/tests/Support/Database/Factories/ModelFactory.php +++ b/tests/Support/Database/Factories/ModelFactory.php @@ -3,6 +3,7 @@ namespace Lomkit\Access\Tests\Support\Database\Factories; use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Support\Facades\Auth; use Lomkit\Access\Tests\Support\Models\Model; class ModelFactory extends Factory @@ -19,10 +20,23 @@ public function definition() return [ 'name' => fake()->name(), 'number' => fake()->numberBetween(-9999999, 9999999), - 'is_shared' => false, - 'is_global' => false, - 'is_client' => false, - 'is_own' => false, ]; } + + public function clientPerimeter(): Factory + { + return $this->for(Auth::user()->client); + } + + public function sharedPerimeter(): Factory + { + return $this->afterCreating(function(Model $model) { + $model->sharedWithUsers()->attach(Auth::user()); + }); + } + + public function ownPerimeter(): Factory + { + return $this->for(Auth::user(), 'author'); + } } diff --git a/tests/Support/Database/Factories/UserFactory.php b/tests/Support/Database/Factories/UserFactory.php index d981673..99f589f 100644 --- a/tests/Support/Database/Factories/UserFactory.php +++ b/tests/Support/Database/Factories/UserFactory.php @@ -26,10 +26,6 @@ public function definition(): array 'email_verified_at' => now(), 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 'remember_token' => Str::random(10), - 'should_shared' => false, - 'should_global' => false, - 'should_own' => false, - 'should_client' => false, ]; } diff --git a/tests/Support/Database/migrations/2014_00_00_000000_create_users_table.php b/tests/Support/Database/migrations/2014_00_00_000000_create_users_table.php index 161a152..8443173 100644 --- a/tests/Support/Database/migrations/2014_00_00_000000_create_users_table.php +++ b/tests/Support/Database/migrations/2014_00_00_000000_create_users_table.php @@ -18,10 +18,7 @@ public function up() $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); - $table->boolean('should_shared'); - $table->boolean('should_global'); - $table->boolean('should_client'); - $table->boolean('should_own'); + $table->foreignIdFor(\Lomkit\Access\Tests\Support\Models\Client::class, 'client_id')->nullable()->constrained(); $table->rememberToken(); $table->timestamps(); }); diff --git a/tests/Support/Database/migrations/2023_04_00_000000_create_clients_table.php b/tests/Support/Database/migrations/2023_04_00_000000_create_clients_table.php new file mode 100644 index 0000000..bf74f0d --- /dev/null +++ b/tests/Support/Database/migrations/2023_04_00_000000_create_clients_table.php @@ -0,0 +1,21 @@ +*id(); + $table->string('name'); + $table->timestamps(); + }); + } +}; diff --git a/tests/Support/Database/migrations/2023_04_00_000000_create_models_table.php b/tests/Support/Database/migrations/2023_04_00_000000_create_models_table.php index c2f27c0..3210eec 100644 --- a/tests/Support/Database/migrations/2023_04_00_000000_create_models_table.php +++ b/tests/Support/Database/migrations/2023_04_00_000000_create_models_table.php @@ -18,11 +18,8 @@ public function up() $table->string('string')->nullable(); $table->string('unique')->unique()->nullable(); $table->bigInteger('number'); - $table->string('allowed_methods')->nullable(); - $table->boolean('is_shared'); - $table->boolean('is_global'); - $table->boolean('is_client'); - $table->boolean('is_own'); + $table->foreignIdFor(\Lomkit\Access\Tests\Support\Models\User::class, 'author_id')->nullable()->constrained(); + $table->foreignIdFor(\Lomkit\Access\Tests\Support\Models\Client::class, 'client_id')->nullable()->constrained(); $table->timestamps(); }); } diff --git a/tests/Support/Database/migrations/2023_05_00_000000_create_models_shared_with_users_table.php b/tests/Support/Database/migrations/2023_05_00_000000_create_models_shared_with_users_table.php new file mode 100644 index 0000000..defcdd6 --- /dev/null +++ b/tests/Support/Database/migrations/2023_05_00_000000_create_models_shared_with_users_table.php @@ -0,0 +1,21 @@ +foreignIdFor(\Lomkit\Access\Tests\Support\Models\Model::class)->constrained(); + $table->foreignIdFor(\Lomkit\Access\Tests\Support\Models\User::class)->constrained(); + $table->timestamps(); + }); + } +}; diff --git a/tests/Support/Models/Client.php b/tests/Support/Models/Client.php new file mode 100644 index 0000000..9867b7a --- /dev/null +++ b/tests/Support/Models/Client.php @@ -0,0 +1,36 @@ + + */ + protected $fillable = [ + 'name', + ]; + + public function models() { + return $this->hasMany(Model::class, 'client_id'); + } + + public function users() { + return $this->hasMany(User::class, 'client_id'); + } +} diff --git a/tests/Support/Models/Model.php b/tests/Support/Models/Model.php index 3d8e05f..5b46ac4 100644 --- a/tests/Support/Models/Model.php +++ b/tests/Support/Models/Model.php @@ -22,4 +22,16 @@ protected static function newFactory() protected $fillable = [ 'id', ]; + + public function author() { + return $this->belongsTo(User::class, 'author_id'); + } + + public function client() { + return $this->belongsTo(Client::class, 'client_id'); + } + + public function sharedWithUsers() { + return $this->belongsToMany(User::class, 'models_shared_with_users'); + } } diff --git a/tests/Support/Models/User.php b/tests/Support/Models/User.php index db081a8..b5f7425 100644 --- a/tests/Support/Models/User.php +++ b/tests/Support/Models/User.php @@ -26,10 +26,6 @@ protected static function newFactory() 'name', 'email', 'password', - 'should_client', - 'should_global', - 'should_own', - 'should_shared', ]; /** @@ -49,9 +45,17 @@ protected static function newFactory() */ protected $casts = [ 'email_verified_at' => 'datetime', - 'should_shared' => 'bool', - 'should_global' => 'bool', - 'should_client' => 'bool', - 'should_own' => 'bool', ]; + + public function client() { + return $this->belongsTo(Client::class, 'client_id'); + } + + public function models() { + return $this->hasMany(Model::class, 'author_id'); + } + + public function sharedModels() { + return $this->belongsToMany(Model::class, 'models_shared_with_users'); + } } From bd9630d9b64d2b7c6977948b561acda34e89de12 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 16 Apr 2025 16:38:00 +0000 Subject: [PATCH 2/4] Apply fixes from StyleCI --- tests/Feature/ControlsQueryTest.php | 1 - tests/Feature/PerimetersTest.php | 5 +---- tests/Feature/TestCase.php | 1 - tests/Support/Database/Factories/ClientFactory.php | 1 - tests/Support/Database/Factories/ModelFactory.php | 2 +- tests/Support/Models/Client.php | 8 ++++---- tests/Support/Models/Model.php | 9 ++++++--- tests/Support/Models/User.php | 9 ++++++--- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/Feature/ControlsQueryTest.php b/tests/Feature/ControlsQueryTest.php index 505320f..d827839 100644 --- a/tests/Feature/ControlsQueryTest.php +++ b/tests/Feature/ControlsQueryTest.php @@ -4,7 +4,6 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Gate; -use Lomkit\Access\Tests\Support\Models\Client; use Lomkit\Access\Tests\Support\Models\Model; use Lomkit\Access\Tests\Support\Models\User; diff --git a/tests/Feature/PerimetersTest.php b/tests/Feature/PerimetersTest.php index ea4527a..037f635 100644 --- a/tests/Feature/PerimetersTest.php +++ b/tests/Feature/PerimetersTest.php @@ -4,12 +4,8 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Gate; use Lomkit\Access\Tests\Support\Access\Perimeters\ClientPerimeter; -use Lomkit\Access\Tests\Support\Access\Perimeters\GlobalPerimeter; -use Lomkit\Access\Tests\Support\Access\Perimeters\OwnPerimeter; use Lomkit\Access\Tests\Support\Access\Perimeters\SharedPerimeter; -use Lomkit\Access\Tests\Support\Models\User; class PerimetersTest extends TestCase { @@ -17,6 +13,7 @@ public function test_should_client_perimeter(): void { $this->assertTrue((new ClientPerimeter())->allowed(function (Model $user, string $method) { return true; })->applyAllowedCallback(Auth::user(), '')); } + public function test_should_not_shared_perimeter(): void { $this->assertFalse((new SharedPerimeter())->allowed(function (Model $user, string $method) { return false; })->applyAllowedCallback(Auth::user(), '')); diff --git a/tests/Feature/TestCase.php b/tests/Feature/TestCase.php index 98c5f94..79e9465 100644 --- a/tests/Feature/TestCase.php +++ b/tests/Feature/TestCase.php @@ -4,7 +4,6 @@ use Lomkit\Access\Tests\Support\Database\Factories\UserFactory; use Lomkit\Access\Tests\Support\Models\Client; -use Lomkit\Access\Tests\Support\Models\User; use Lomkit\Access\Tests\TestCase as BaseTestCase; class TestCase extends BaseTestCase diff --git a/tests/Support/Database/Factories/ClientFactory.php b/tests/Support/Database/Factories/ClientFactory.php index 7b9350c..12001b4 100644 --- a/tests/Support/Database/Factories/ClientFactory.php +++ b/tests/Support/Database/Factories/ClientFactory.php @@ -3,7 +3,6 @@ namespace Lomkit\Access\Tests\Support\Database\Factories; use Illuminate\Database\Eloquent\Factories\Factory; -use Illuminate\Support\Str; use Lomkit\Access\Tests\Support\Models\Client; use Lomkit\Access\Tests\Support\Models\User; diff --git a/tests/Support/Database/Factories/ModelFactory.php b/tests/Support/Database/Factories/ModelFactory.php index 3d1f6a5..c8077c0 100644 --- a/tests/Support/Database/Factories/ModelFactory.php +++ b/tests/Support/Database/Factories/ModelFactory.php @@ -30,7 +30,7 @@ public function clientPerimeter(): Factory public function sharedPerimeter(): Factory { - return $this->afterCreating(function(Model $model) { + return $this->afterCreating(function (Model $model) { $model->sharedWithUsers()->attach(Auth::user()); }); } diff --git a/tests/Support/Models/Client.php b/tests/Support/Models/Client.php index 9867b7a..dcab3c0 100644 --- a/tests/Support/Models/Client.php +++ b/tests/Support/Models/Client.php @@ -4,9 +4,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; -use Illuminate\Notifications\Notifiable; use Lomkit\Access\Tests\Support\Database\Factories\ClientFactory; -use Lomkit\Access\Tests\Support\Database\Factories\UserFactory; class Client extends Authenticatable { @@ -26,11 +24,13 @@ protected static function newFactory() 'name', ]; - public function models() { + public function models() + { return $this->hasMany(Model::class, 'client_id'); } - public function users() { + public function users() + { return $this->hasMany(User::class, 'client_id'); } } diff --git a/tests/Support/Models/Model.php b/tests/Support/Models/Model.php index 5b46ac4..53b63c6 100644 --- a/tests/Support/Models/Model.php +++ b/tests/Support/Models/Model.php @@ -23,15 +23,18 @@ protected static function newFactory() 'id', ]; - public function author() { + public function author() + { return $this->belongsTo(User::class, 'author_id'); } - public function client() { + public function client() + { return $this->belongsTo(Client::class, 'client_id'); } - public function sharedWithUsers() { + public function sharedWithUsers() + { return $this->belongsToMany(User::class, 'models_shared_with_users'); } } diff --git a/tests/Support/Models/User.php b/tests/Support/Models/User.php index b5f7425..0fb0637 100644 --- a/tests/Support/Models/User.php +++ b/tests/Support/Models/User.php @@ -47,15 +47,18 @@ protected static function newFactory() 'email_verified_at' => 'datetime', ]; - public function client() { + public function client() + { return $this->belongsTo(Client::class, 'client_id'); } - public function models() { + public function models() + { return $this->hasMany(Model::class, 'author_id'); } - public function sharedModels() { + public function sharedModels() + { return $this->belongsToMany(Model::class, 'models_shared_with_users'); } } From 35546d88997b2f07797953ce816264f4e2a60596 Mon Sep 17 00:00:00 2001 From: Gautier DELEGLISE Date: Wed, 16 Apr 2025 18:43:32 +0200 Subject: [PATCH 3/4] Update 2013_04_00_000000_create_clients_table.php --- ...ients_table.php => 2013_04_00_000000_create_clients_table.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/Support/Database/migrations/{2023_04_00_000000_create_clients_table.php => 2013_04_00_000000_create_clients_table.php} (100%) diff --git a/tests/Support/Database/migrations/2023_04_00_000000_create_clients_table.php b/tests/Support/Database/migrations/2013_04_00_000000_create_clients_table.php similarity index 100% rename from tests/Support/Database/migrations/2023_04_00_000000_create_clients_table.php rename to tests/Support/Database/migrations/2013_04_00_000000_create_clients_table.php From 3e5feb035f6c4e2f46e69b5250a655365443097e Mon Sep 17 00:00:00 2001 From: Gautier DELEGLISE Date: Thu, 17 Apr 2025 08:53:40 +0200 Subject: [PATCH 4/4] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20ai=20feedback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Perimeters/Perimeter.php | 2 +- .../migrations/2013_04_00_000000_create_clients_table.php | 2 +- .../2023_05_00_000000_create_models_shared_with_users_table.php | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Perimeters/Perimeter.php b/src/Perimeters/Perimeter.php index 0a63841..97f12f9 100644 --- a/src/Perimeters/Perimeter.php +++ b/src/Perimeters/Perimeter.php @@ -23,7 +23,7 @@ public function __construct() $this->scoutQueryCallback = function (\Laravel\Scout\Builder $query, Model $user) { return $query; }; $this->queryCallback = function (Builder $query, Model $user) { return $query; }; $this->shouldCallback = function (Model $user, Model $model) { return true; }; - $this->allowedCallback = function (Model $user, string $method, Model $model) { return true; }; + $this->allowedCallback = function (Model $user, string $method) { return true; }; } /** diff --git a/tests/Support/Database/migrations/2013_04_00_000000_create_clients_table.php b/tests/Support/Database/migrations/2013_04_00_000000_create_clients_table.php index bf74f0d..02532d3 100644 --- a/tests/Support/Database/migrations/2013_04_00_000000_create_clients_table.php +++ b/tests/Support/Database/migrations/2013_04_00_000000_create_clients_table.php @@ -1,4 +1,4 @@ -*foreignIdFor(\Lomkit\Access\Tests\Support\Models\Model::class)->constrained(); $table->foreignIdFor(\Lomkit\Access\Tests\Support\Models\User::class)->constrained(); + $table->primary(['model_id', 'user_id']); $table->timestamps(); }); }