diff --git a/.github/ISSUE_TEMPLATE/Bug_report.yml b/.github/ISSUE_TEMPLATE/Bug_report.yml deleted file mode 100644 index f307b4c..0000000 --- a/.github/ISSUE_TEMPLATE/Bug_report.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Bug Report -description: "Report something that's broken." -body: - - type: input - attributes: - label: Laravel Rest Api Version - description: Provide the Laravel Rest Api version that you are using. - placeholder: 1.0.0 - validations: - required: true - - type: input - attributes: - label: Laravel Version - description: Provide the Laravel version that you are using. - placeholder: 10.4.1 - validations: - required: true - - type: input - attributes: - label: PHP Version - description: Provide the PHP version that you are using. - placeholder: 8.1.4 - validations: - required: true - - type: input - attributes: - label: Database Driver & Version - description: If applicable, provide the database driver and version you are using. - placeholder: "MySQL 8.0.31 for macOS 13.0 on arm64 (Homebrew)" - validations: - required: false - - type: textarea - attributes: - label: Description - description: Provide a detailed description of the issue you are facing. - validations: - required: true - - type: textarea - attributes: - label: Steps To Reproduce - description: Provide detailed steps to reproduce your issue. If necessary, please provide a GitHub repository to demonstrate your issue. - validations: - required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/Question_or_enhancement_proposal.yml b/.github/ISSUE_TEMPLATE/Question_or_enhancement_proposal.yml deleted file mode 100644 index 5e64840..0000000 --- a/.github/ISSUE_TEMPLATE/Question_or_enhancement_proposal.yml +++ /dev/null @@ -1,9 +0,0 @@ -name: Question / Enchancement proposal -description: "Ask a question or for a feature" -body: - - type: textarea - attributes: - label: Description - description: Leave a comment - validations: - required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 1d74663..0000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,5 +0,0 @@ -blank_issues_enabled: false -contact_links: - - name: Documentation issue - url: https://github.com/Lomkit/laravel-rest-api-doc - about: For documentation issues, open a pull request at the lomkit/laravel-rest-api-doc repository \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 531732b..f1d25a2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,8 +14,8 @@ jobs: strategy: fail-fast: false matrix: - php-version: [ '8.1', '8.2', '8.3' ] - laravel-version: [ '^10.0' ] + php-version: [ '8.2', '8.3', '8.4' ] + laravel-version: [ '^11.0', '^12.0' ] database: [ 'sqlite', 'mysql', 'pgsql' ] name: Tests on PHP ${{ matrix.php-version }} with Laravel ${{ matrix.laravel-version }} and ${{ matrix.database }} diff --git a/composer.json b/composer.json index bcde619..90fd26b 100644 --- a/composer.json +++ b/composer.json @@ -10,14 +10,14 @@ } ], "require": { - "php": "^8.1", + "php": "^8.2", "ext-json": "*", - "laravel/framework": "^10.0" + "laravel/framework": "^11.0|^12.0" }, "require-dev": { "guzzlehttp/guzzle": "^6.0|^7.0", - "orchestra/testbench": "^8.5", - "phpunit/phpunit": "^8.0|^9.0|^10.0" + "orchestra/testbench": "^9|^10", + "phpunit/phpunit": "^11.0" }, "autoload": { "psr-4": { diff --git a/config/access-control.php b/config/access-control.php index cb4049b..3633039 100644 --- a/config/access-control.php +++ b/config/access-control.php @@ -1,27 +1,14 @@ [ - 'path' => app_path('Access/Perimeters'), - ], - /* |-------------------------------------------------------------------------- | Access Control Queries |-------------------------------------------------------------------------- | - | */ 'queries' => [ - 'enabled_by_default' => true, + 'enabled_by_default' => false, + 'isolated' => true, // Isolate the control's logic by applying a parent where on the query ], ]; diff --git a/src/AccessServiceProvider.php b/src/AccessServiceProvider.php index 5ee6406..f9b8d4e 100644 --- a/src/AccessServiceProvider.php +++ b/src/AccessServiceProvider.php @@ -3,14 +3,11 @@ namespace Lomkit\Access; use Illuminate\Support\ServiceProvider; -use Lomkit\Access\Perimeters\Perimeters; class AccessServiceProvider extends ServiceProvider { - // @TODO: add the ability to remove control scope on certain conditions - /** - * Register the service provider. + * Registers the service provider. * * @return void */ @@ -20,8 +17,6 @@ public function register() __DIR__.'/../config/access-control.php', 'access-control' ); - - $this->registerServices(); } /** @@ -32,31 +27,6 @@ public function register() public function boot() { $this->registerPublishing(); - - $this->registerPerimeters(); - } - - /** - * Register Access's perimeters. - * - * @return void - */ - protected function registerPerimeters() - { - $this->app->make(Perimeters::class) - ->perimetersIn( - config('access-control.perimeters.path', app_path('Access/Perimeters')) - ); - } - - /** - * Register Access's services in the container. - * - * @return void - */ - protected function registerServices() - { - $this->app->singleton(Perimeters::class); } /** diff --git a/src/ControlScope.php b/src/ControlScope.php deleted file mode 100644 index 7ae699a..0000000 --- a/src/ControlScope.php +++ /dev/null @@ -1,73 +0,0 @@ -withControl(); - } - } - - /** - * Extend the query builder with the needed functions. - * - * @param \Illuminate\Database\Eloquent\Builder $builder - * - * @return void - */ - public function extend(Builder $builder) - { - foreach ($this->extensions as $extension) { - $this->{"add{$extension}"}($builder); - } - } - - /** - * Add the with-control extension to the builder. - * - * @param \Illuminate\Database\Eloquent\Builder $builder - * - * @return void - */ - protected function addWithControl(Builder $builder) - { - $builder->macro('withControl', function (Builder $builder) { - /** @var Control $control */ - $control = $builder->getModel()->newControl(); - - return $control->runQuery($builder); - }); - } - - /** - * Add the without-control extension to the builder. - * - * @param \Illuminate\Database\Eloquent\Builder $builder - * - * @return void - */ - protected function addWithoutControl(Builder $builder) - { - $builder->macro('withoutControl', function (Builder $builder) { - return $builder->withoutGlobalScope($this); - }); - } -} diff --git a/src/Controls/Concerns/HasPolicy.php b/src/Controls/Concerns/HasPolicy.php deleted file mode 100644 index 8852a08..0000000 --- a/src/Controls/Concerns/HasPolicy.php +++ /dev/null @@ -1,32 +0,0 @@ -getConcernedPerimeters(); - - return $concernedPerimeters->contains(function (Perimeter $concernedPerimeter) use ($method, $model, $user) { - return $this->policy($concernedPerimeter, $method, $user, $model); - }); - } - - public function policy(Perimeter $perimeter, string $method, Model $user, Model $model): bool - { - // @TODO: for the "shared" example, implement the fact that for the query you can add multiple query - $policyMethod = Str::camel($perimeter->name).'Policy'; - - if (method_exists($this, $policyMethod)) { - return $this->$policyMethod($method, $user, $model); - } - - return false; - } -} diff --git a/src/Controls/Concerns/HasQuery.php b/src/Controls/Concerns/HasQuery.php deleted file mode 100644 index 3fa38e6..0000000 --- a/src/Controls/Concerns/HasQuery.php +++ /dev/null @@ -1,47 +0,0 @@ -getConcernedPerimeters())->isNotEmpty()) { - return tap($query, function (Builder $query) use ($concernedPerimeters) { - foreach ($concernedPerimeters as $concernedPerimeter) { - $this->query($concernedPerimeter, $query); - if ($concernedPerimeter->final()) { - return; - } - } - }); - - return; - } - - return $this->fallbackQuery($query); - } - - public function query(Perimeter $perimeter, Builder $query): Builder - { - $queryMethod = Str::camel($perimeter->name).'Query'; - - if (method_exists($this, $queryMethod)) { - $this->$queryMethod($query); - - return $query; - } - - throw new QueryNotImplemented(sprintf('The %s method is not implemented in the %s class', $queryMethod, get_class($this))); - } - - public function fallbackQuery(Builder $query): Builder - { - return $query; - } -} diff --git a/src/Controls/Control.php b/src/Controls/Control.php index 668bc7f..50a8283 100644 --- a/src/Controls/Control.php +++ b/src/Controls/Control.php @@ -2,52 +2,210 @@ namespace Lomkit\Access\Controls; -use Illuminate\Support\Collection; +use Illuminate\Container\Container; +use Illuminate\Contracts\Foundation\Application; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Str; -use Lomkit\Access\Controls\Concerns\HasPolicy; -use Lomkit\Access\Controls\Concerns\HasQuery; use Lomkit\Access\Perimeters\Perimeter; -use Lomkit\Access\Perimeters\Perimeters; +use Throwable; class Control { - use HasQuery; - use HasPolicy; + // @TODO: scout queried + /** + * The control name resolver. + * + * @var callable + */ + protected static $controlNameResolver; - protected Perimeters $perimeters; + /** + * The default namespace where control reside. + * + * @var string + */ + public static $namespace = 'App\\Access\\Controls\\'; - protected Collection $concernedPerimeters; - - public function __construct(Perimeters $perimeters) + /** + * Retrieve the list of perimeter definitions for the current control. + * + * @return array An array of Perimeter objects. + */ + protected function perimeters(): array { - $this->perimeters = $perimeters; + return []; } - public function should(Perimeter $perimeter): bool + /** + * Determines if the control applies based on the user's permissions and model state. + * + * @param Model $user The user whose permissions are evaluated. + * @param string $method The action or method to verify. + * @param Model $model The target model; if it does not exist, the control applies by default. + * + * @return bool True if the control applies to the user and model; otherwise, false. + */ + public function applies(Model $user, string $method, Model $model): bool { - $perimeterMethod = 'should'.Str::studly($perimeter->name); + foreach ($this->perimeters() as $perimeter) { + if ($perimeter->applyAllowedCallback($user)) { + // 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); - if (method_exists($this, $perimeterMethod)) { - return $this->$perimeterMethod(); + if (!$perimeter->overlays() || $should) { + return $should; + } + } } return false; } - public function getConcernedPerimeters(): Collection + /** + * Modifies the query builder to enforce access control restrictions for a given user. + * + * @param Builder $query The query builder instance to modify. + * @param Model $user The user model used to determine applicable query control restrictions. + * + * @return Builder The modified query builder with access controls applied. + */ + public function queried(Builder $query, Model $user): Builder { - if (isset($this->concernedPerimeters)) { - return $this->concernedPerimeters; + $callback = function (Builder $query, Model $user) { + return $this->applyQueryControl($query, $user); + }; + + if (config('access-control.queries.isolated')) { + return $query->where(function (Builder $query) use ($user, $callback) { + $callback($query, $user); + }); } - $perimeters = new Collection(); + return $callback($query, $user); + } + + /** + * Applies query modifications based on access control perimeters for the given user. + * + * @param Builder $query The query builder instance to be modified. + * @param Model $user The user model used to evaluate access control conditions. + * + * @return Builder The query builder after applying access control modifications. + */ + protected function applyQueryControl(Builder $query, Model $user): Builder + { + $noResultCallback = function (Builder $query) { + return $this->noResultQuery($query); + }; + + foreach ($this->perimeters() as $perimeter) { + if ($perimeter->applyAllowedCallback($user)) { + $query = $perimeter->applyQueryCallback($query, $user); - foreach ($this->perimeters->getPerimeters() as $perimeter) { - if ($this->should($perimeter)) { - $perimeters->push($perimeter); + $noResultCallback = function ($query) {return $query; }; + + if (!$perimeter->overlays()) { + return $query; + } } } - return $this->concernedPerimeters = $perimeters; + return $noResultCallback($query); + } + + /** + * Modifies the query builder to return no results. + * + * @param Builder $query The query builder instance to modify. + * + * @return Builder The modified query builder that yields an empty result set. + */ + protected function noResultQuery(Builder $query): Builder + { + return $query->whereRaw('0=1'); + } + + /** + * Specify the callback that should be invoked to guess control names. + * + * @param callable(class-string<\Illuminate\Database\Eloquent\Model>): class-string<\Lomkit\Access\Controls\Control> $callback + * + * @return void + */ + public static function guessControlNamesUsing(callable $callback): void + { + static::$controlNameResolver = $callback; + } + + /** + * Get a new control instance for the given model name. + * + * @template TClass of \Illuminate\Database\Eloquent\Model + * + * @param class-string $modelName + * + * @return \Lomkit\Access\Controls\Control + */ + public static function controlForModel(string $modelName): self + { + $control = static::resolveControlName($modelName); + + return $control::new(); + } + + /** + * Creates a new instance of the control. + * + * @return static A newly created control instance. + */ + public static function new(): self + { + return new static(); + } + + /** + * Resolve the control name for a given model. + * + * @template TClass of \Illuminate\Database\Eloquent\Model + * + * @param class-string $modelName The fully qualified model class name. + * + * @return class-string<\Lomkit\Access\Controls\Control> The fully qualified control class name corresponding to the model. + */ + public static function resolveControlName(string $modelName): string + { + $resolver = static::$controlNameResolver ?? function (string $modelName) { + $appNamespace = static::appNamespace(); + + $modelName = Str::startsWith($modelName, $appNamespace.'Models\\') + ? Str::after($modelName, $appNamespace.'Models\\') + : Str::after($modelName, $appNamespace); + + return static::$namespace.$modelName.'Control'; + }; + + return $resolver($modelName); + } + + /** + * Retrieves the application's namespace. + * + * @return string The resolved or default application namespace. + */ + protected static function appNamespace(): string + { + try { + return Container::getInstance() + ->make(Application::class) + ->getNamespace(); + } catch (Throwable) { + return 'App\\'; + } } } diff --git a/src/Controls/HasControl.php b/src/Controls/HasControl.php new file mode 100644 index 0000000..9c47810 --- /dev/null +++ b/src/Controls/HasControl.php @@ -0,0 +1,38 @@ +controlled(); + } + } + + /** + * Extend the query builder with the needed functions. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * + * @return void + */ + public function extend(Builder $builder) + { + foreach ($this->extensions as $extension) { + $this->{"add{$extension}"}($builder); + } + } + + /** + * Registers the "controlled" macro on the query builder. + * + * @param \Illuminate\Database\Eloquent\Builder $builder The query builder instance to extend. + * + * @return void + */ + protected function addControlled(Builder $builder): void + { + $builder->macro('controlled', function (Builder $builder) { + /** @var Control $control */ + $control = $builder->getModel()->newControl(); + + return $control->queried($builder, Auth::user()); + }); + } + + /** + * Registers the "uncontrolled" macro on the query builder. + * + * @param \Illuminate\Database\Eloquent\Builder $builder The query builder instance to extend. + * + * @return void + */ + protected function addUncontrolled(Builder $builder) + { + $builder->macro('uncontrolled', function (Builder $builder) { + return $builder->withoutGlobalScope($this); + }); + } +} diff --git a/src/Exceptions/QueryNotImplemented.php b/src/Exceptions/QueryNotImplemented.php deleted file mode 100644 index b52a5ca..0000000 --- a/src/Exceptions/QueryNotImplemented.php +++ /dev/null @@ -1,7 +0,0 @@ -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; }; + } + /** - * The priority of the perimeter. + * Executes the should callback to determine if the access control condition is met. + * + * @param Model $user The user instance for which the check is performed. + * @param string $method The access control method or action being evaluated. + * @param Model $model The model instance related to the access check. * - * @var int + * @return bool True if the callback validation passes; otherwise, false. */ - public int $priority; + public function applyShouldCallback(Model $user, string $method, Model $model): bool + { + return ($this->shouldCallback)($user, $method, $model); + } /** - * The name of the perimeter. + * Applies the registered query callback to modify the query builder based on the user's context. + * + * @param Builder $query The query builder instance to be customized. + * @param Model $user The user model providing context for the query modification. * - * @var string + * @return Builder The modified query builder. */ - public string $name; + public function applyQueryCallback(Builder $query, Model $user): Builder + { + return ($this->queryCallback)($query, $user); + } /** - * Determine if the perimeter is final. + * Executes the allowed callback to check user access. + * + * @param Model $user The user model instance to evaluate for access. * - * @var bool + * @return bool True if the user is allowed; false otherwise. */ - public bool $final; + public function applyAllowedCallback(Model $user): bool + { + return ($this->allowedCallback)($user); + } /** - * Get the priority of the perimeter. + * Sets the allowed callback for permission checks. + * + * @param Closure $allowedCallback A callback that performs the permission evaluation. * - * @return int + * @return self Returns the current instance. */ - public function priority(): int + public function allowed(Closure $allowedCallback): self { - return $this->priority ?? 1; + $this->allowedCallback = $allowedCallback; + + return $this; } /** - * Get the name of the perimeter. + * Sets the callback used to determine if a specific access control condition should be applied. * - * @return string + * @param Closure $shouldCallback A callback that returns a boolean based on custom logic. + * + * @return self The current instance. */ - public function name(): string + public function should(Closure $shouldCallback): self { - return $this->name ?? Str::of((new \ReflectionClass($this))->getShortName())->beforeLast('Perimeter')->camel()->toString(); + $this->shouldCallback = $shouldCallback; + + return $this; } /** - * Get the final perimeter status. + * Sets the query modification callback. + * + * @param Closure $queryCallback A callback that customizes the query logic. * - * @return int + * @return self Returns the current instance for method chaining. */ - public function final(): int + public function query(Closure $queryCallback): self { - return $this->final ?? true; + $this->queryCallback = $queryCallback; + + return $this; } /** - * Determine if the perimeter matches a given name. + * Creates and returns a new instance of the Perimeter class. * - * @param string $name + * @return static A new instance of the current class. + */ + public static function new(): static + { + return new static(); + } + + /** + * Determines whether this Perimeter instance supports overlay functionality with other perimeters. * * @return bool */ - public function matches(string $name) + public function overlays(): bool { - return $name === $this->name(); + return false; } } diff --git a/src/Perimeters/PerimeterCollection.php b/src/Perimeters/PerimeterCollection.php deleted file mode 100644 index 2b50621..0000000 --- a/src/Perimeters/PerimeterCollection.php +++ /dev/null @@ -1,130 +0,0 @@ -addToCollections($perimeter); - - return $this; - } - - /** - * Add the given perimeter to the arrays of perimeters. - * - * @param Perimeter $perimeter - * - * @return void - */ - protected function addToCollections(Perimeter $perimeter) - { - $this->perimeters[$perimeter->priority][] = $perimeter; - - $this->allPerimeters = collect($this->allPerimeters) - ->push($perimeter) - ->sortBy('priority') - ->all(); - } - - /** - * Find the first perimeter matching a given name. - * - * @param string $name - * - * @throws \RuntimeException - * - * @return Perimeter - */ - public function match(string $name) - { - $perimeters = $this->get(); - - $perimeter = $this->matchAgainstPerimeters($perimeters, $name); - - return $this->handleMatchedPerimeter($name, $perimeter); - } - - /** - * Determine if a perimeter in the array matches the name. - * - * @param Perimeter[] $perimeters - * @param \Illuminate\Http\Request $request - * - * @return Perimeter|null - */ - protected function matchAgainstPerimeters(array $perimeters, string $name) - { - return collect($perimeters)->first( - fn (Perimeter $perimeter) => $perimeter->matches($name) - ); - } - - /** - * Handle the matched perimeter. - * - * @param string $name - * @param Perimeter|null $perimeter - * - * @throws \RuntimeException - * - * @return Perimeter - */ - protected function handleMatchedPerimeter(string $name, $perimeter) - { - if (!is_null($perimeter)) { - return $perimeter; - } - - throw new \RuntimeException(sprintf( - 'The perimeter %s could not be found.', - $name - )); - } - - /** - * Get perimeters from the collection by priority. - * - * @param int|null $priority - * - * @return Perimeter[] - */ - public function get(int $priority = null) - { - return is_null($priority) ? $this->getPerimeters() : Arr::get($this->perimeters, $priority, []); - } - - /** - * Get all of the perimeters in the collection. - * - * @return Perimeter[] - */ - public function getPerimeters() - { - return array_values($this->allPerimeters); - } -} diff --git a/src/Perimeters/Perimeters.php b/src/Perimeters/Perimeters.php deleted file mode 100644 index 304382a..0000000 --- a/src/Perimeters/Perimeters.php +++ /dev/null @@ -1,91 +0,0 @@ -perimeters = new PerimeterCollection(); - } - - /** - * Add a route to the underlying route collection. - * - * @param Perimeter $perimeter - * - * @return PerimeterCollection - */ - public function addPerimeter(Perimeter $perimeter): PerimeterCollection - { - return $this->perimeters->add($perimeter); - } - - /** - * Find the perimeter matching a given name. - * - * @param \Illuminate\Http\Request $request - * - * @return Perimeter - */ - public function findPerimeter(string $name) - { - $perimeter = $this->perimeters->match($name); - - return $perimeter; - } - - /** - * Get the plain perimeters. - * - * @return Perimeter[] - */ - public function getPerimeters() - { - return $this->perimeters->getPerimeters(); - } - - /** - * Register all the perimeter classes in the given directory. - * - * @param string $directory - * - * @return void - */ - public function perimetersIn($directory) - { - $namespace = app()->getNamespace(); - - foreach ((new Finder())->in($directory)->files() as $perimeter) { - $perimeter = $namespace.str_replace( - ['/', '.php'], - ['\\', ''], - Str::after($perimeter->getPathname(), app_path().DIRECTORY_SEPARATOR) - ); - - if ( - is_subclass_of($perimeter, \Lomkit\Access\Perimeters\Perimeter::class) && - !(new ReflectionClass($perimeter))->isAbstract() - ) { - $this->addPerimeter($perimeter); - } - } - } -} diff --git a/src/Policies/ControlledPolicy.php b/src/Policies/ControlledPolicy.php new file mode 100644 index 0000000..cfd17ed --- /dev/null +++ b/src/Policies/ControlledPolicy.php @@ -0,0 +1,101 @@ +model; + } + + /** + * Retrieves the control instance associated with the current model. + * + * @return Control The control instance for the current model. + */ + protected function getControl(): Control + { + return Control::controlForModel($this->getModel()); + } + + /** + * Determine if the user is authorized to view any instances of the model. + * + * @param Model $user The user for which the permission check is performed. + * + * @return bool True if the user is authorized to view any instances, false otherwise. + */ + public function viewAny(Model $user) + { + return $this->getControl()->should($user, __FUNCTION__, new ($this->getModel())); + } + + /** + * Checks whether a specific model instance is viewable by the given user. + * + * @param Model $user The user whose permission to view the model is being evaluated. + * @param Model $model The model instance for which view permission is checked. + * + * @return bool True if the user is authorized to view the model instance, false otherwise. + */ + public function view(Model $user, Model $model) + { + return $this->getControl()->should($user, __FUNCTION__, $model); + } + + /** + * Checks if the given user has permission to create a new instance of the model. + * + * @param Model $user The user whose permission to create the model is being verified. + * + * @return bool True if the user is allowed to create a new model instance, false otherwise. + */ + public function create(Model $user) + { + return $this->getControl()->should($user, __FUNCTION__, new ($this->getModel())); + } + + /** + * Determines whether the user is authorized to update the specified model instance. + * + * @param Model $user The user attempting to perform the update. + * @param Model $model The model instance targeted for update. + * + * @return bool True if the update action is permitted, false otherwise. + */ + public function update(Model $user, Model $model) + { + return $this->getControl()->should($user, __FUNCTION__, $model); + } + + /** + * Determines if the specified user is authorized to delete the given model instance. + * + * @param Model $user The user attempting the deletion. + * @param Model $model The model instance to be deleted. + * + * @return bool True if deletion is permitted, false otherwise. + */ + public function delete(Model $user, Model $model) + { + return $this->getControl()->should($user, __FUNCTION__, $model); + } +} diff --git a/src/PoliciesControlled.php b/src/PoliciesControlled.php deleted file mode 100644 index e6f35b2..0000000 --- a/src/PoliciesControlled.php +++ /dev/null @@ -1,70 +0,0 @@ - - */ - public function getControl(): string - { - return ''; - } - - /** - * Return a new control instance. - * - * @return Control - */ - public function newControl(): Control - { - return App::make($this->getControl()); - } - - /** - * Determine if any model can be viewed by the user. - */ - public function viewAny(Model $user) - { - return $this->newControl()->getConcernedPerimeters()->isNotEmpty(); - } - - /** - * Determine if the given model can be viewed by the user. - */ - public function view(Model $user, Model $model) - { - return $this->newControl()->runPolicy(__FUNCTION__, $user, $model); - } - - /** - * Determine if the model can be created by the user. - */ - public function create(Model $user) - { - return $this->newControl()->getConcernedPerimeters()->isNotEmpty(); - } - - /** - * Determine if the given model can be updated by the user. - */ - public function update(Model $user, Model $model) - { - return $this->newControl()->runPolicy(__FUNCTION__, $user, $model); - } - - /** - * Determine if the given model can be deleted by the user. - */ - public function delete(Model $user, Model $model) - { - return $this->newControl()->runPolicy(__FUNCTION__, $user, $model); - } -} diff --git a/src/QueriesControlled.php b/src/QueriesControlled.php deleted file mode 100644 index 69cc9cf..0000000 --- a/src/QueriesControlled.php +++ /dev/null @@ -1,39 +0,0 @@ - - */ - public function getControl(): string - { - return ''; - } - - /** - * Return a new control instance. - * - * @return Control - */ - public function newControl(): Control - { - return App::make($this->getControl()); - } - - /** - * Boot the access controlled trait for a model. - * - * @return void - */ - public static function bootQueriesControlled() - { - static::addGlobalScope(new ControlScope()); - } -} diff --git a/tests/Feature/ControlsQueryTest.php b/tests/Feature/ControlsQueryTest.php new file mode 100644 index 0000000..d0acae0 --- /dev/null +++ b/tests/Feature/ControlsQueryTest.php @@ -0,0 +1,157 @@ +count(50) + ->create(); + + $query = Model::query(); + $query = (new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->queried($query, Auth::user()); + + $this->assertEquals(0, $query->count()); + } + + public function test_control_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(); + + $query = Model::query(); + $query = (new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->queried($query, Auth::user()); + + $this->assertEquals(50, $query->count()); + } + + public function test_control_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(); + + $query = Model::query(); + $query = (new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->queried($query, Auth::user()); + + $this->assertEquals(100, $query->count()); + } + + 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]); + + 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(); + + $query = Model::query(); + $query = (new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->queried($query, Auth::user()); + + $this->assertEquals(100, $query->count()); + } + + public function test_control_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(); + + $query = Model::query(); + $query = (new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->queried($query, Auth::user()); + + $this->assertEquals(50, $query->count()); + } + + public function test_control_queried_isolated(): 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, 'is_client' => true]) + ->count(50) + ->create(); + Model::factory() + ->state(['is_own' => true]) + ->count(50) + ->create(); + + $query = Model::query()->where('is_client', true); + $query = (new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->queried($query, Auth::user()); + + $this->assertEquals(50, $query->count()); + } + + 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]); + + Model::factory() + ->state(['is_client' => true]) + ->count(50) + ->create(); + Model::factory() + ->state(['is_shared' => true, 'is_client' => true]) + ->count(50) + ->create(); + Model::factory() + ->state(['is_own' => true]) + ->count(50) + ->create(); + + $query = Model::query()->where('is_client', true); + $query = (new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->queried($query, Auth::user()); + + $this->assertEquals(150, $query->count()); + } +} diff --git a/tests/Feature/ControlsShouldTest.php b/tests/Feature/ControlsShouldTest.php new file mode 100644 index 0000000..3bed597 --- /dev/null +++ b/tests/Feature/ControlsShouldTest.php @@ -0,0 +1,152 @@ +assertFalse((new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->applies(Auth::user(), 'create', new Model())); + } + + public function test_control_should_view_any_using_client_perimeter(): void + { + Auth::user()->update(['should_client' => 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]); + $model = Model::factory() + ->create([ + 'allowed_methods' => 'view', + ]); + + $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]); + $model = Model::factory() + ->create([ + 'allowed_methods' => '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]); + + $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]); + $model = Model::factory() + ->create([ + 'allowed_methods' => 'update', + ]); + + $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]); + $model = Model::factory() + ->create([ + 'allowed_methods' => 'delete', + ]); + + $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]); + + $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]); + $model = Model::factory() + ->create([ + 'allowed_methods' => 'view', + ]); + + $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]); + $model = Model::factory() + ->create([ + 'allowed_methods' => '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]); + + $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]); + $model = Model::factory() + ->create([ + 'allowed_methods' => 'update', + ]); + + $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]); + $model = Model::factory() + ->create([ + 'allowed_methods' => 'delete', + ]); + + $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]); + $model = Model::factory() + ->create([ + 'allowed_methods' => 'delete', + ]); + + $this->assertTrue((new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->applies(Auth::user(), 'delete', $model)); + } + + public function test_control_should_not_delete_global_using_shared_overlayed_perimeter(): void + { + Auth::user()->update(['should_shared' => true]); + Auth::user()->update(['should_global' => true]); + $model = Model::factory() + ->create([ + 'allowed_methods' => 'delete_shared', + ]); + + $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 new file mode 100644 index 0000000..dbc582d --- /dev/null +++ b/tests/Feature/PerimetersTest.php @@ -0,0 +1,69 @@ +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())); + } + + 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())); + } +} diff --git a/tests/Feature/QueriesTest.php b/tests/Feature/QueriesTest.php deleted file mode 100644 index eef29ed..0000000 --- a/tests/Feature/QueriesTest.php +++ /dev/null @@ -1,174 +0,0 @@ -create(['is_client' => true]); - - $this->assertThrows( - fn () => NotImplementedQueryModel::query()->get(), - QueryNotImplemented::class - ); - } - - public function test_should_first_perimeter(): void - { - Cache::set('model-should-client', true); - Cache::set('model-should-site', true); - Cache::set('model-should-own', true); - - $model = ModelFactory::new()->create(['is_client' => true]); - ModelFactory::new()->create(['is_site' => true]); - ModelFactory::new()->create(['is_own' => true]); - ModelFactory::new()->create(); - - $this->assertEquals( - [$model->fresh()->toArray()], - Model::query()->get()->toArray() - ); - } - - public function test_should_second_perimeter(): void - { - Cache::set('model-should-client', false); - Cache::set('model-should-site', true); - Cache::set('model-should-own', true); - - ModelFactory::new()->create(['is_client' => true]); - $model = ModelFactory::new()->create(['is_site' => true]); - ModelFactory::new()->create(['is_own' => true]); - - $this->assertEquals( - [$model->fresh()->toArray()], - Model::query()->get()->toArray() - ); - } - - public function test_should_third_perimeter(): void - { - Cache::set('model-should-client', false); - Cache::set('model-should-site', false); - Cache::set('model-should-own', true); - - ModelFactory::new()->create(['is_client' => true]); - ModelFactory::new()->create(['is_site' => true]); - $model = ModelFactory::new()->create(['is_own' => true]); - - $this->assertEquals( - [$model->fresh()->toArray()], - Model::query()->get()->toArray() - ); - } - - public function test_should_not_final_perimeter(): void - { - Cache::set('model-should-shared', true); - Cache::set('model-should-client', false); - Cache::set('model-should-site', false); - Cache::set('model-should-own', true); - - ModelFactory::new()->create(['is_client' => true]); - ModelFactory::new()->create(['is_site' => true]); - $modelOwn = ModelFactory::new()->create(['is_own' => true]); - $modelOwnAndShared = ModelFactory::new()->create(['is_own' => true, 'is_shared' => true]); - $modelShared = ModelFactory::new()->create(['is_shared' => true]); - - $this->assertEquals( - [$modelOwn->fresh()->toArray(), $modelOwnAndShared->fresh()->toArray(), $modelShared->fresh()->toArray()], - Model::query()->get()->toArray() - ); - } - - public function test_should_not_final_perimeter_with_no_other_perimeter(): void - { - Cache::set('model-should-shared', true); - Cache::set('model-should-client', false); - Cache::set('model-should-site', false); - Cache::set('model-should-own', false); - - ModelFactory::new()->create(['is_client' => true]); - ModelFactory::new()->create(['is_site' => true]); - ModelFactory::new()->create(['is_own' => true]); - $modelOwnAndShared = ModelFactory::new()->create(['is_own' => true, 'is_shared' => true]); - $modelShared = ModelFactory::new()->create(['is_shared' => true]); - - $this->assertEquals( - [$modelOwnAndShared->fresh()->toArray(), $modelShared->fresh()->toArray()], - Model::query()->get()->toArray() - ); - } - - public function test_without_control_scope(): void - { - Cache::set('model-should-client', true); - Cache::set('model-should-site', true); - Cache::set('model-should-own', true); - - $models = - ModelFactory::new() - ->count(3) - ->create( - new Sequence( - ['is_client' => true], - ['is_site' => true], - ['is_own' => true], - ) - ); - - $this->assertEquals( - $models->fresh()->toArray(), - Model::query()->withoutControl()->get()->toArray() - ); - } - - public function test_unauthenticated(): void - { - Auth::logout(); - - ModelFactory::new() - ->count(3) - ->create( - new Sequence( - ['is_client' => true], - ['is_site' => true], - ['is_own' => true], - ) - ); - - $this->assertEquals( - [], - Model::query()->get()->toArray() - ); - } - - public function test_default_query(): void - { - ModelFactory::new() - ->count(3) - ->create( - new Sequence( - ['is_client' => true], - ['is_site' => true], - ['is_own' => true], - ) - ); - - $this->assertEquals( - [], - Model::query()->get()->toArray() - ); - } -} diff --git a/tests/Feature/TestCase.php b/tests/Feature/TestCase.php index 1a07760..ccad5cc 100644 --- a/tests/Feature/TestCase.php +++ b/tests/Feature/TestCase.php @@ -3,13 +3,10 @@ namespace Lomkit\Access\Tests\Feature; use Lomkit\Access\Tests\Support\Database\Factories\UserFactory; -use Lomkit\Access\Tests\Support\Traits\InteractsWithAuthorization; use Lomkit\Access\Tests\TestCase as BaseTestCase; class TestCase extends BaseTestCase { - use InteractsWithAuthorization; - protected function setUp(): void { parent::setUp(); diff --git a/tests/Support/Access/Controls/ModelControl.php b/tests/Support/Access/Controls/ModelControl.php index 6db803d..fc3add1 100644 --- a/tests/Support/Access/Controls/ModelControl.php +++ b/tests/Support/Access/Controls/ModelControl.php @@ -4,73 +4,56 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Facades\Cache; use Lomkit\Access\Controls\Control; +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; class ModelControl extends Control { - protected function shouldShared() - { - return Cache::get('model-should-shared', false); - } - - protected function shouldClient() - { - return Cache::get('model-should-client', false); - } - - protected function shouldSite() - { - return Cache::get('model-should-site', false); - } - - protected function shouldOwn() - { - return Cache::get('model-should-own', false); - } - - public function sharedQuery(Builder $query) - { - $query->orWhere('is_shared', true); - } - - public function clientQuery(Builder $query) - { - $query->orWhere('is_client', true); - } - - public function siteQuery(Builder $query) - { - $query->orWhere('is_site', true); - } - - public function ownQuery(Builder $query) - { - $query->orWhere('is_own', true); - } - - public function fallbackQuery(Builder $query): Builder - { - return $query->whereRaw('0 = 1'); - } - - public function sharedPolicy(string $method, Model $user, Model $model): bool - { - return true; - } - - public function clientPolicy(string $method, Model $user, Model $model): bool - { - return true; - } - - public function sitePolicy(string $method, Model $user, Model $model): bool - { - return true; - } - - public function ownPolicy(string $method, Model $user, Model $model): bool - { - return true; + protected function perimeters(): array + { + // @TODO: possible to extract the should callback to another method ?? + $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; + }) + ->should(function (Model $user, string $method, Model $model) { + return in_array($method.'_shared', explode(',', $model->allowed_methods)); + }) + ->query(function (Builder $query, Model $user) { + return $query->orWhere('is_shared', true); + }), + GlobalPerimeter::new() + ->allowed(function (Model $user) { + return $user->should_global; + }) + ->should($shouldCallback) + ->query(function (Builder $query, Model $user) { + return $query->orWhere('is_global', true); + }), + ClientPerimeter::new() + ->allowed(function (Model $user) { + return $user->should_client; + }) + ->should($shouldCallback) + ->query(function (Builder $query, Model $user) { + return $query->orWhere('is_client', true); + }), + OwnPerimeter::new() + ->allowed(function (Model $user) { + return $user->should_own; + }) + ->should($shouldCallback) + ->query(function (Builder $query, Model $user) { + return $query->orWhere('is_own', true); + }), + ]; } } diff --git a/tests/Support/Access/Controls/NotImplementedQueryControl.php b/tests/Support/Access/Controls/NotImplementedQueryControl.php deleted file mode 100644 index 1a45dad..0000000 --- a/tests/Support/Access/Controls/NotImplementedQueryControl.php +++ /dev/null @@ -1,30 +0,0 @@ -whereRaw('0 = 1'); - } -} diff --git a/tests/Support/Access/Perimeters/ClientPerimeter.php b/tests/Support/Access/Perimeters/ClientPerimeter.php index 57c9439..16e2225 100644 --- a/tests/Support/Access/Perimeters/ClientPerimeter.php +++ b/tests/Support/Access/Perimeters/ClientPerimeter.php @@ -6,7 +6,4 @@ class ClientPerimeter extends Perimeter { - public string $name = 'client'; - - public int $priority = 2; } diff --git a/tests/Support/Access/Perimeters/SitePerimeter.php b/tests/Support/Access/Perimeters/GlobalPerimeter.php similarity index 51% rename from tests/Support/Access/Perimeters/SitePerimeter.php rename to tests/Support/Access/Perimeters/GlobalPerimeter.php index 6eae5ca..ed0a72b 100644 --- a/tests/Support/Access/Perimeters/SitePerimeter.php +++ b/tests/Support/Access/Perimeters/GlobalPerimeter.php @@ -4,9 +4,6 @@ use Lomkit\Access\Perimeters\Perimeter; -class SitePerimeter extends Perimeter +class GlobalPerimeter extends Perimeter { - public string $name = 'site'; - - public int $priority = 3; } diff --git a/tests/Support/Access/Perimeters/OwnPerimeter.php b/tests/Support/Access/Perimeters/OwnPerimeter.php index 2859276..7c6bdce 100644 --- a/tests/Support/Access/Perimeters/OwnPerimeter.php +++ b/tests/Support/Access/Perimeters/OwnPerimeter.php @@ -6,7 +6,4 @@ class OwnPerimeter extends Perimeter { - public string $name = 'own'; - - public int $priority = 4; } diff --git a/tests/Support/Access/Perimeters/SharedPerimeter.php b/tests/Support/Access/Perimeters/SharedPerimeter.php index 560a53a..34785f6 100644 --- a/tests/Support/Access/Perimeters/SharedPerimeter.php +++ b/tests/Support/Access/Perimeters/SharedPerimeter.php @@ -2,13 +2,8 @@ namespace Lomkit\Access\Tests\Support\Access\Perimeters; -use Lomkit\Access\Perimeters\Perimeter; +use Lomkit\Access\Perimeters\OverlayPerimeter; -class SharedPerimeter extends Perimeter +class SharedPerimeter extends OverlayPerimeter { - public string $name = 'shared'; - - public bool $final = false; - - public int $priority = 1; } diff --git a/tests/Support/Access/Policies/ModelPolicy.php b/tests/Support/Access/Policies/ModelPolicy.php deleted file mode 100644 index 94ce6c1..0000000 --- a/tests/Support/Access/Policies/ModelPolicy.php +++ /dev/null @@ -1,22 +0,0 @@ - - */ - public function getControl(): string - { - return ModelControl::class; - } -} diff --git a/tests/Support/Database/Factories/ModelFactory.php b/tests/Support/Database/Factories/ModelFactory.php index 6ae89fc..88409a3 100644 --- a/tests/Support/Database/Factories/ModelFactory.php +++ b/tests/Support/Database/Factories/ModelFactory.php @@ -14,16 +14,15 @@ class ModelFactory extends Factory */ protected $model = Model::class; - /** - * Define the model's default state. - * - * @return array - */ public function definition() { return [ - 'name' => fake()->name(), - 'number' => fake()->numberBetween(-9999999, 9999999), + 'name' => fake()->name(), + 'number' => fake()->numberBetween(-9999999, 9999999), + 'is_shared' => false, + 'is_global' => false, + 'is_client' => false, + 'is_own' => false, ]; } } diff --git a/tests/Support/Database/Factories/UserFactory.php b/tests/Support/Database/Factories/UserFactory.php index c9a5733..d981673 100644 --- a/tests/Support/Database/Factories/UserFactory.php +++ b/tests/Support/Database/Factories/UserFactory.php @@ -18,11 +18,6 @@ class UserFactory extends Factory */ protected $model = User::class; - /** - * Define the model's default state. - * - * @return array - */ public function definition(): array { return [ @@ -31,6 +26,10 @@ 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 290c232..161a152 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,6 +18,10 @@ 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->rememberToken(); $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 6305a45..c2f27c0 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 @@ -15,13 +15,14 @@ public function up() Schema::create('models', function (Blueprint $table) { $table->id(); $table->string('name'); - $table->bigInteger('number'); - $table->boolean('is_shared')->default(false); - $table->boolean('is_client')->default(false); - $table->boolean('is_site')->default(false); - $table->boolean('is_own')->default(false); $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->timestamps(); }); } diff --git a/tests/Support/Models/Model.php b/tests/Support/Models/Model.php index 6c2677a..7b1d43c 100644 --- a/tests/Support/Models/Model.php +++ b/tests/Support/Models/Model.php @@ -4,25 +4,13 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model as BaseModel; -use Lomkit\Access\Controls\Control; -use Lomkit\Access\QueriesControlled; -use Lomkit\Access\Tests\Support\Access\Controls\ModelControl; +use Lomkit\Access\Controls\HasControl; use Lomkit\Access\Tests\Support\Database\Factories\ModelFactory; class Model extends BaseModel { use HasFactory; - use QueriesControlled; - - /** - * Return the control instance string. - * - * @return class-string - */ - public function getControl(): string - { - return ModelControl::class; - } + use HasControl; protected static function newFactory() { @@ -32,10 +20,4 @@ protected static function newFactory() protected $fillable = [ 'id', ]; - - protected $casts = [ - 'is_client' => 'bool', - 'is_site' => 'bool', - 'is_own' => 'bool', - ]; } diff --git a/tests/Support/Models/NotImplementedQueryModel.php b/tests/Support/Models/NotImplementedQueryModel.php deleted file mode 100644 index f269c3d..0000000 --- a/tests/Support/Models/NotImplementedQueryModel.php +++ /dev/null @@ -1,14 +0,0 @@ - 'datetime', + 'should_shared' => 'bool', + 'should_global' => 'bool', + 'should_client' => 'bool', + 'should_own' => 'bool', ]; } diff --git a/tests/Support/Policies/ModelPolicy.php b/tests/Support/Policies/ModelPolicy.php new file mode 100644 index 0000000..41e553c --- /dev/null +++ b/tests/Support/Policies/ModelPolicy.php @@ -0,0 +1,11 @@ +actingAs($user ?? $this->resolveAuthFactoryClass()::new()->create(), $driver); - } - - protected function resolveAuthFactoryClass() - { - return null; - } - - protected function assertUnauthorizedResponse($response) - { - $response->assertStatus(403); - $response->assertJson(['message' => 'This action is unauthorized.']); - } -} diff --git a/tests/TestCase.php b/tests/TestCase.php index 9b306d1..250dc20 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -7,11 +7,6 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabaseState; use Lomkit\Access\AccessServiceProvider; -use Lomkit\Access\Perimeters\Perimeters; -use Lomkit\Access\Tests\Support\Access\Perimeters\ClientPerimeter; -use Lomkit\Access\Tests\Support\Access\Perimeters\OwnPerimeter; -use Lomkit\Access\Tests\Support\Access\Perimeters\SharedPerimeter; -use Lomkit\Access\Tests\Support\Access\Perimeters\SitePerimeter; use Orchestra\Testbench\TestCase as BaseTestCase; class TestCase extends BaseTestCase @@ -62,26 +57,11 @@ protected function refreshInMemoryDatabase() */ protected function defineEnvironment($app) { - foreach ( - [ - SharedPerimeter::class, - ClientPerimeter::class, - OwnPerimeter::class, - SitePerimeter::class, - ] - as $perimeter - ) { - app(Perimeters::class) - ->addPerimeter(new $perimeter()); - } - tap($app->make('config'), function (Repository $config) { $config->set('auth.guards.web', [ 'driver' => 'session', 'provider' => 'users', ]); - - $config->set('access-control.perimeters.path', __DIR__); }); } @@ -98,4 +78,20 @@ protected function getPackageProviders($app) AccessServiceProvider::class, ]; } + + protected function withAuthenticatedUser($user = null, string $driver = 'web') + { + return $this->actingAs($user ?? $this->resolveAuthFactoryClass()::new()->create(), $driver); + } + + protected function resolveAuthFactoryClass() + { + return null; + } + + protected function assertUnauthorizedResponse($response) + { + $response->assertStatus(403); + $response->assertJson(['message' => 'This action is unauthorized.']); + } } diff --git a/tests/Unit/ControlsTest.php b/tests/Unit/ControlsTest.php deleted file mode 100644 index 1833701..0000000 --- a/tests/Unit/ControlsTest.php +++ /dev/null @@ -1,61 +0,0 @@ -shouldAllowMockingProtectedMethods() - ->makePartial(); - - $controlMock->shouldReceive('shouldClient')->with()->once()->andReturn(true); - $controlMock->shouldReceive('shouldSite')->with()->never(); - $controlMock->shouldReceive('shouldOwn')->with()->never(); - - $this->assertTrue( - $controlMock - ->should(new \Lomkit\Access\Tests\Support\Access\Perimeters\ClientPerimeter()) - ); - } - - public function test_should_second_perimeter() - { - $controlMock = Mockery::mock(\Lomkit\Access\Tests\Support\Access\Controls\ModelControl::class) - ->shouldAllowMockingProtectedMethods() - ->makePartial(); - - $controlMock->shouldReceive('shouldClient')->with()->never(); - $controlMock->shouldReceive('shouldSite')->with()->once()->andReturn(true); - $controlMock->shouldReceive('shouldOwn')->with()->never(); - - $this->assertTrue( - $controlMock - ->should(new \Lomkit\Access\Tests\Support\Access\Perimeters\SitePerimeter()) - ); - } - - public function test_should_third_perimeter() - { - $controlMock = Mockery::mock(\Lomkit\Access\Tests\Support\Access\Controls\ModelControl::class) - ->shouldAllowMockingProtectedMethods() - ->makePartial(); - - $controlMock->shouldReceive('shouldClient')->with()->never(); - $controlMock->shouldReceive('shouldSite')->with()->never(); - $controlMock->shouldReceive('shouldOwn')->with()->once()->andReturn(true); - - $this->assertTrue( - $controlMock - ->should(new \Lomkit\Access\Tests\Support\Access\Perimeters\OwnPerimeter()) - ); - } -} diff --git a/tests/Unit/PoliciesTest.php b/tests/Unit/PoliciesTest.php deleted file mode 100644 index 013603e..0000000 --- a/tests/Unit/PoliciesTest.php +++ /dev/null @@ -1,85 +0,0 @@ -shouldAllowMockingProtectedMethods() - ->makePartial(); - - Cache::set('model-should-own', true); - - Gate::policy(Model::class, ModelPolicy::class); - - $model = \Lomkit\Access\Tests\Support\Database\Factories\ModelFactory::new()->create(); - - $controlMock->shouldReceive('clientPolicy')->never(); - $controlMock->shouldReceive('sitePolicy')->never(); - $controlMock->shouldReceive('ownPolicy')->with('view', \Illuminate\Support\Facades\Auth::user(), $model)->once()->andReturn(true); - - $this->assertTrue( - $controlMock->runPolicy('view', Auth::user(), $model) - ); - } - - public function test_policy_update() - { - $controlMock = Mockery::mock(\Lomkit\Access\Tests\Support\Access\Controls\ModelControl::class, [app(Perimeters::class)]) - ->shouldAllowMockingProtectedMethods() - ->makePartial(); - - Cache::set('model-should-site', true); - Cache::set('model-should-own', true); - - Gate::policy(Model::class, ModelPolicy::class); - - $model = \Lomkit\Access\Tests\Support\Database\Factories\ModelFactory::new()->create(); - - $controlMock->shouldReceive('clientPolicy')->never(); - $controlMock->shouldReceive('sitePolicy')->with('update', \Illuminate\Support\Facades\Auth::user(), $model)->once()->andReturn(true); - $controlMock->shouldReceive('ownPolicy')->never(); - - $this->assertTrue( - $controlMock->runPolicy('update', Auth::user(), $model) - ); - } - - public function test_policy_delete() - { - $controlMock = Mockery::mock(\Lomkit\Access\Tests\Support\Access\Controls\ModelControl::class, [app(Perimeters::class)]) - ->shouldAllowMockingProtectedMethods() - ->makePartial(); - - Cache::set('model-should-client', true); - Cache::set('model-should-site', true); - Cache::set('model-should-own', true); - - Gate::policy(Model::class, ModelPolicy::class); - - $model = \Lomkit\Access\Tests\Support\Database\Factories\ModelFactory::new()->create(); - - $controlMock->shouldReceive('clientPolicy')->with('delete', \Illuminate\Support\Facades\Auth::user(), $model)->once()->andReturn(true); - $controlMock->shouldReceive('sitePolicy')->never(); - $controlMock->shouldReceive('ownPolicy')->never(); - - $this->assertTrue( - $controlMock->runPolicy('delete', Auth::user(), $model) - ); - } -} diff --git a/tests/Unit/TestCase.php b/tests/Unit/TestCase.php index 839a1db..977e9e9 100644 --- a/tests/Unit/TestCase.php +++ b/tests/Unit/TestCase.php @@ -1,15 +1,10 @@