Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/liberating resource permissions #140

Merged
merged 4 commits into from
Nov 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 93 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ Table of contents
- [Upgrade](#upgrade)
- [v2.x](#v2x)
- [Installation](#installation)
- [Resource Custom Permissions](#resource-custom-permissions)
- [Resources](#resources)
- [Default](#default)
- [Custom Permissions](#custom-permissions)
- [Pages](#pages)
- [Pages Hooks](#pages-hooks)
- [Pages Redirect Path](#pages-redirect-path)
Expand Down Expand Up @@ -136,6 +138,7 @@ php artisan vendor:publish --tag=filament-shield-config
'navigation_badge' => true,
'navigation_group' => true,
'is_globally_searchable' => false,
'show_model_path' => true,
],

'auth_provider_model' => [
Expand Down Expand Up @@ -211,9 +214,96 @@ php artisan shield:install

Follow the prompts and enjoy!

#### Resource Custom Permissions
#### Resources
Generally there are two scenraios that shield handles permissions for your `Filament` resources.

You can add custom permissions for `Resources` through Config file.
##### Default
Out of the box `Shield` handles the predefined permissions for `Filament` resources. So if that's all what you need you are all set.
If you need to add a single permission(for instance `lock`) and have it available for all your resources just append it to following `config` key:

```php
permission_prefixes' => [
'resource' => [
'view',
'view_any',
'create',
'update',
'restore',
'restore_any',
'replicate',
'reorder',
'delete',
'delete_any',
'force_delete',
'force_delete_any',
'lock'
],
...
],
```
:bulb: Now you are thinking **`what if I need a permission to be only avaialble for just one resource?`**
No worries, that's where [Custom Permissions](#custom-permissions) come to play.

##### Custom Permissions
To define custom permissions per `Resource` your `Resource` must implement the `HasShieldPermissions` contract.
This contract has a `getPermissionPrefixes()` method which returns an array of permission prefixes for your `Resource`.

Consider you have a `PostResource` and you want a couple of the predefined permissions plus a new permission called `publish_posts` to be only available for `PostResource` only.

```php
<?php

namespace BezhanSalleh\FilamentShield\Resources;

use BezhanSalleh\FilamentShield\Contracts\HasShieldPermissions;
...

class RoleResource extends Resource implements HasShieldPermissions
{
...

public static function getPermissionPrefixes(): array
{
return [
'view',
'view_any',
'create',
'update',
'delete',
'delete_any',
'publish'
];
}

...
}
```
In the above example the `getPermissionPrefixes()` method returns the permission prefixes `Shield` needs to generate the permissions.

✅ Now to enforce `publish_post` permission headover to your `PostPolicy` and add a `publish()` method:
```php
/**
* Determine whether the user can publish posts.
*
* @param \App\Models\User $admin
* @return \Illuminate\Auth\Access\Response|bool
*/
public function publish(User $user)
{
return $user->can('publish_post');
}
```
🅰️/🈯️ To make the prefix translatable, publish `Shield`'s translations and append the prefix inside `resource_permission_prefixes_labels` as key and it's translation as value for the languages you need.
```php
//lang/en/filament-shield.php
'resource_permission_prefixes_labels' => [
'publish' => 'Publish'
],
//lang/es/filament-shield.php
'resource_permission_prefixes_labels' => [
'publish' => 'Publicar'
],
```

#### Pages

Expand Down
1 change: 1 addition & 0 deletions config/filament-shield.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
'navigation_badge' => true,
'navigation_group' => true,
'is_globally_searchable' => false,
'show_model_path' => true,
],

'auth_provider_model' => [
Expand Down
2 changes: 1 addition & 1 deletion src/Commands/Concerns/CanGeneratePolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ protected function generatePolicyPath(array $entity): string

protected function generatePolicyStubVariables(array $entity): array
{
$stubVariables = collect(Utils::getGeneralResourcePermissionPrefixes())
$stubVariables = collect(Utils::getResourcePermissionPrefixes($entity['fqcn']))
->reduce(function ($gates, $permission) use ($entity) {
$gates[Str::studly($permission)] = $permission.'_'.$entity['resource'];

Expand Down
12 changes: 7 additions & 5 deletions src/Commands/MakeShieldGenerateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -192,15 +192,15 @@ protected function generateForResources(array $resources): Collection
->each(function ($entity) {
if ($this->generatorOption === 'policies_and_permissions') {
$this->copyStubToApp(static::getPolicyStub($entity['model']), $this->generatePolicyPath($entity), $this->generatePolicyStubVariables($entity));
FilamentShield::generateForResource($entity['resource']);
FilamentShield::generateForResource($entity);
}

if ($this->generatorOption === 'policies') {
$this->copyStubToApp(static::getPolicyStub($entity['model']), $this->generatePolicyPath($entity), $this->generatePolicyStubVariables($entity));
}

if ($this->generatorOption === 'permissions') {
FilamentShield::generateForResource($entity['resource']);
FilamentShield::generateForResource($entity);
}
});
}
Expand Down Expand Up @@ -236,9 +236,11 @@ protected function resourceInfo(array $resources): void
'#' => $key + 1,
'Resource' => $resource['model'],
'Policy' => "{$resource['model']}Policy.php".($this->generatorOption !== 'permissions' ? ' ✅' : ' ❌'),
'Permissions' => implode(','.PHP_EOL, collect(config('filament-shield.permission_prefixes.resource'))->map(function ($permission, $key) use ($resource) {
return $permission.'_'.$resource['resource'];
})->toArray()).($this->generatorOption !== 'policies' ? ' ✅' : ' ❌'),
'Permissions' => implode(','.PHP_EOL,
collect(Utils::getResourcePermissionPrefixes($resource['fqcn'])
)->map(function ($permission, $key) use ($resource) {
return $permission.'_'.$resource['resource'];
})->toArray()).($this->generatorOption !== 'policies' ? ' ✅' : ' ❌'),
];
})
);
Expand Down
10 changes: 10 additions & 0 deletions src/Contracts/HasShieldPermissions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace BezhanSalleh\FilamentShield\Contracts;

interface HasShieldPermissions
{
public static function getPermissionPrefixes(): array;
}
12 changes: 8 additions & 4 deletions src/FilamentShield.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,18 @@

class FilamentShield
{
public static function generateForResource(string $resource): void
public static function generateForResource(array $entity): void
{
$resourceByFQCN = $entity['fqcn'];
$resourceName = $entity['resource'];
$permissionPrefixes = Utils::getResourcePermissionPrefixes($resourceByFQCN);

if (Utils::isResourceEntityEnabled()) {
$permissions = collect();
collect(Utils::getGeneralResourcePermissionPrefixes())
->each(function ($prefix) use ($resource, $permissions) {
collect($permissionPrefixes)
->each(function ($prefix) use ($resourceName, $permissions) {
$permissions->push(Permission::firstOrCreate(
['name' => $prefix.'_'.$resource],
['name' => $prefix.'_'.$resourceName],
['guard_name' => Utils::getFilamentAuthGuard()]
));
});
Expand Down
52 changes: 33 additions & 19 deletions src/Resources/RoleResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace BezhanSalleh\FilamentShield\Resources;

use BezhanSalleh\FilamentShield\Contracts\HasShieldPermissions;
use BezhanSalleh\FilamentShield\FilamentShield;
use BezhanSalleh\FilamentShield\Resources\RoleResource\Pages;
use BezhanSalleh\FilamentShield\Support\Utils;
Expand All @@ -18,14 +19,26 @@
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;

class RoleResource extends Resource
class RoleResource extends Resource implements HasShieldPermissions
{
protected static ?string $model = Role::class;

protected static ?string $recordTitleAttribute = 'name';

protected static $permissionsCollection;

public static function getPermissionPrefixes(): array
{
return [
'view',
'view_any',
'create',
'update',
'delete',
'delete_any',
];
}

public static function form(Form $form): Form
{
return $form
Expand Down Expand Up @@ -236,12 +249,12 @@ public static function getResourceEntitiesSchema(): ?array
->schema([
Forms\Components\Toggle::make($entity['resource'])
->label(FilamentShield::getLocalizedResourceLabel($entity['fqcn']))
->helperText(get_class(new ($entity['fqcn']::getModel())()))
->helperText(Utils::showModelPath($entity['fqcn']))
->onIcon('heroicon-s-lock-open')
->offIcon('heroicon-s-lock-closed')
->reactive()
->afterStateUpdated(function (Closure $set, Closure $get, $state) use ($entity) {
collect(Utils::getGeneralResourcePermissionPrefixes())->each(function ($permission) use ($set, $entity, $state) {
collect(Utils::getResourcePermissionPrefixes($entity['fqcn']))->each(function ($permission) use ($set, $entity, $state) {
$set($permission.'_'.$entity['resource'], $state);
});

Expand Down Expand Up @@ -271,7 +284,7 @@ public static function getResourceEntitiesSchema(): ?array

public static function getResourceEntityPermissionsSchema($entity): ?array
{
return collect(Utils::getGeneralResourcePermissionPrefixes())->reduce(function ($permissions /** @phpstan ignore-line */, $permission) use ($entity) {
return collect(Utils::getResourcePermissionPrefixes($entity['fqcn']))->reduce(function ($permissions /** @phpstan ignore-line */, $permission) use ($entity) {
$permissions[] = Forms\Components\Checkbox::make($permission.'_'.$entity['resource'])
->label(FilamentShield::getLocalizedResourcePermissionLabel($permission))
->extraAttributes(['class' => 'text-primary-600'])
Expand All @@ -282,13 +295,13 @@ public static function getResourceEntityPermissionsSchema($entity): ?array

$set($permission.'_'.$entity['resource'], $record->checkPermissionTo($permission.'_'.$entity['resource']));

static::refreshResourceEntityStateAfterHydrated($record, $set, $entity['resource']);
static::refreshResourceEntityStateAfterHydrated($record, $set, $entity);

static::refreshSelectAllStateViaEntities($set, $get);
})
->reactive()
->afterStateUpdated(function (Closure $set, Closure $get, $state) use ($entity) {
static::refreshResourceEntityStateAfterUpdate($set, $get, Str::of($entity['resource']));
static::refreshResourceEntityStateAfterUpdate($set, $get, $entity);

if (! $state) {
$set($entity['resource'], false);
Expand Down Expand Up @@ -331,7 +344,7 @@ protected static function refreshEntitiesStatesViaSelectAll(Closure $set, $state
{
collect(FilamentShield::getResources())->each(function ($entity) use ($set, $state) {
$set($entity['resource'], $state);
collect(Utils::getGeneralResourcePermissionPrefixes())->each(function ($permission) use ($entity, $set, $state) {
collect(Utils::getResourcePermissionPrefixes($entity['fqcn']))->each(function ($permission) use ($entity, $set, $state) {
$set($permission.'_'.$entity['resource'], $state);
});
});
Expand All @@ -355,23 +368,23 @@ protected static function refreshEntitiesStatesViaSelectAll(Closure $set, $state
});
}

protected static function refreshResourceEntityStateAfterUpdate(Closure $set, Closure $get, string $entity): void
protected static function refreshResourceEntityStateAfterUpdate(Closure $set, Closure $get, array $entity): void
{
$permissionStates = collect(Utils::getGeneralResourcePermissionPrefixes())
$permissionStates = collect(Utils::getResourcePermissionPrefixes($entity['fqcn']))
->map(function ($permission) use ($get, $entity) {
return (bool) $get($permission.'_'.$entity);
return (bool) $get($permission.'_'.$entity['resource']);
});

if ($permissionStates->containsStrict(false) === false) {
$set($entity, true);
$set($entity['resource'], true);
}

if ($permissionStates->containsStrict(false) === true) {
$set($entity, false);
$set($entity['resource'], false);
}
}

protected static function refreshResourceEntityStateAfterHydrated(Model $record, Closure $set, string $entity): void
protected static function refreshResourceEntityStateAfterHydrated(Model $record, Closure $set, array $entity): void
{
$entities = $record->permissions->pluck('name')
->reduce(function ($roles, $role) {
Expand All @@ -383,8 +396,9 @@ protected static function refreshResourceEntityStateAfterHydrated(Model $record,
->groupBy(function ($item) {
return $item;
})->map->count()
->reduce(function ($counts, $role, $key) {
if ($role > 1 && $role == count(Utils::getGeneralResourcePermissionPrefixes())) {
->reduce(function ($counts, $role, $key) use ($entity) {
$count = count(Utils::getResourcePermissionPrefixes($entity['fqcn']));
if ($role > 1 && $role === $count) {
$counts[$key] = true;
} else {
$counts[$key] = false;
Expand All @@ -394,10 +408,10 @@ protected static function refreshResourceEntityStateAfterHydrated(Model $record,
}, []);

// set entity's state if one are all permissions are true
if (Arr::exists($entities, $entity) && Arr::get($entities, $entity)) {
$set($entity, true);
if (Arr::exists($entities, $entity['resource']) && Arr::get($entities, $entity['resource'])) {
$set($entity['resource'], true);
} else {
$set($entity, false);
$set($entity['resource'], false);
$set('select_all', false);
}
}
Expand Down Expand Up @@ -491,7 +505,7 @@ protected static function getCustomEntities(): ?Collection
{
$resourcePermissions = collect();
collect(FilamentShield::getResources())->each(function ($entity) use ($resourcePermissions) {
collect(Utils::getGeneralResourcePermissionPrefixes())->map(function ($permission) use ($resourcePermissions, $entity) {
collect(Utils::getResourcePermissionPrefixes($entity['fqcn']))->map(function ($permission) use ($resourcePermissions, $entity) {
$resourcePermissions->push((string) Str::of($permission.'_'.$entity['resource']));
});
});
Expand Down
21 changes: 21 additions & 0 deletions src/Support/Utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace BezhanSalleh\FilamentShield\Support;

use BezhanSalleh\FilamentShield\Contracts\HasShieldPermissions;

class Utils
{
public static function getFilamentAuthGuard(): string
Expand Down Expand Up @@ -154,4 +156,23 @@ public static function isRolePolicyRegistered(): bool
{
return (bool) config('filament-shield.register_role_policy', true);
}

public static function doesResourceHaveCustomPermissions(string $resourceClass): bool
{
return in_array(HasShieldPermissions::class, class_implements($resourceClass));
}

public static function showModelPath(string $resourceFQCN): string
{
return config('filament-shield.shield_resource.show_model_path', true)
? get_class(new ($resourceFQCN::getModel())())
: '';
}

public static function getResourcePermissionPrefixes(string $resourceFQCN): array
{
return static::doesResourceHaveCustomPermissions($resourceFQCN)
? $resourceFQCN::getPermissionPrefixes()
: static::getGeneralResourcePermissionPrefixes();
}
}