From d81df3488c843538e6298c2e12502c00478c465e Mon Sep 17 00:00:00 2001 From: Boy132 Date: Fri, 19 Jul 2024 15:15:27 +0200 Subject: [PATCH 01/47] add spatie/permissions --- app/Models/User.php | 2 + composer.json | 1 + composer.lock | 84 +++++++- config/permission.php | 186 ++++++++++++++++++ ..._07_19_130942_create_permission_tables.php | 140 +++++++++++++ 5 files changed, 412 insertions(+), 1 deletion(-) create mode 100644 config/permission.php create mode 100644 database/migrations/2024_07_19_130942_create_permission_tables.php diff --git a/app/Models/User.php b/app/Models/User.php index fc96ed0f67..8ac338c228 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -25,6 +25,7 @@ use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; use App\Notifications\SendPasswordReset as ResetPasswordNotification; +use Spatie\Permission\Traits\HasRoles; /** * App\Models\User. @@ -95,6 +96,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac use CanResetPassword; use HasAccessTokens; use Notifiable; + use HasRoles; public const USER_LEVEL_USER = 0; public const USER_LEVEL_ADMIN = 1; diff --git a/composer.json b/composer.json index aaa3e9bc51..7e8ac8a0c7 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,7 @@ "s1lentium/iptools": "~1.2.0", "socialiteproviders/discord": "^4.2", "spatie/laravel-fractal": "^6.2", + "spatie/laravel-permission": "^6.9", "spatie/laravel-query-builder": "^5.8.1", "spatie/temporary-directory": "^2.2", "symfony/http-client": "^7.1", diff --git a/composer.lock b/composer.lock index 22543ba079..c8e3fa55a7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "443ec1d95b892b261af5481f27b31083", + "content-hash": "507ac5b637c51b90e6ae00717fe085cc", "packages": [ { "name": "abdelhamiderrahmouni/filament-monaco-editor", @@ -7234,6 +7234,88 @@ ], "time": "2024-03-20T07:29:11+00:00" }, + { + "name": "spatie/laravel-permission", + "version": "6.9.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-permission.git", + "reference": "fe973a58b44380d0e8620107259b7bda22f70408" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/fe973a58b44380d0e8620107259b7bda22f70408", + "reference": "fe973a58b44380d0e8620107259b7bda22f70408", + "shasum": "" + }, + "require": { + "illuminate/auth": "^8.12|^9.0|^10.0|^11.0", + "illuminate/container": "^8.12|^9.0|^10.0|^11.0", + "illuminate/contracts": "^8.12|^9.0|^10.0|^11.0", + "illuminate/database": "^8.12|^9.0|^10.0|^11.0", + "php": "^8.0" + }, + "require-dev": { + "laravel/passport": "^11.0|^12.0", + "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0", + "phpunit/phpunit": "^9.4|^10.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.x-dev", + "dev-master": "6.x-dev" + }, + "laravel": { + "providers": [ + "Spatie\\Permission\\PermissionServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\Permission\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Permission handling for Laravel 8.0 and up", + "homepage": "https://github.com/spatie/laravel-permission", + "keywords": [ + "acl", + "laravel", + "permission", + "permissions", + "rbac", + "roles", + "security", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-permission/issues", + "source": "https://github.com/spatie/laravel-permission/tree/6.9.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2024-06-22T23:04:52+00:00" + }, { "name": "spatie/laravel-query-builder", "version": "5.8.1", diff --git a/config/permission.php b/config/permission.php new file mode 100644 index 0000000000..2a520f3512 --- /dev/null +++ b/config/permission.php @@ -0,0 +1,186 @@ + [ + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * Eloquent model should be used to retrieve your permissions. Of course, it + * is often just the "Permission" model but you may use whatever you like. + * + * The model you want to use as a Permission model needs to implement the + * `Spatie\Permission\Contracts\Permission` contract. + */ + + 'permission' => Spatie\Permission\Models\Permission::class, + + /* + * When using the "HasRoles" trait from this package, we need to know which + * Eloquent model should be used to retrieve your roles. Of course, it + * is often just the "Role" model but you may use whatever you like. + * + * The model you want to use as a Role model needs to implement the + * `Spatie\Permission\Contracts\Role` contract. + */ + + 'role' => Spatie\Permission\Models\Role::class, + + ], + + 'table_names' => [ + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'roles' => 'roles', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your permissions. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'permissions' => 'permissions', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your models permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_permissions' => 'model_has_permissions', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your models roles. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_roles' => 'model_has_roles', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'role_has_permissions' => 'role_has_permissions', + ], + + 'column_names' => [ + /* + * Change this if you want to name the related pivots other than defaults + */ + 'role_pivot_key' => null, //default 'role_id', + 'permission_pivot_key' => null, //default 'permission_id', + + /* + * Change this if you want to name the related model primary key other than + * `model_id`. + * + * For example, this would be nice if your primary keys are all UUIDs. In + * that case, name this `model_uuid`. + */ + + 'model_morph_key' => 'model_id', + + /* + * Change this if you want to use the teams feature and your related model's + * foreign key is other than `team_id`. + */ + + 'team_foreign_key' => 'team_id', + ], + + /* + * When set to true, the method for checking permissions will be registered on the gate. + * Set this to false if you want to implement custom logic for checking permissions. + */ + + 'register_permission_check_method' => true, + + /* + * When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered + * this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated + * NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it. + */ + 'register_octane_reset_listener' => false, + + /* + * Teams Feature. + * When set to true the package implements teams using the 'team_foreign_key'. + * If you want the migrations to register the 'team_foreign_key', you must + * set this to true before doing the migration. + * If you already did the migration then you must make a new migration to also + * add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions' + * (view the latest version of this package's migration file) + */ + + 'teams' => false, + + /* + * Passport Client Credentials Grant + * When set to true the package will use Passports Client to check permissions + */ + + 'use_passport_client_credentials' => false, + + /* + * When set to true, the required permission names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_permission_in_exception' => false, + + /* + * When set to true, the required role names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_role_in_exception' => false, + + /* + * By default wildcard permission lookups are disabled. + * See documentation to understand supported syntax. + */ + + 'enable_wildcard_permission' => false, + + /* + * The class to use for interpreting wildcard permissions. + * If you need to modify delimiters, override the class and specify its name here. + */ + // 'permission.wildcard_permission' => Spatie\Permission\WildcardPermission::class, + + /* Cache-specific settings */ + + 'cache' => [ + + /* + * By default all permissions are cached for 24 hours to speed up performance. + * When permissions or roles are updated the cache is flushed automatically. + */ + + 'expiration_time' => \DateInterval::createFromDateString('24 hours'), + + /* + * The cache key used to store all permissions. + */ + + 'key' => 'spatie.permission.cache', + + /* + * You may optionally indicate a specific cache driver to use for permission and + * role caching using any of the `store` drivers listed in the cache.php config + * file. Using 'default' here means to use the `default` set in cache.php. + */ + + 'store' => 'default', + ], +]; diff --git a/database/migrations/2024_07_19_130942_create_permission_tables.php b/database/migrations/2024_07_19_130942_create_permission_tables.php new file mode 100644 index 0000000000..9c7044b46e --- /dev/null +++ b/database/migrations/2024_07_19_130942_create_permission_tables.php @@ -0,0 +1,140 @@ +engine('InnoDB'); + $table->bigIncrements('id'); // permission id + $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format) + $table->string('guard_name'); // For MyISAM use string('guard_name', 25); + $table->timestamps(); + + $table->unique(['name', 'guard_name']); + }); + + Schema::create($tableNames['roles'], function (Blueprint $table) use ($teams, $columnNames) { + //$table->engine('InnoDB'); + $table->bigIncrements('id'); // role id + if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing + $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable(); + $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index'); + } + $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format) + $table->string('guard_name'); // For MyISAM use string('guard_name', 25); + $table->timestamps(); + if ($teams || config('permission.testing')) { + $table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']); + } else { + $table->unique(['name', 'guard_name']); + } + }); + + Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) { + $table->unsignedBigInteger($pivotPermission); + + $table->string('model_type'); + $table->unsignedBigInteger($columnNames['model_morph_key']); + $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index'); + + $table->foreign($pivotPermission) + ->references('id') // permission id + ->on($tableNames['permissions']) + ->onDelete('cascade'); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index'); + + $table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } else { + $table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } + + }); + + Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) { + $table->unsignedBigInteger($pivotRole); + + $table->string('model_type'); + $table->unsignedBigInteger($columnNames['model_morph_key']); + $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index'); + + $table->foreign($pivotRole) + ->references('id') // role id + ->on($tableNames['roles']) + ->onDelete('cascade'); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index'); + + $table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary'); + } else { + $table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary'); + } + }); + + Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) { + $table->unsignedBigInteger($pivotPermission); + $table->unsignedBigInteger($pivotRole); + + $table->foreign($pivotPermission) + ->references('id') // permission id + ->on($tableNames['permissions']) + ->onDelete('cascade'); + + $table->foreign($pivotRole) + ->references('id') // role id + ->on($tableNames['roles']) + ->onDelete('cascade'); + + $table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary'); + }); + + app('cache') + ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null) + ->forget(config('permission.cache.key')); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $tableNames = config('permission.table_names'); + + if (empty($tableNames)) { + throw new \Exception('Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.'); + } + + Schema::drop($tableNames['role_has_permissions']); + Schema::drop($tableNames['model_has_roles']); + Schema::drop($tableNames['model_has_permissions']); + Schema::drop($tableNames['roles']); + Schema::drop($tableNames['permissions']); + } +}; From 091e87a305733b29981ced39110f8e5b850d7c6d Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 22 Jul 2024 11:22:16 +0200 Subject: [PATCH 02/47] add policies --- app/Models/User.php | 5 ++- app/Policies/ApiKeyPolicy.php | 10 +++++ app/Policies/DatabaseHostPolicy.php | 10 +++++ app/Policies/DatabasePolicy.php | 10 +++++ app/Policies/DefaultPolicies.php | 65 ++++++++++++++++++++++++++++ app/Policies/EggPolicy.php | 9 ++-- app/Policies/MountPolicy.php | 10 +++++ app/Policies/NodePolicy.php | 10 +++++ app/Policies/RolePolicy.php | 10 +++++ app/Policies/ServerPolicy.php | 38 +--------------- app/Policies/UserPolicy.php | 10 +++++ app/Providers/AppServiceProvider.php | 9 ++++ 12 files changed, 152 insertions(+), 44 deletions(-) create mode 100644 app/Policies/ApiKeyPolicy.php create mode 100644 app/Policies/DatabaseHostPolicy.php create mode 100644 app/Policies/DatabasePolicy.php create mode 100644 app/Policies/DefaultPolicies.php create mode 100644 app/Policies/MountPolicy.php create mode 100644 app/Policies/NodePolicy.php create mode 100644 app/Policies/RolePolicy.php create mode 100644 app/Policies/UserPolicy.php diff --git a/app/Models/User.php b/app/Models/User.php index 8ac338c228..38e3373dc7 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -95,8 +95,8 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac use AvailableLanguages; use CanResetPassword; use HasAccessTokens; - use Notifiable; use HasRoles; + use Notifiable; public const USER_LEVEL_USER = 0; public const USER_LEVEL_ADMIN = 1; @@ -360,7 +360,8 @@ public function isLastRootAdmin(): bool public function canAccessPanel(Panel $panel): bool { - return $this->root_admin; + // TODO: better check + return $this->root_admin || $this->roles()->count() >= 1; } public function getFilamentName(): string diff --git a/app/Policies/ApiKeyPolicy.php b/app/Policies/ApiKeyPolicy.php new file mode 100644 index 0000000000..37aac90939 --- /dev/null +++ b/app/Policies/ApiKeyPolicy.php @@ -0,0 +1,10 @@ +can('viewAny ' . $this->modelName); + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, Model $model): bool + { + return $user->can('view ' . $this->modelName, $model); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->can('create ' . $this->modelName); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, Model $model): bool + { + return $user->can('update ' . $this->modelName, $model); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, Model $model): bool + { + return $user->can('delete ' . $this->modelName, $model); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, Model $model): bool + { + return $user->can('restore ' . $this->modelName, $model); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, Model $model): bool + { + return $user->can('forceDelete ' . $this->modelName, $model); + } +} diff --git a/app/Policies/EggPolicy.php b/app/Policies/EggPolicy.php index 856eeedf62..bd589f1b25 100644 --- a/app/Policies/EggPolicy.php +++ b/app/Policies/EggPolicy.php @@ -2,12 +2,9 @@ namespace App\Policies; -use App\Models\User; - class EggPolicy { - public function create(User $user): bool - { - return true; - } + use DefaultPolicies; + + protected string $modelName = 'egg'; } diff --git a/app/Policies/MountPolicy.php b/app/Policies/MountPolicy.php new file mode 100644 index 0000000000..4f9d58b63b --- /dev/null +++ b/app/Policies/MountPolicy.php @@ -0,0 +1,10 @@ +subusers->where('user_id', $user->id)->first(); - if (!$subuser || empty($permission)) { - return false; - } - - return in_array($permission, $subuser->permissions); - } - - /** - * Runs before any of the functions are called. Used to determine if user is root admin, if so, ignore permissions. - */ - public function before(User $user, string $ability, Server $server): bool - { - if ($user->root_admin || $server->owner_id === $user->id) { - return true; - } - - return $this->checkPermission($user, $server, $ability); - } + use DefaultPolicies; - /** - * This is a horrendous hack to avoid Laravel's "smart" behavior that does - * not call the before() function if there isn't a function matching the - * policy permission. - */ - public function __call(string $name, mixed $arguments) - { - // do nothing - } + protected string $modelName = 'server'; } diff --git a/app/Policies/UserPolicy.php b/app/Policies/UserPolicy.php new file mode 100644 index 0000000000..442f5147d4 --- /dev/null +++ b/app/Policies/UserPolicy.php @@ -0,0 +1,10 @@ +extendSocialite('discord', \SocialiteProviders\Discord\Provider::class); }); + + Gate::before(function (User $user, $ability) { + return $user->root_admin ? true : null; + }); + + Gate::policy(Role::class, RolePolicy::class); } /** From 91682e5809f5c44196cab1d181d5e00e58441417 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 22 Jul 2024 11:22:39 +0200 Subject: [PATCH 03/47] add role resource --- app/Filament/Resources/RoleResource.php | 116 ++++++++++++++++++ .../RoleResource/Pages/CreateRole.php | 44 +++++++ .../Resources/RoleResource/Pages/EditRole.php | 50 ++++++++ .../RoleResource/Pages/ListRoles.php | 59 +++++++++ .../Resources/UserResource/Pages/EditUser.php | 35 +----- .../UserResource/Pages/ListUsers.php | 20 +-- 6 files changed, 280 insertions(+), 44 deletions(-) create mode 100644 app/Filament/Resources/RoleResource.php create mode 100644 app/Filament/Resources/RoleResource/Pages/CreateRole.php create mode 100644 app/Filament/Resources/RoleResource/Pages/EditRole.php create mode 100644 app/Filament/Resources/RoleResource/Pages/ListRoles.php diff --git a/app/Filament/Resources/RoleResource.php b/app/Filament/Resources/RoleResource.php new file mode 100644 index 0000000000..700d25c49e --- /dev/null +++ b/app/Filament/Resources/RoleResource.php @@ -0,0 +1,116 @@ +schema([ + TextInput::make('name') + ->label('Role Name') + ->required(), + TextInput::make('guard_name') + ->label('Guard Name') + ->default(Filament::getCurrentPanel()?->getAuthGuard() ?? '') + ->nullable(), + Section::make('Permissions') + ->columns(3) + ->schema($permissions), + ]); + } + + private const PERMISSION_PREFIXES = [ + 'viewAny', + 'view', + 'create', + 'update', + 'delete', + 'restore', + 'forceDelete', + ]; + + private static function generatePermissionChecklist(string $model): CheckboxList + { + $options = []; + + foreach (self::PERMISSION_PREFIXES as $prefix) { + $options[$prefix . ' ' . strtolower($model)] = studly_case($prefix); + } + + return CheckboxList::make($model) + ->label($model) + ->options($options) + ->bulkToggleable() + ->afterStateHydrated( + function (Component $component, string $operation, ?Model $record) use ($options) { + if (in_array($operation, ['edit', 'view'])) { + + if (blank($record)) { + return; + } + + if ($component->isVisible() && count($options) > 0) { + $component->state( + collect($options) + ->filter(fn ($value, $key) => $record->checkPermissionTo($key)) + ->keys() + ->toArray() + ); + } + } + } + ) + ->dehydrated(fn ($state) => !blank($state)); + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListRoles::route('/'), + 'create' => Pages\CreateRole::route('/create'), + 'edit' => Pages\EditRole::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/RoleResource/Pages/CreateRole.php b/app/Filament/Resources/RoleResource/Pages/CreateRole.php new file mode 100644 index 0000000000..6e88f86632 --- /dev/null +++ b/app/Filament/Resources/RoleResource/Pages/CreateRole.php @@ -0,0 +1,44 @@ +permissions = collect($data) + ->filter(function ($permission, $key) { + return !in_array($key, ['name', 'guard_name']); + }) + ->values() + ->flatten() + ->unique(); + + return Arr::only($data, ['name', 'guard_name']); + } + + protected function afterCreate(): void + { + $permissionModels = collect(); + $this->permissions->each(function ($permission) use ($permissionModels) { + $permissionModels->push(Permission::firstOrCreate([ + 'name' => $permission, + 'guard_name' => $this->data['guard_name'], + ])); + }); + + $this->record->syncPermissions($permissionModels); + } +} diff --git a/app/Filament/Resources/RoleResource/Pages/EditRole.php b/app/Filament/Resources/RoleResource/Pages/EditRole.php new file mode 100644 index 0000000000..63517218be --- /dev/null +++ b/app/Filament/Resources/RoleResource/Pages/EditRole.php @@ -0,0 +1,50 @@ +permissions = collect($data) + ->filter(function ($permission, $key) { + return !in_array($key, ['name', 'guard_name']); + }) + ->values() + ->flatten() + ->unique(); + + return Arr::only($data, ['name', 'guard_name']); + } + + protected function afterSave(): void + { + $permissionModels = collect(); + $this->permissions->each(function ($permission) use ($permissionModels) { + $permissionModels->push(Permission::firstOrCreate([ + 'name' => $permission, + 'guard_name' => $this->data['guard_name'], + ])); + }); + + $this->record->syncPermissions($permissionModels); + } + + protected function getHeaderActions(): array + { + return [ + Actions\DeleteAction::make(), + ]; + } +} diff --git a/app/Filament/Resources/RoleResource/Pages/ListRoles.php b/app/Filament/Resources/RoleResource/Pages/ListRoles.php new file mode 100644 index 0000000000..a7209e0b97 --- /dev/null +++ b/app/Filament/Resources/RoleResource/Pages/ListRoles.php @@ -0,0 +1,59 @@ +columns([ + TextColumn::make('name') + ->sortable() + ->searchable(), + TextColumn::make('guard_name') + ->sortable() + ->searchable(), + TextColumn::make('permissions_count') + ->label('Permissions') + ->badge() + ->counts('permissions'), + ]) + ->actions([ + EditAction::make(), + ]) + ->bulkActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]) + ->emptyStateIcon('tabler-users-group') + ->emptyStateDescription('') + ->emptyStateHeading('No Roles') + ->emptyStateActions([ + CreateAction::make('create') + ->label('Create Role') + ->button(), + ]); + } + + protected function getHeaderActions(): array + { + return [ + Actions\CreateAction::make() + ->label('Create Role'), + ]; + } +} diff --git a/app/Filament/Resources/UserResource/Pages/EditUser.php b/app/Filament/Resources/UserResource/Pages/EditUser.php index 6e63864053..529cc255d5 100644 --- a/app/Filament/Resources/UserResource/Pages/EditUser.php +++ b/app/Filament/Resources/UserResource/Pages/EditUser.php @@ -22,45 +22,22 @@ public function form(Form $form): Form Section::make()->schema([ Forms\Components\TextInput::make('username')->required()->maxLength(255), Forms\Components\TextInput::make('email')->email()->required()->maxLength(255), - Forms\Components\TextInput::make('password') ->dehydrateStateUsing(fn (string $state): string => Hash::make($state)) ->dehydrated(fn (?string $state): bool => filled($state)) ->required(fn (string $operation): bool => $operation === 'create') ->password(), - - Forms\Components\ToggleButtons::make('root_admin') - ->label('Administrator (Root)') - ->options([ - false => 'No', - true => 'Admin', - ]) - ->colors([ - false => 'primary', - true => 'danger', - ]) - ->disableOptionWhen(function (string $operation, $value, User $user) { - if ($operation !== 'edit' || $value) { - return false; - } - - return $user->isLastRootAdmin(); - }) - ->hint(fn (User $user) => $user->isLastRootAdmin() ? 'This is the last root administrator!' : '') - ->helperText(fn (User $user) => $user->isLastRootAdmin() ? 'You must have at least one root administrator in your system.' : '') - ->hintColor('warning') - ->inline() - ->required() - ->default(false), - - Forms\Components\Hidden::make('skipValidation')->default(true), - Forms\Components\Select::make('language') ->required() ->hidden() ->default('en') ->options(fn (User $user) => $user->getAvailableLanguages()), - + Forms\Components\Hidden::make('skipValidation')->default(true), + Forms\Components\CheckboxList::make('roles') + ->relationship('roles', 'name') + ->label('Admin Roles') + ->columnSpanFull() + ->bulkToggleable(false), ])->columns(), ]); } diff --git a/app/Filament/Resources/UserResource/Pages/ListUsers.php b/app/Filament/Resources/UserResource/Pages/ListUsers.php index 42dc17fab2..dbfb31f009 100644 --- a/app/Filament/Resources/UserResource/Pages/ListUsers.php +++ b/app/Filament/Resources/UserResource/Pages/ListUsers.php @@ -91,25 +91,15 @@ protected function getHeaderActions(): array ->required() ->unique() ->maxLength(255), - Forms\Components\TextInput::make('password') ->hintIcon('tabler-question-mark') ->hintIconTooltip('Providing a user password is optional. New user email will prompt users to create a password the first time they login.') ->password(), - - Forms\Components\ToggleButtons::make('root_admin') - ->label('Administrator (Root)') - ->options([ - false => 'No', - true => 'Admin', - ]) - ->colors([ - false => 'primary', - true => 'danger', - ]) - ->inline() - ->required() - ->default(false), + Forms\Components\CheckboxList::make('roles') + ->relationship('roles', 'name') + ->label('Admin Roles') + ->columnSpanFull() + ->bulkToggleable(false), ]), ]) ->successRedirectUrl(route('filament.admin.resources.users.index')) From aa34d6c3407ce697a4ee655df5285bc4063dc72a Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 22 Jul 2024 12:58:28 +0200 Subject: [PATCH 04/47] add root admin role handling --- app/Filament/Resources/RoleResource.php | 8 +++++++- .../Resources/RoleResource/Pages/EditRole.php | 5 ++++- .../Resources/RoleResource/Pages/ListRoles.php | 5 ++++- .../Resources/ServerResource/Pages/ListServers.php | 9 ++++++++- .../Resources/UserResource/Pages/ListUsers.php | 8 +++++--- app/Models/User.php | 11 ++++++++--- app/Providers/AppServiceProvider.php | 2 +- database/Seeders/DatabaseSeeder.php | 3 +++ 8 files changed, 40 insertions(+), 11 deletions(-) diff --git a/app/Filament/Resources/RoleResource.php b/app/Filament/Resources/RoleResource.php index 700d25c49e..45b9efdee6 100644 --- a/app/Filament/Resources/RoleResource.php +++ b/app/Filament/Resources/RoleResource.php @@ -6,9 +6,11 @@ use Filament\Facades\Filament; use Filament\Forms\Components\CheckboxList; use Filament\Forms\Components\Component; +use Filament\Forms\Components\Placeholder; use Filament\Forms\Components\Section; use Filament\Forms\Components\TextInput; use Filament\Forms\Form; +use Filament\Forms\Get; use Filament\Resources\Resource; use Illuminate\Database\Eloquent\Model; use Spatie\Permission\Models\Role; @@ -57,7 +59,11 @@ public static function form(Form $form): Form ->nullable(), Section::make('Permissions') ->columns(3) - ->schema($permissions), + ->schema($permissions) + ->hidden(fn (Get $get) => $get('name') === 'Root Admin'), + Placeholder::make('permissions') + ->content('The Root Admin has all permissions.') + ->visible(fn (Get $get) => $get('name') === 'Root Admin'), ]); } diff --git a/app/Filament/Resources/RoleResource/Pages/EditRole.php b/app/Filament/Resources/RoleResource/Pages/EditRole.php index 63517218be..3bbd8977d2 100644 --- a/app/Filament/Resources/RoleResource/Pages/EditRole.php +++ b/app/Filament/Resources/RoleResource/Pages/EditRole.php @@ -8,6 +8,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Spatie\Permission\Models\Permission; +use Spatie\Permission\Models\Role; class EditRole extends EditRecord { @@ -44,7 +45,9 @@ protected function afterSave(): void protected function getHeaderActions(): array { return [ - Actions\DeleteAction::make(), + Actions\DeleteAction::make() + ->disabled(fn (Role $role) => $role->name === 'Root Admin') + ->label(fn (Role $role) => $role->name === 'Root Admin' ? 'Can\'t delete Root Admin' : 'Delete'), ]; } } diff --git a/app/Filament/Resources/RoleResource/Pages/ListRoles.php b/app/Filament/Resources/RoleResource/Pages/ListRoles.php index a7209e0b97..5e3af2f071 100644 --- a/app/Filament/Resources/RoleResource/Pages/ListRoles.php +++ b/app/Filament/Resources/RoleResource/Pages/ListRoles.php @@ -11,6 +11,7 @@ use Filament\Tables\Actions\EditAction; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; +use Spatie\Permission\Models\Role; class ListRoles extends ListRecords { @@ -29,11 +30,13 @@ public function table(Table $table): Table TextColumn::make('permissions_count') ->label('Permissions') ->badge() - ->counts('permissions'), + ->counts('permissions') + ->formatStateUsing(fn (Role $role, $state) => $role->name === 'Root Admin' ? 'All' : $state), ]) ->actions([ EditAction::make(), ]) + ->checkIfRecordIsSelectableUsing(fn (Role $role) => $role->name !== 'Root Admin') ->bulkActions([ BulkActionGroup::make([ DeleteBulkAction::make(), diff --git a/app/Filament/Resources/ServerResource/Pages/ListServers.php b/app/Filament/Resources/ServerResource/Pages/ListServers.php index bd0f1fa21d..b0bc6ef9bc 100644 --- a/app/Filament/Resources/ServerResource/Pages/ListServers.php +++ b/app/Filament/Resources/ServerResource/Pages/ListServers.php @@ -4,6 +4,7 @@ use App\Filament\Resources\ServerResource; use App\Models\Server; +use App\Models\User; use Filament\Actions; use Filament\Resources\Pages\ListRecords; use Filament\Tables\Actions\CreateAction; @@ -76,7 +77,13 @@ public function table(Table $table): Table ->actions([ Tables\Actions\Action::make('View') ->icon('tabler-terminal') - ->url(fn (Server $server) => "/server/$server->uuid_short"), + ->url(fn (Server $server) => "/server/$server->uuid_short") + ->visible(function (Server $server) { + /** @var User $user */ + $user = auth()->user(); + + return $user->isRootAdmin() || $user->id === $server->owner_id; + }), Tables\Actions\EditAction::make(), ]) ->emptyStateIcon('tabler-brand-docker') diff --git a/app/Filament/Resources/UserResource/Pages/ListUsers.php b/app/Filament/Resources/UserResource/Pages/ListUsers.php index dbfb31f009..6c820ebf6b 100644 --- a/app/Filament/Resources/UserResource/Pages/ListUsers.php +++ b/app/Filament/Resources/UserResource/Pages/ListUsers.php @@ -40,12 +40,14 @@ public function table(Table $table): Table ->icon('tabler-mail'), Tables\Columns\IconColumn::make('root_admin') ->visibleFrom('md') - ->label('Admin') + ->label('Root Admin') ->boolean() ->trueIcon('tabler-star-filled') ->falseIcon('tabler-star-off') - ->sortable(), - Tables\Columns\IconColumn::make('use_totp')->label('2FA') + ->sortable() + ->state(fn (User $user) => $user->isRootAdmin()), + Tables\Columns\IconColumn::make('use_totp') + ->label('2FA') ->visibleFrom('lg') ->icon(fn (User $user) => $user->use_totp ? 'tabler-lock' : 'tabler-lock-open-off') ->boolean()->sortable(), diff --git a/app/Models/User.php b/app/Models/User.php index 38e3373dc7..a6b4a1cfa6 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -317,7 +317,7 @@ public function subusers(): HasMany protected function checkPermission(Server $server, string $permission = ''): bool { - if ($this->root_admin || $server->owner_id === $this->id) { + if ($this->isRootAdmin() || $server->owner_id === $this->id) { return true; } @@ -353,15 +353,20 @@ public function can($abilities, mixed $arguments = []): bool public function isLastRootAdmin(): bool { - $rootAdmins = User::query()->where('root_admin', true)->limit(2)->get(); + $rootAdmins = User::all()->filter(fn ($user) => $user->isRootAdmin()); return once(fn () => $rootAdmins->count() === 1 && $rootAdmins->first()->is($this)); } + public function isRootAdmin(): bool + { + return $this->hasRole('Root Admin'); + } + public function canAccessPanel(Panel $panel): bool { // TODO: better check - return $this->root_admin || $this->roles()->count() >= 1; + return $this->isRootAdmin() || $this->roles()->count() >= 1; } public function getFilamentName(): string diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 76dc5b7063..38ade908e8 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -85,7 +85,7 @@ public function boot(): void }); Gate::before(function (User $user, $ability) { - return $user->root_admin ? true : null; + return $user->isRootAdmin() ? true : null; }); Gate::policy(Role::class, RolePolicy::class); diff --git a/database/Seeders/DatabaseSeeder.php b/database/Seeders/DatabaseSeeder.php index 3a67335f81..6ae6775382 100644 --- a/database/Seeders/DatabaseSeeder.php +++ b/database/Seeders/DatabaseSeeder.php @@ -3,6 +3,7 @@ namespace Database\Seeders; use Illuminate\Database\Seeder; +use Spatie\Permission\Models\Role; class DatabaseSeeder extends Seeder { @@ -12,5 +13,7 @@ class DatabaseSeeder extends Seeder public function run() { $this->call(EggSeeder::class); + + Role::firstOrCreate(['name' => 'Root Admin']); } } From 86b197831ad9fcb6d033008b528d5e3a3f8287e0 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 22 Jul 2024 13:08:46 +0200 Subject: [PATCH 05/47] replace some "root_admin" with function --- app/Console/Commands/User/MakeUserCommand.php | 2 +- .../Resources/ServerResource/Pages/CreateServer.php | 2 +- app/Http/Controllers/Api/Client/ClientController.php | 2 +- .../Api/Remote/SftpAuthenticationController.php | 2 +- app/Http/Middleware/AdminAuthenticate.php | 2 +- .../Api/Application/AuthenticateApplicationUser.php | 2 +- .../Api/Client/Server/AuthenticateServerAccess.php | 4 ++-- app/Http/Middleware/RequireTwoFactorAuthentication.php | 2 +- app/Http/Requests/Admin/AdminFormRequest.php | 2 +- .../Api/Client/Servers/Subusers/SubuserRequest.php | 2 +- app/Models/User.php | 2 +- app/Services/Servers/GetUserPermissionsService.php | 4 ++-- app/Services/Users/UserCreationService.php | 8 ++++++++ app/Transformers/Api/Application/BaseTransformer.php | 2 +- app/Transformers/Api/Application/UserTransformer.php | 2 +- app/Transformers/Api/Client/ActivityLogTransformer.php | 2 +- app/Transformers/Api/Client/UserTransformer.php | 4 ++-- 17 files changed, 27 insertions(+), 19 deletions(-) diff --git a/app/Console/Commands/User/MakeUserCommand.php b/app/Console/Commands/User/MakeUserCommand.php index 20de2ec206..320f07f534 100644 --- a/app/Console/Commands/User/MakeUserCommand.php +++ b/app/Console/Commands/User/MakeUserCommand.php @@ -52,7 +52,7 @@ public function handle(): int ['UUID', $user->uuid], ['Email', $user->email], ['Username', $user->username], - ['Admin', $user->root_admin ? 'Yes' : 'No'], + ['Admin', $user->isRootAdmin() ? 'Yes' : 'No'], ]); return 0; diff --git a/app/Filament/Resources/ServerResource/Pages/CreateServer.php b/app/Filament/Resources/ServerResource/Pages/CreateServer.php index 572499aa04..fbfdf8a8f4 100644 --- a/app/Filament/Resources/ServerResource/Pages/CreateServer.php +++ b/app/Filament/Resources/ServerResource/Pages/CreateServer.php @@ -81,7 +81,7 @@ public function form(Form $form): Form ]) ->relationship('user', 'username') ->searchable(['user', 'username', 'email']) - ->getOptionLabelFromRecordUsing(fn (User $user) => "$user->email | $user->username " . ($user->root_admin ? '(admin)' : '')) + ->getOptionLabelFromRecordUsing(fn (User $user) => "$user->email | $user->username " . ($user->isRootAdmin() ? '(admin)' : '')) ->createOptionForm([ Forms\Components\TextInput::make('username') ->alphaNum() diff --git a/app/Http/Controllers/Api/Client/ClientController.php b/app/Http/Controllers/Api/Client/ClientController.php index 319d599193..7ac7562f17 100644 --- a/app/Http/Controllers/Api/Client/ClientController.php +++ b/app/Http/Controllers/Api/Client/ClientController.php @@ -48,7 +48,7 @@ public function index(GetServersRequest $request): array if (in_array($type, ['admin', 'admin-all'])) { // If they aren't an admin but want all the admin servers don't fail the request, just // make it a query that will never return any results back. - if (!$user->root_admin) { + if (!$user->isRootAdmin()) { $builder->whereRaw('1 = 2'); } else { $builder = $type === 'admin-all' diff --git a/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php b/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php index af9cf97b16..9b59704f15 100644 --- a/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php +++ b/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php @@ -140,7 +140,7 @@ protected function reject(Request $request, bool $increment = true): void */ protected function validateSftpAccess(User $user, Server $server): void { - if (!$user->root_admin && $server->owner_id !== $user->id) { + if (!$user->isRootAdmin() && $server->owner_id !== $user->id) { $permissions = $this->permissions->handle($server, $user); if (!in_array(Permission::ACTION_FILE_SFTP, $permissions)) { diff --git a/app/Http/Middleware/AdminAuthenticate.php b/app/Http/Middleware/AdminAuthenticate.php index dc3296b06c..6b86a86ee0 100644 --- a/app/Http/Middleware/AdminAuthenticate.php +++ b/app/Http/Middleware/AdminAuthenticate.php @@ -14,7 +14,7 @@ class AdminAuthenticate */ public function handle(Request $request, \Closure $next): mixed { - if (!$request->user() || !$request->user()->root_admin) { + if (!$request->user() || !$request->user()->isRootAdmin()) { throw new AccessDeniedHttpException(); } diff --git a/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php b/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php index a18c58baff..054739d27a 100644 --- a/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php +++ b/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php @@ -15,7 +15,7 @@ public function handle(Request $request, \Closure $next): mixed { /** @var \App\Models\User|null $user */ $user = $request->user(); - if (!$user || !$user->root_admin) { + if (!$user || !$user->isRootAdmin()) { throw new AccessDeniedHttpException('This account does not have permission to access the API.'); } diff --git a/app/Http/Middleware/Api/Client/Server/AuthenticateServerAccess.php b/app/Http/Middleware/Api/Client/Server/AuthenticateServerAccess.php index a8218b4106..a8ef5d0f38 100644 --- a/app/Http/Middleware/Api/Client/Server/AuthenticateServerAccess.php +++ b/app/Http/Middleware/Api/Client/Server/AuthenticateServerAccess.php @@ -39,7 +39,7 @@ public function handle(Request $request, \Closure $next): mixed // At the very least, ensure that the user trying to make this request is the // server owner, a subuser, or a root admin. We'll leave it up to the controllers // to authenticate more detailed permissions if needed. - if ($user->id !== $server->owner_id && !$user->root_admin) { + if ($user->id !== $server->owner_id && !$user->isRootAdmin()) { // Check for subuser status. if (!$server->subusers->contains('user_id', $user->id)) { throw new NotFoundHttpException(trans('exceptions.api.resource_not_found')); @@ -55,7 +55,7 @@ public function handle(Request $request, \Closure $next): mixed if (($server->isSuspended() || $server->node->isUnderMaintenance()) && !$request->routeIs('api:client:server.resources')) { throw $exception; } - if (!$user->root_admin || !$request->routeIs($this->except)) { + if (!$user->isRootAdmin() || !$request->routeIs($this->except)) { throw $exception; } } diff --git a/app/Http/Middleware/RequireTwoFactorAuthentication.php b/app/Http/Middleware/RequireTwoFactorAuthentication.php index ac1f4a1e27..470cc73b30 100644 --- a/app/Http/Middleware/RequireTwoFactorAuthentication.php +++ b/app/Http/Middleware/RequireTwoFactorAuthentication.php @@ -51,7 +51,7 @@ public function handle(Request $request, \Closure $next): mixed // If the level is set as admin and the user is not an admin, pass them through as well. if ($level === self::LEVEL_NONE || $user->use_totp) { return $next($request); - } elseif ($level === self::LEVEL_ADMIN && !$user->root_admin) { + } elseif ($level === self::LEVEL_ADMIN && !$user->isRootAdmin()) { return $next($request); } diff --git a/app/Http/Requests/Admin/AdminFormRequest.php b/app/Http/Requests/Admin/AdminFormRequest.php index 766ac92a4b..54e9f5e92c 100644 --- a/app/Http/Requests/Admin/AdminFormRequest.php +++ b/app/Http/Requests/Admin/AdminFormRequest.php @@ -21,7 +21,7 @@ public function authorize(): bool return false; } - return (bool) $this->user()->root_admin; + return $this->user()->isRootAdmin(); } /** diff --git a/app/Http/Requests/Api/Client/Servers/Subusers/SubuserRequest.php b/app/Http/Requests/Api/Client/Servers/Subusers/SubuserRequest.php index 7a4d52430b..bd68fb8908 100644 --- a/app/Http/Requests/Api/Client/Servers/Subusers/SubuserRequest.php +++ b/app/Http/Requests/Api/Client/Servers/Subusers/SubuserRequest.php @@ -56,7 +56,7 @@ protected function validatePermissionsCanBeAssigned(array $permissions) $server = $this->route()->parameter('server'); // If we are a root admin or the server owner, no need to perform these checks. - if ($user->root_admin || $user->id === $server->owner_id) { + if ($user->isRootAdmin() || $user->id === $server->owner_id) { return; } diff --git a/app/Models/User.php b/app/Models/User.php index a6b4a1cfa6..940352d4d2 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -360,7 +360,7 @@ public function isLastRootAdmin(): bool public function isRootAdmin(): bool { - return $this->hasRole('Root Admin'); + return $this->root_admin || $this->hasRole('Root Admin'); } public function canAccessPanel(Panel $panel): bool diff --git a/app/Services/Servers/GetUserPermissionsService.php b/app/Services/Servers/GetUserPermissionsService.php index 8c58c23f62..882f8b7cc7 100644 --- a/app/Services/Servers/GetUserPermissionsService.php +++ b/app/Services/Servers/GetUserPermissionsService.php @@ -14,10 +14,10 @@ class GetUserPermissionsService */ public function handle(Server $server, User $user): array { - if ($user->root_admin || $user->id === $server->owner_id) { + if ($user->isRootAdmin() || $user->id === $server->owner_id) { $permissions = ['*']; - if ($user->root_admin) { + if ($user->isRootAdmin()) { $permissions[] = 'admin.websocket.errors'; $permissions[] = 'admin.websocket.install'; $permissions[] = 'admin.websocket.transfer'; diff --git a/app/Services/Users/UserCreationService.php b/app/Services/Users/UserCreationService.php index 2f53696031..d565aacbdc 100644 --- a/app/Services/Users/UserCreationService.php +++ b/app/Services/Users/UserCreationService.php @@ -8,6 +8,7 @@ use Illuminate\Database\ConnectionInterface; use Illuminate\Contracts\Auth\PasswordBroker; use App\Notifications\AccountCreated; +use Spatie\Permission\Models\Role; class UserCreationService { @@ -39,10 +40,17 @@ public function handle(array $data): User $data['password'] = $this->hasher->make(str_random(30)); } + $rootAdmin = $data['root_admin']; + unset($data['root_admin']); + $user = User::query()->forceCreate(array_merge($data, [ 'uuid' => Uuid::uuid4()->toString(), ])); + if ($rootAdmin) { + $user->assignRole(Role::findOrCreate('Root Admin')); + } + if (isset($generateResetToken)) { $token = $this->passwordBroker->createToken($user); } diff --git a/app/Transformers/Api/Application/BaseTransformer.php b/app/Transformers/Api/Application/BaseTransformer.php index 939266f82f..62ee4ceeb6 100644 --- a/app/Transformers/Api/Application/BaseTransformer.php +++ b/app/Transformers/Api/Application/BaseTransformer.php @@ -77,7 +77,7 @@ protected function authorize(string $resource): bool // the user is a root admin at the moment. In a future release we'll be rolling // out more specific permissions for keys. if ($token->key_type === ApiKey::TYPE_ACCOUNT) { - return $this->request->user()->root_admin; + return $this->request->user()->isRootAdmin(); } return AdminAcl::check($token, $resource); diff --git a/app/Transformers/Api/Application/UserTransformer.php b/app/Transformers/Api/Application/UserTransformer.php index ddec17e82b..e14ffb4c57 100644 --- a/app/Transformers/Api/Application/UserTransformer.php +++ b/app/Transformers/Api/Application/UserTransformer.php @@ -36,7 +36,7 @@ public function transform(User $user): array 'first_name' => $user->name_first, 'last_name' => $user->name_last, 'language' => $user->language, - 'root_admin' => (bool) $user->root_admin, + 'root_admin' => $user->isRootAdmin(), '2fa_enabled' => (bool) $user->use_totp, '2fa' => (bool) $user->use_totp, // deprecated, use "2fa_enabled" 'created_at' => $this->formatTimestamp($user->created_at), diff --git a/app/Transformers/Api/Client/ActivityLogTransformer.php b/app/Transformers/Api/Client/ActivityLogTransformer.php index 488ad1c954..58d5f13ae5 100644 --- a/app/Transformers/Api/Client/ActivityLogTransformer.php +++ b/app/Transformers/Api/Client/ActivityLogTransformer.php @@ -113,6 +113,6 @@ protected function hasAdditionalMetadata(ActivityLog $model): bool */ protected function canViewIP(Model $actor = null): bool { - return $actor?->is($this->request->user()) || $this->request->user()->root_admin; + return $actor?->is($this->request->user()) || $this->request->user()->isRootAdmin(); } } diff --git a/app/Transformers/Api/Client/UserTransformer.php b/app/Transformers/Api/Client/UserTransformer.php index 6b42ae507a..f875463e56 100644 --- a/app/Transformers/Api/Client/UserTransformer.php +++ b/app/Transformers/Api/Client/UserTransformer.php @@ -29,8 +29,8 @@ public function transform(User $user): array 'last_name' => $user->name_last, 'language' => $user->language, 'image' => 'https://gravatar.com/avatar/' . md5(Str::lower($user->email)), // deprecated - 'admin' => (bool) $user->root_admin, // deprecated, use "root_admin" - 'root_admin' => (bool) $user->root_admin, + 'admin' => $user->isRootAdmin(), // deprecated, use "root_admin" + 'root_admin' => $user->isRootAdmin(), '2fa_enabled' => (bool) $user->use_totp, 'created_at' => $this->formatTimestamp($user->created_at), 'updated_at' => $this->formatTimestamp($user->updated_at), From e16bbe39b531482c4d8e9793277b460b5634e00b Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 22 Jul 2024 13:24:59 +0200 Subject: [PATCH 06/47] add model specific permissions --- .../Resources/EggResource/Pages/ListEggs.php | 15 ++- app/Filament/Resources/RoleResource.php | 98 ++++++++++--------- 2 files changed, 67 insertions(+), 46 deletions(-) diff --git a/app/Filament/Resources/EggResource/Pages/ListEggs.php b/app/Filament/Resources/EggResource/Pages/ListEggs.php index 947d2231a7..68ddb4804e 100644 --- a/app/Filament/Resources/EggResource/Pages/ListEggs.php +++ b/app/Filament/Resources/EggResource/Pages/ListEggs.php @@ -4,6 +4,7 @@ use App\Filament\Resources\EggResource; use App\Models\Egg; +use App\Models\User; use App\Services\Eggs\Sharing\EggExporterService; use App\Services\Eggs\Sharing\EggImporterService; use Exception; @@ -55,7 +56,13 @@ public function table(Table $table): Table ->color('primary') ->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) { echo $service->handle($egg->id); - }, 'egg-' . $egg->getKebabName() . '.json')), + }, 'egg-' . $egg->getKebabName() . '.json')) + ->visible(function () { + /** @var User $user */ + $user = auth()->user(); + + return $user->can('export egg'); + }), ]) ->bulkActions([ BulkActionGroup::make([ @@ -138,6 +145,12 @@ protected function getHeaderActions(): array ->title('Import Success') ->success() ->send(); + }) + ->visible(function () { + /** @var User $user */ + $user = auth()->user(); + + return $user->can('import egg'); }), ]; } diff --git a/app/Filament/Resources/RoleResource.php b/app/Filament/Resources/RoleResource.php index 45b9efdee6..31fa0bc061 100644 --- a/app/Filament/Resources/RoleResource.php +++ b/app/Filament/Resources/RoleResource.php @@ -40,12 +40,64 @@ public static function getNavigationBadge(): ?string 'User', ]; + private const PERMISSION_PREFIXES = [ + 'viewAny', + 'view', + 'create', + 'update', + 'delete', + 'restore', + 'forceDelete', + ]; + + private const MODEL_SPECIFIC_PERMISSIONS = [ + 'Egg' => [ + 'import', + 'export', + ], + ]; + public static function form(Form $form): Form { $permissions = []; foreach (self::PERMISSION_MODELS as $model) { - $permissions[] = self::generatePermissionChecklist($model); + $options = []; + + foreach (self::PERMISSION_PREFIXES as $prefix) { + $options[$prefix . ' ' . strtolower($model)] = studly_case($prefix); + } + + if (array_key_exists($model, self::MODEL_SPECIFIC_PERMISSIONS)) { + foreach (self::MODEL_SPECIFIC_PERMISSIONS[$model] as $permission) { + $options[$permission . ' ' . strtolower($model)] = studly_case($permission); + } + } + + $permissions[] = CheckboxList::make($model) + ->label($model) + ->options($options) + ->bulkToggleable() + ->afterStateHydrated( + function (Component $component, string $operation, ?Model $record) use ($options) { + if (in_array($operation, ['edit', 'view'])) { + + if (blank($record)) { + return; + } + + if ($component->isVisible() && count($options) > 0) { + $component->state( + collect($options) + ->filter(fn ($value, $key) => $record->checkPermissionTo($key)) + ->keys() + ->toArray() + ); + } + } + } + ) + ->dehydrated(fn ($state) => !blank($state)); } return $form @@ -67,50 +119,6 @@ public static function form(Form $form): Form ]); } - private const PERMISSION_PREFIXES = [ - 'viewAny', - 'view', - 'create', - 'update', - 'delete', - 'restore', - 'forceDelete', - ]; - - private static function generatePermissionChecklist(string $model): CheckboxList - { - $options = []; - - foreach (self::PERMISSION_PREFIXES as $prefix) { - $options[$prefix . ' ' . strtolower($model)] = studly_case($prefix); - } - - return CheckboxList::make($model) - ->label($model) - ->options($options) - ->bulkToggleable() - ->afterStateHydrated( - function (Component $component, string $operation, ?Model $record) use ($options) { - if (in_array($operation, ['edit', 'view'])) { - - if (blank($record)) { - return; - } - - if ($component->isVisible() && count($options) > 0) { - $component->state( - collect($options) - ->filter(fn ($value, $key) => $record->checkPermissionTo($key)) - ->keys() - ->toArray() - ); - } - } - } - ) - ->dehydrated(fn ($state) => !blank($state)); - } - public static function getPages(): array { return [ From 18249e5a1dca8c8810c32f53ed0b79992d3148ff Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 22 Jul 2024 14:33:06 +0200 Subject: [PATCH 07/47] make permission selection nicer --- app/Filament/Resources/RoleResource.php | 74 ++++++++++++------- .../RoleResource/Pages/ListRoles.php | 1 + 2 files changed, 48 insertions(+), 27 deletions(-) diff --git a/app/Filament/Resources/RoleResource.php b/app/Filament/Resources/RoleResource.php index 31fa0bc061..c0e45b7355 100644 --- a/app/Filament/Resources/RoleResource.php +++ b/app/Filament/Resources/RoleResource.php @@ -4,8 +4,10 @@ use App\Filament\Resources\RoleResource\Pages; use Filament\Facades\Filament; +use Filament\Forms\Components\Actions\Action; use Filament\Forms\Components\CheckboxList; use Filament\Forms\Components\Component; +use Filament\Forms\Components\Fieldset; use Filament\Forms\Components\Placeholder; use Filament\Forms\Components\Section; use Filament\Forms\Components\TextInput; @@ -13,6 +15,7 @@ use Filament\Forms\Get; use Filament\Resources\Resource; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Str; use Spatie\Permission\Models\Role; class RoleResource extends Resource @@ -65,42 +68,58 @@ public static function form(Form $form): Form $options = []; foreach (self::PERMISSION_PREFIXES as $prefix) { - $options[$prefix . ' ' . strtolower($model)] = studly_case($prefix); + $options[$prefix . ' ' . strtolower($model)] = Str::headline($prefix); } if (array_key_exists($model, self::MODEL_SPECIFIC_PERMISSIONS)) { foreach (self::MODEL_SPECIFIC_PERMISSIONS[$model] as $permission) { - $options[$permission . ' ' . strtolower($model)] = studly_case($permission); + $options[$permission . ' ' . strtolower($model)] = Str::headline($permission); } } - $permissions[] = CheckboxList::make($model) - ->label($model) - ->options($options) - ->bulkToggleable() - ->afterStateHydrated( - function (Component $component, string $operation, ?Model $record) use ($options) { - if (in_array($operation, ['edit', 'view'])) { - - if (blank($record)) { - return; - } - - if ($component->isVisible() && count($options) > 0) { - $component->state( - collect($options) - ->filter(fn ($value, $key) => $record->checkPermissionTo($key)) - ->keys() - ->toArray() - ); + $permissions[] = Section::make(Str::headline(Str::plural($model))) + ->columnSpan(1) + ->collapsible() + ->collapsed() + ->icon(('\App\Filament\Resources\\' . $model . 'Resource')::getNavigationIcon()) + ->headerActions([ + Action::make('count') + ->label(fn (Get $get) => count($get(strtolower($model) . '_list'))) + ->badge(), + ]) + ->schema([ + CheckboxList::make(strtolower($model) . '_list') + ->label('') + ->options($options) + ->columns() + ->gridDirection('row') + ->bulkToggleable() + ->live() + ->afterStateHydrated( + function (Component $component, string $operation, ?Model $record) use ($options) { + if (in_array($operation, ['edit', 'view'])) { + + if (blank($record)) { + return; + } + + if ($component->isVisible() && count($options) > 0) { + $component->state( + collect($options) + ->filter(fn ($value, $key) => $record->checkPermissionTo($key)) + ->keys() + ->toArray() + ); + } + } } - } - } - ) - ->dehydrated(fn ($state) => !blank($state)); + ) + ->dehydrated(fn ($state) => !blank($state)), + ]); } return $form + ->columns(1) ->schema([ TextInput::make('name') ->label('Role Name') @@ -108,8 +127,9 @@ function (Component $component, string $operation, ?Model $record) use ($options TextInput::make('guard_name') ->label('Guard Name') ->default(Filament::getCurrentPanel()?->getAuthGuard() ?? '') - ->nullable(), - Section::make('Permissions') + ->nullable() + ->hidden(), + Fieldset::make('Permissions') ->columns(3) ->schema($permissions) ->hidden(fn (Get $get) => $get('name') === 'Root Admin'), diff --git a/app/Filament/Resources/RoleResource/Pages/ListRoles.php b/app/Filament/Resources/RoleResource/Pages/ListRoles.php index 5e3af2f071..82577d128b 100644 --- a/app/Filament/Resources/RoleResource/Pages/ListRoles.php +++ b/app/Filament/Resources/RoleResource/Pages/ListRoles.php @@ -25,6 +25,7 @@ public function table(Table $table): Table ->sortable() ->searchable(), TextColumn::make('guard_name') + ->hidden() ->sortable() ->searchable(), TextColumn::make('permissions_count') From 18d52f3c2462362167c6f23ee7047e7bd057c546 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 22 Jul 2024 14:33:19 +0200 Subject: [PATCH 08/47] fix user creation --- app/Services/Users/UserCreationService.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/Services/Users/UserCreationService.php b/app/Services/Users/UserCreationService.php index d565aacbdc..2f53696031 100644 --- a/app/Services/Users/UserCreationService.php +++ b/app/Services/Users/UserCreationService.php @@ -8,7 +8,6 @@ use Illuminate\Database\ConnectionInterface; use Illuminate\Contracts\Auth\PasswordBroker; use App\Notifications\AccountCreated; -use Spatie\Permission\Models\Role; class UserCreationService { @@ -40,17 +39,10 @@ public function handle(array $data): User $data['password'] = $this->hasher->make(str_random(30)); } - $rootAdmin = $data['root_admin']; - unset($data['root_admin']); - $user = User::query()->forceCreate(array_merge($data, [ 'uuid' => Uuid::uuid4()->toString(), ])); - if ($rootAdmin) { - $user->assignRole(Role::findOrCreate('Root Admin')); - } - if (isset($generateResetToken)) { $token = $this->passwordBroker->createToken($user); } From f3d6c0a2badd63321adde830f3eb591194789ff3 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 22 Jul 2024 14:33:40 +0200 Subject: [PATCH 09/47] fix tests --- app/Http/Middleware/AdminAuthenticate.php | 2 +- .../Middleware/Api/Application/AuthenticateApplicationUser.php | 2 +- tests/TestCase.php | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/Http/Middleware/AdminAuthenticate.php b/app/Http/Middleware/AdminAuthenticate.php index 6b86a86ee0..dc3296b06c 100644 --- a/app/Http/Middleware/AdminAuthenticate.php +++ b/app/Http/Middleware/AdminAuthenticate.php @@ -14,7 +14,7 @@ class AdminAuthenticate */ public function handle(Request $request, \Closure $next): mixed { - if (!$request->user() || !$request->user()->isRootAdmin()) { + if (!$request->user() || !$request->user()->root_admin) { throw new AccessDeniedHttpException(); } diff --git a/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php b/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php index 054739d27a..a18c58baff 100644 --- a/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php +++ b/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php @@ -15,7 +15,7 @@ public function handle(Request $request, \Closure $next): mixed { /** @var \App\Models\User|null $user */ $user = $request->user(); - if (!$user || !$user->isRootAdmin()) { + if (!$user || !$user->root_admin) { throw new AccessDeniedHttpException('This account does not have permission to access the API.'); } diff --git a/tests/TestCase.php b/tests/TestCase.php index fabaa7762d..abcd9d84d8 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -5,6 +5,7 @@ use Carbon\Carbon; use Carbon\CarbonImmutable; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; +use Spatie\Permission\PermissionRegistrar; abstract class TestCase extends BaseTestCase { @@ -28,6 +29,8 @@ protected function setUp(): void config()->set('app.debug', false); $this->setKnownUuidFactory(); + + $this->app->make(PermissionRegistrar::class)->forgetCachedPermissions(); } /** From 7806182ea4f82123f88b4072387cdbdde5dc0875 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 22 Jul 2024 14:36:06 +0200 Subject: [PATCH 10/47] add back subuser checks in server policy --- app/Policies/ServerPolicy.php | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/app/Policies/ServerPolicy.php b/app/Policies/ServerPolicy.php index e231fefa1d..f2d9e5411f 100644 --- a/app/Policies/ServerPolicy.php +++ b/app/Policies/ServerPolicy.php @@ -2,9 +2,39 @@ namespace App\Policies; +use App\Models\Server; +use App\Models\User; + class ServerPolicy { use DefaultPolicies; protected string $modelName = 'server'; + + /** + * Runs before any of the functions are called. Used to determine if the (sub-)user has permissions. + */ + public function before(User $user, string $ability, Server $server): bool + { + if ($server->owner_id === $user->id) { + return true; + } + + $subuser = $server->subusers->where('user_id', $user->id)->first(); + if (!$subuser || empty($ability)) { + return false; + } + + return in_array($ability, $subuser->permissions); + } + + /** + * This is a horrendous hack to avoid Laravel's "smart" behavior that does + * not call the before() function if there isn't a function matching the + * policy permission. + */ + public function __call(string $name, mixed $arguments) + { + // do nothing + } } From 27880d5832cd380f02679066940148b0cdfb57ab Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 22 Jul 2024 14:50:45 +0200 Subject: [PATCH 11/47] add custom model for role --- app/Filament/Resources/RoleResource.php | 6 +++--- .../Resources/RoleResource/Pages/EditRole.php | 6 +++--- .../Resources/RoleResource/Pages/ListRoles.php | 6 +++--- app/Models/Role.php | 15 +++++++++++++++ app/Models/User.php | 2 +- app/Providers/AppServiceProvider.php | 4 ---- config/permission.php | 2 +- database/Seeders/DatabaseSeeder.php | 4 ++-- 8 files changed, 28 insertions(+), 17 deletions(-) create mode 100644 app/Models/Role.php diff --git a/app/Filament/Resources/RoleResource.php b/app/Filament/Resources/RoleResource.php index c0e45b7355..6d641ae944 100644 --- a/app/Filament/Resources/RoleResource.php +++ b/app/Filament/Resources/RoleResource.php @@ -3,6 +3,7 @@ namespace App\Filament\Resources; use App\Filament\Resources\RoleResource\Pages; +use App\Models\Role; use Filament\Facades\Filament; use Filament\Forms\Components\Actions\Action; use Filament\Forms\Components\CheckboxList; @@ -16,7 +17,6 @@ use Filament\Resources\Resource; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Str; -use Spatie\Permission\Models\Role; class RoleResource extends Resource { @@ -132,10 +132,10 @@ function (Component $component, string $operation, ?Model $record) use ($options Fieldset::make('Permissions') ->columns(3) ->schema($permissions) - ->hidden(fn (Get $get) => $get('name') === 'Root Admin'), + ->hidden(fn (Get $get) => $get('name') === Role::ROOT_ADMIN), Placeholder::make('permissions') ->content('The Root Admin has all permissions.') - ->visible(fn (Get $get) => $get('name') === 'Root Admin'), + ->visible(fn (Get $get) => $get('name') === Role::ROOT_ADMIN), ]); } diff --git a/app/Filament/Resources/RoleResource/Pages/EditRole.php b/app/Filament/Resources/RoleResource/Pages/EditRole.php index 3bbd8977d2..e7710581d1 100644 --- a/app/Filament/Resources/RoleResource/Pages/EditRole.php +++ b/app/Filament/Resources/RoleResource/Pages/EditRole.php @@ -3,12 +3,12 @@ namespace App\Filament\Resources\RoleResource\Pages; use App\Filament\Resources\RoleResource; +use App\Models\Role; use Filament\Actions; use Filament\Resources\Pages\EditRecord; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Spatie\Permission\Models\Permission; -use Spatie\Permission\Models\Role; class EditRole extends EditRecord { @@ -46,8 +46,8 @@ protected function getHeaderActions(): array { return [ Actions\DeleteAction::make() - ->disabled(fn (Role $role) => $role->name === 'Root Admin') - ->label(fn (Role $role) => $role->name === 'Root Admin' ? 'Can\'t delete Root Admin' : 'Delete'), + ->disabled(fn (Role $role) => $role->isRootAdmin()) + ->label(fn (Role $role) => $role->isRootAdmin() ? 'Can\'t delete Root Admin' : 'Delete'), ]; } } diff --git a/app/Filament/Resources/RoleResource/Pages/ListRoles.php b/app/Filament/Resources/RoleResource/Pages/ListRoles.php index 82577d128b..775482f39a 100644 --- a/app/Filament/Resources/RoleResource/Pages/ListRoles.php +++ b/app/Filament/Resources/RoleResource/Pages/ListRoles.php @@ -3,6 +3,7 @@ namespace App\Filament\Resources\RoleResource\Pages; use App\Filament\Resources\RoleResource; +use App\Models\Role; use Filament\Actions; use Filament\Resources\Pages\ListRecords; use Filament\Tables\Actions\BulkActionGroup; @@ -11,7 +12,6 @@ use Filament\Tables\Actions\EditAction; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; -use Spatie\Permission\Models\Role; class ListRoles extends ListRecords { @@ -32,12 +32,12 @@ public function table(Table $table): Table ->label('Permissions') ->badge() ->counts('permissions') - ->formatStateUsing(fn (Role $role, $state) => $role->name === 'Root Admin' ? 'All' : $state), + ->formatStateUsing(fn (Role $role, $state) => $role->isRootAdmin() ? 'All' : $state), ]) ->actions([ EditAction::make(), ]) - ->checkIfRecordIsSelectableUsing(fn (Role $role) => $role->name !== 'Root Admin') + ->checkIfRecordIsSelectableUsing(fn (Role $role) => !$role->isRootAdmin()) ->bulkActions([ BulkActionGroup::make([ DeleteBulkAction::make(), diff --git a/app/Models/Role.php b/app/Models/Role.php new file mode 100644 index 0000000000..caddef265a --- /dev/null +++ b/app/Models/Role.php @@ -0,0 +1,15 @@ +name === self::ROOT_ADMIN; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 940352d4d2..7743b695cd 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -360,7 +360,7 @@ public function isLastRootAdmin(): bool public function isRootAdmin(): bool { - return $this->root_admin || $this->hasRole('Root Admin'); + return $this->root_admin || $this->hasRole(Role::ROOT_ADMIN); } public function canAccessPanel(Panel $panel): bool diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 38ade908e8..502fc02bac 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -7,7 +7,6 @@ use App\Models\ApiKey; use App\Models\Node; use App\Models\User; -use App\Policies\RolePolicy; use App\Services\Helpers\SoftwareVersionService; use Dedoc\Scramble\Scramble; use Dedoc\Scramble\Support\Generator\OpenApi; @@ -23,7 +22,6 @@ use Illuminate\Support\ServiceProvider; use Illuminate\Support\Str; use Laravel\Sanctum\Sanctum; -use Spatie\Permission\Models\Role; class AppServiceProvider extends ServiceProvider { @@ -87,8 +85,6 @@ public function boot(): void Gate::before(function (User $user, $ability) { return $user->isRootAdmin() ? true : null; }); - - Gate::policy(Role::class, RolePolicy::class); } /** diff --git a/config/permission.php b/config/permission.php index 2a520f3512..f9309973cc 100644 --- a/config/permission.php +++ b/config/permission.php @@ -24,7 +24,7 @@ * `Spatie\Permission\Contracts\Role` contract. */ - 'role' => Spatie\Permission\Models\Role::class, + 'role' => \App\Models\Role::class, ], diff --git a/database/Seeders/DatabaseSeeder.php b/database/Seeders/DatabaseSeeder.php index 6ae6775382..2f7f6694e1 100644 --- a/database/Seeders/DatabaseSeeder.php +++ b/database/Seeders/DatabaseSeeder.php @@ -2,8 +2,8 @@ namespace Database\Seeders; +use App\Models\Role; use Illuminate\Database\Seeder; -use Spatie\Permission\Models\Role; class DatabaseSeeder extends Seeder { @@ -14,6 +14,6 @@ public function run() { $this->call(EggSeeder::class); - Role::firstOrCreate(['name' => 'Root Admin']); + Role::firstOrCreate(['name' => Role::ROOT_ADMIN]); } } From 8da88c605aaf57587443fe611ba33ad4212dd08b Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 22 Jul 2024 14:56:42 +0200 Subject: [PATCH 12/47] assign new users to role if root_admin is set --- app/Services/Users/UserCreationService.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/Services/Users/UserCreationService.php b/app/Services/Users/UserCreationService.php index 2f53696031..b55f1a4508 100644 --- a/app/Services/Users/UserCreationService.php +++ b/app/Services/Users/UserCreationService.php @@ -2,6 +2,7 @@ namespace App\Services\Users; +use App\Models\Role; use Ramsey\Uuid\Uuid; use App\Models\User; use Illuminate\Contracts\Hashing\Hasher; @@ -43,6 +44,10 @@ public function handle(array $data): User 'uuid' => Uuid::uuid4()->toString(), ])); + if (array_key_exists('root_admin', $data) && $data['root_admin']) { + $user->syncRoles(Role::findOrCreate(Role::ROOT_ADMIN)); + } + if (isset($generateResetToken)) { $token = $this->passwordBroker->createToken($user); } From da85e514a88775ee76f1e4b5e26ee6a20b99c0a8 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 22 Jul 2024 15:25:21 +0200 Subject: [PATCH 13/47] add api for roles --- .../Api/Application/Roles/RoleController.php | 88 +++++++++++++++++++ .../Api/Application/Users/UserController.php | 14 +++ .../Application/Roles/DeleteRoleRequest.php | 13 +++ .../Api/Application/Roles/GetRoleRequest.php | 13 +++ .../Application/Roles/StoreRoleRequest.php | 19 ++++ .../Application/Roles/UpdateRoleRequest.php | 16 ++++ .../Users/AssignUserRolesRequest.php | 17 ++++ app/Models/Role.php | 2 + app/Services/Acl/Api/AdminAcl.php | 1 + .../Application/RolePermissionTransformer.php | 23 +++++ .../Api/Application/RoleTransformer.php | 47 ++++++++++ .../Api/Application/UserTransformer.php | 22 ++++- routes/api-application.php | 21 +++++ 13 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 app/Http/Controllers/Api/Application/Roles/RoleController.php create mode 100644 app/Http/Requests/Api/Application/Roles/DeleteRoleRequest.php create mode 100644 app/Http/Requests/Api/Application/Roles/GetRoleRequest.php create mode 100644 app/Http/Requests/Api/Application/Roles/StoreRoleRequest.php create mode 100644 app/Http/Requests/Api/Application/Roles/UpdateRoleRequest.php create mode 100644 app/Http/Requests/Api/Application/Users/AssignUserRolesRequest.php create mode 100644 app/Transformers/Api/Application/RolePermissionTransformer.php create mode 100644 app/Transformers/Api/Application/RoleTransformer.php diff --git a/app/Http/Controllers/Api/Application/Roles/RoleController.php b/app/Http/Controllers/Api/Application/Roles/RoleController.php new file mode 100644 index 0000000000..9a776c442e --- /dev/null +++ b/app/Http/Controllers/Api/Application/Roles/RoleController.php @@ -0,0 +1,88 @@ +allowedFilters(['name']) + ->allowedSorts(['name']) + ->paginate($request->query('per_page') ?? 10); + + return $this->fractal->collection($roles) + ->transformWith($this->getTransformer(RoleTransformer::class)) + ->toArray(); + } + + /** + * Return a single role. + */ + public function view(GetRoleRequest $request, Role $role): array + { + return $this->fractal->item($role) + ->transformWith($this->getTransformer(RoleTransformer::class)) + ->toArray(); + } + + /** + * Store a new role on the Panel and return an HTTP/201 response code with the + * new role attached. + * + * @throws \Throwable + */ + public function store(StoreRoleRequest $request): JsonResponse + { + $role = Role::create($request->validated()); + + return $this->fractal->item($role) + ->transformWith($this->getTransformer(RoleTransformer::class)) + ->addMeta([ + 'resource' => route('api.application.roles.view', [ + 'role' => $role->id, + ]), + ]) + ->respond(201); + } + + /** + * Update a role on the Panel and return the updated record to the user. + * + * @throws \Throwable + */ + public function update(UpdateRoleRequest $request, Role $role): array + { + $role->update($request->validated()); + + return $this->fractal->item($role) + ->transformWith($this->getTransformer(RoleTransformer::class)) + ->toArray(); + } + + /** + * Delete a role from the Panel. + * + * @throws \Exception + */ + public function delete(DeleteRoleRequest $request, Role $role): Response + { + $role->delete(); + + return $this->returnNoContent(); + } +} diff --git a/app/Http/Controllers/Api/Application/Users/UserController.php b/app/Http/Controllers/Api/Application/Users/UserController.php index a561da508d..f7ed424967 100644 --- a/app/Http/Controllers/Api/Application/Users/UserController.php +++ b/app/Http/Controllers/Api/Application/Users/UserController.php @@ -13,6 +13,7 @@ use App\Http\Requests\Api\Application\Users\DeleteUserRequest; use App\Http\Requests\Api\Application\Users\UpdateUserRequest; use App\Http\Controllers\Api\Application\ApplicationApiController; +use App\Http\Requests\Api\Application\Users\AssignUserRolesRequest; class UserController extends ApplicationApiController { @@ -75,6 +76,19 @@ public function update(UpdateUserRequest $request, User $user): array return $response->toArray(); } + /** + * Assign roles to a user. + */ + public function roles(AssignUserRolesRequest $request, User $user): array + { + $user->syncRoles($request->input('roles')); + + $response = $this->fractal->item($user) + ->transformWith($this->getTransformer(UserTransformer::class)); + + return $response->toArray(); + } + /** * Store a new user on the system. Returns the created user and an HTTP/201 * header on successful creation. diff --git a/app/Http/Requests/Api/Application/Roles/DeleteRoleRequest.php b/app/Http/Requests/Api/Application/Roles/DeleteRoleRequest.php new file mode 100644 index 0000000000..43c005fe8f --- /dev/null +++ b/app/Http/Requests/Api/Application/Roles/DeleteRoleRequest.php @@ -0,0 +1,13 @@ +route()->parameter('role'); + + return $rules ?? Role::getRulesForUpdate($role->name); + } +} diff --git a/app/Http/Requests/Api/Application/Users/AssignUserRolesRequest.php b/app/Http/Requests/Api/Application/Users/AssignUserRolesRequest.php new file mode 100644 index 0000000000..b042289668 --- /dev/null +++ b/app/Http/Requests/Api/Application/Users/AssignUserRolesRequest.php @@ -0,0 +1,17 @@ + 'array', + 'roles.*' => 'string', + ]; + } +} diff --git a/app/Models/Role.php b/app/Models/Role.php index caddef265a..0dc2705fe6 100644 --- a/app/Models/Role.php +++ b/app/Models/Role.php @@ -6,6 +6,8 @@ class Role extends BaseRole { + public const RESOURCE_NAME = 'role'; + public const ROOT_ADMIN = 'Root Admin'; public function isRootAdmin(): bool diff --git a/app/Services/Acl/Api/AdminAcl.php b/app/Services/Acl/Api/AdminAcl.php index 3a0e4961c1..dd58e317c6 100644 --- a/app/Services/Acl/Api/AdminAcl.php +++ b/app/Services/Acl/Api/AdminAcl.php @@ -32,6 +32,7 @@ class AdminAcl public const RESOURCE_DATABASE_HOSTS = 'database_hosts'; public const RESOURCE_SERVER_DATABASES = 'server_databases'; public const RESOURCE_MOUNTS = 'mounts'; + public const RESOURCE_ROLES = 'roles'; /** * Determine if an API key has permission to perform a specific read/write operation. diff --git a/app/Transformers/Api/Application/RolePermissionTransformer.php b/app/Transformers/Api/Application/RolePermissionTransformer.php new file mode 100644 index 0000000000..968a54f8fc --- /dev/null +++ b/app/Transformers/Api/Application/RolePermissionTransformer.php @@ -0,0 +1,23 @@ + $model->name, + 'guard_name' => $model->guard_name, + 'created_at' => $model->created_at->toAtomString(), + 'updated_at' => $model->updated_at->toAtomString(), + ]; + } +} diff --git a/app/Transformers/Api/Application/RoleTransformer.php b/app/Transformers/Api/Application/RoleTransformer.php new file mode 100644 index 0000000000..d77b04e570 --- /dev/null +++ b/app/Transformers/Api/Application/RoleTransformer.php @@ -0,0 +1,47 @@ + $model->name, + 'guard_name' => $model->guard_name, + 'created_at' => $model->created_at->toAtomString(), + 'updated_at' => $model->updated_at->toAtomString(), + ]; + } + + /** + * Include the permissions associated with this role. + * + * @throws \App\Exceptions\Transformer\InvalidTransformerLevelException + */ + public function includePermissions(Role $model): Collection|NullResource + { + $model->loadMissing('permissions'); + + return $this->collection($model->getRelation('permissions'), $this->makeTransformer(RolePermissionTransformer::class), 'permissions'); + } +} diff --git a/app/Transformers/Api/Application/UserTransformer.php b/app/Transformers/Api/Application/UserTransformer.php index e14ffb4c57..5cf800c08a 100644 --- a/app/Transformers/Api/Application/UserTransformer.php +++ b/app/Transformers/Api/Application/UserTransformer.php @@ -2,6 +2,7 @@ namespace App\Transformers\Api\Application; +use App\Models\Role; use App\Models\User; use League\Fractal\Resource\Collection; use League\Fractal\Resource\NullResource; @@ -12,7 +13,10 @@ class UserTransformer extends BaseTransformer /** * List of resources that can be included. */ - protected array $availableIncludes = ['servers']; + protected array $availableIncludes = [ + 'servers', + 'roles', + ]; /** * Return the resource name for the JSONAPI output. @@ -59,4 +63,20 @@ public function includeServers(User $user): Collection|NullResource return $this->collection($user->getRelation('servers'), $this->makeTransformer(ServerTransformer::class), 'server'); } + + /** + * Return the roles associated with this user. + * + * @throws \App\Exceptions\Transformer\InvalidTransformerLevelException + */ + public function includeRoles(User $user): Collection|NullResource + { + if (!$this->authorize(AdminAcl::RESOURCE_ROLES)) { + return $this->null(); + } + + $user->loadMissing('roles'); + + return $this->collection($user->getRelation('roles'), $this->makeTransformer(RoleTransformer::class), Role::RESOURCE_NAME); + } } diff --git a/routes/api-application.php b/routes/api-application.php index c213c74ab3..cf5cbb9dfc 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -19,6 +19,8 @@ Route::post('/', [Application\Users\UserController::class, 'store']); Route::patch('/{user:id}', [Application\Users\UserController::class, 'update']); + Route::patch('/{user:id}/roles', [Application\Users\UserController::class, 'roles']); + Route::delete('/{user:id}', [Application\Users\UserController::class, 'delete']); }); @@ -141,3 +143,22 @@ Route::delete('/{mount:id}/eggs/{egg_id}', [Application\Mounts\MountController::class, 'deleteEgg']); Route::delete('/{mount:id}/nodes/{node_id}', [Application\Mounts\MountController::class, 'deleteNode']); }); + +/* +|-------------------------------------------------------------------------- +| Role Controller Routes +|-------------------------------------------------------------------------- +| +| Endpoint: /api/application/roles +| +*/ +Route::group(['prefix' => '/roles'], function () { + Route::get('/', [Application\Roles\RoleController::class, 'index'])->name('api.application.roles'); + Route::get('/{role:id}', [Application\Roles\RoleController::class, 'view'])->name('api.application.roles.view'); + + Route::post('/', [Application\Roles\RoleController::class, 'store']); + + Route::patch('/{role:id}', [Application\Roles\RoleController::class, 'update']); + + Route::delete('/{role:id}', [Application\Roles\RoleController::class, 'delete']); +}); From 4d809cd2a89cdbc5825234a69537cb4805541105 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 22 Jul 2024 15:44:00 +0200 Subject: [PATCH 14/47] fix phpstan --- app/Filament/Resources/RoleResource.php | 5 ++--- app/Filament/Resources/RoleResource/Pages/CreateRole.php | 4 ++++ app/Filament/Resources/RoleResource/Pages/EditRole.php | 3 +++ .../Requests/Api/Application/Roles/StoreRoleRequest.php | 6 ++++-- .../Requests/Api/Application/Roles/UpdateRoleRequest.php | 9 --------- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/app/Filament/Resources/RoleResource.php b/app/Filament/Resources/RoleResource.php index 6d641ae944..232c7489c1 100644 --- a/app/Filament/Resources/RoleResource.php +++ b/app/Filament/Resources/RoleResource.php @@ -15,7 +15,6 @@ use Filament\Forms\Form; use Filament\Forms\Get; use Filament\Resources\Resource; -use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Str; class RoleResource extends Resource @@ -96,14 +95,14 @@ public static function form(Form $form): Form ->bulkToggleable() ->live() ->afterStateHydrated( - function (Component $component, string $operation, ?Model $record) use ($options) { + function (Component $component, string $operation, ?Role $record) use ($options) { if (in_array($operation, ['edit', 'view'])) { if (blank($record)) { return; } - if ($component->isVisible() && count($options) > 0) { + if ($component->isVisible()) { $component->state( collect($options) ->filter(fn ($value, $key) => $record->checkPermissionTo($key)) diff --git a/app/Filament/Resources/RoleResource/Pages/CreateRole.php b/app/Filament/Resources/RoleResource/Pages/CreateRole.php index 6e88f86632..2f69bba011 100644 --- a/app/Filament/Resources/RoleResource/Pages/CreateRole.php +++ b/app/Filament/Resources/RoleResource/Pages/CreateRole.php @@ -3,11 +3,15 @@ namespace App\Filament\Resources\RoleResource\Pages; use App\Filament\Resources\RoleResource; +use App\Models\Role; use Filament\Resources\Pages\CreateRecord; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Spatie\Permission\Models\Permission; +/** + * @property Role $record + */ class CreateRole extends CreateRecord { protected static string $resource = RoleResource::class; diff --git a/app/Filament/Resources/RoleResource/Pages/EditRole.php b/app/Filament/Resources/RoleResource/Pages/EditRole.php index e7710581d1..5e34eb0c2b 100644 --- a/app/Filament/Resources/RoleResource/Pages/EditRole.php +++ b/app/Filament/Resources/RoleResource/Pages/EditRole.php @@ -10,6 +10,9 @@ use Illuminate\Support\Collection; use Spatie\Permission\Models\Permission; +/** + * @property Role $record + */ class EditRole extends EditRecord { protected static string $resource = RoleResource::class; diff --git a/app/Http/Requests/Api/Application/Roles/StoreRoleRequest.php b/app/Http/Requests/Api/Application/Roles/StoreRoleRequest.php index b84fa164e1..4a9aa21f67 100644 --- a/app/Http/Requests/Api/Application/Roles/StoreRoleRequest.php +++ b/app/Http/Requests/Api/Application/Roles/StoreRoleRequest.php @@ -4,7 +4,6 @@ use App\Services\Acl\Api\AdminAcl; use App\Http\Requests\Api\Application\ApplicationApiRequest; -use App\Models\Role; class StoreRoleRequest extends ApplicationApiRequest { @@ -14,6 +13,9 @@ class StoreRoleRequest extends ApplicationApiRequest public function rules(array $rules = null): array { - return $rules ?? Role::getRules(); + return [ + 'name' => 'required|string', + 'guard_name' => 'nullable|string', + ]; } } diff --git a/app/Http/Requests/Api/Application/Roles/UpdateRoleRequest.php b/app/Http/Requests/Api/Application/Roles/UpdateRoleRequest.php index 381378cc73..48dc3d04e9 100644 --- a/app/Http/Requests/Api/Application/Roles/UpdateRoleRequest.php +++ b/app/Http/Requests/Api/Application/Roles/UpdateRoleRequest.php @@ -2,15 +2,6 @@ namespace App\Http\Requests\Api\Application\Roles; -use App\Models\Role; - class UpdateRoleRequest extends StoreRoleRequest { - public function rules(array $rules = null): array - { - /** @var Role $role */ - $role = $this->route()->parameter('role'); - - return $rules ?? Role::getRulesForUpdate($role->name); - } } From 434ffa72d6d9b42663cf33a40989dc47086a38ad Mon Sep 17 00:00:00 2001 From: Boy132 Date: Wed, 31 Jul 2024 10:28:42 +0200 Subject: [PATCH 15/47] add permissions for settings page --- app/Filament/Pages/Settings.php | 43 +++++++++- app/Filament/Resources/RoleResource.php | 108 +++++++++++++++--------- 2 files changed, 108 insertions(+), 43 deletions(-) diff --git a/app/Filament/Pages/Settings.php b/app/Filament/Pages/Settings.php index 5ec862795e..56c4085507 100644 --- a/app/Filament/Pages/Settings.php +++ b/app/Filament/Pages/Settings.php @@ -3,6 +3,7 @@ namespace App\Filament\Pages; use App\Models\Backup; +use App\Models\User; use App\Notifications\MailTested; use App\Traits\Commands\EnvironmentWriterTrait; use Exception; @@ -49,12 +50,26 @@ public function mount(): void $this->form->fill(); } + public static function canAccess(): bool + { + /** @var User $user */ + $user = auth()->user(); + + return $user->can('view Settings'); + } + protected function getFormSchema(): array { return [ Tabs::make('Tabs') ->columns() ->persistTabInQueryString() + ->disabled(function () { + /** @var User $user */ + $user = auth()->user(); + + return !$user->can('update Settings'); + }) ->tabs([ Tab::make('general') ->label('General') @@ -146,10 +161,22 @@ private function generalSettings(): array ->color('danger') ->icon('tabler-trash') ->requiresConfirmation() + ->disabled(function () { + /** @var User $user */ + $user = auth()->user(); + + return !$user->can('update Settings'); + }) ->action(fn (Set $set) => $set('TRUSTED_PROXIES', [])), FormAction::make('cloudflare') ->label('Set to Cloudflare IPs') ->icon('tabler-brand-cloudflare') + ->disabled(function () { + /** @var User $user */ + $user = auth()->user(); + + return !$user->can('update Settings'); + }) ->action(fn (Set $set) => $set('TRUSTED_PROXIES', [ '173.245.48.0/20', '103.21.244.0/22', @@ -225,6 +252,12 @@ private function mailSettings(): array ->label('Send Test Mail') ->icon('tabler-send') ->hidden(fn (Get $get) => $get('MAIL_MAILER') === 'log') + ->disabled(function () { + /** @var User $user */ + $user = auth()->user(); + + return !$user->can('update Settings'); + }) ->action(function () { try { MailNotification::route('mail', auth()->user()->email) @@ -562,12 +595,14 @@ protected function getHeaderActions(): array return [ Action::make('save') ->action('save') + ->hidden(function () { + /** @var User $user */ + $user = auth()->user(); + + return !$user->can('update Settings'); + }) ->keyBindings(['mod+s']), ]; } - protected function getFormActions(): array - { - return []; - } } diff --git a/app/Filament/Resources/RoleResource.php b/app/Filament/Resources/RoleResource.php index 232c7489c1..7e9307a7f4 100644 --- a/app/Filament/Resources/RoleResource.php +++ b/app/Filament/Resources/RoleResource.php @@ -59,6 +59,13 @@ public static function getNavigationBadge(): ?string ], ]; + private const SPECIAL_PERMISSIONS = [ + 'Settings' => [ + 'view', + 'update', + ], + ]; + public static function form(Form $form): Form { $permissions = []; @@ -76,45 +83,17 @@ public static function form(Form $form): Form } } - $permissions[] = Section::make(Str::headline(Str::plural($model))) - ->columnSpan(1) - ->collapsible() - ->collapsed() - ->icon(('\App\Filament\Resources\\' . $model . 'Resource')::getNavigationIcon()) - ->headerActions([ - Action::make('count') - ->label(fn (Get $get) => count($get(strtolower($model) . '_list'))) - ->badge(), - ]) - ->schema([ - CheckboxList::make(strtolower($model) . '_list') - ->label('') - ->options($options) - ->columns() - ->gridDirection('row') - ->bulkToggleable() - ->live() - ->afterStateHydrated( - function (Component $component, string $operation, ?Role $record) use ($options) { - if (in_array($operation, ['edit', 'view'])) { - - if (blank($record)) { - return; - } - - if ($component->isVisible()) { - $component->state( - collect($options) - ->filter(fn ($value, $key) => $record->checkPermissionTo($key)) - ->keys() - ->toArray() - ); - } - } - } - ) - ->dehydrated(fn ($state) => !blank($state)), - ]); + $permissions[] = self::makeSection($model, $options); + } + + foreach (self::SPECIAL_PERMISSIONS as $model => $prefixes) { + $options = []; + + foreach ($prefixes as $prefix) { + $options[$prefix . ' ' . strtolower($model)] = Str::headline($prefix); + } + + $permissions[] = self::makeSection($model, $options); } return $form @@ -138,6 +117,57 @@ function (Component $component, string $operation, ?Role $record) use ($options) ]); } + private static function makeSection(string $model, array $options): Section + { + $icon = null; + + if (class_exists('\App\Filament\Resources\\' . $model . 'Resource')) { + $icon = ('\App\Filament\Resources\\' . $model . 'Resource')::getNavigationIcon(); + } elseif (class_exists('\App\Filament\Pages\\' . $model)) { + $icon = ('\App\Filament\Pages\\' . $model)::getNavigationIcon(); + } + + return Section::make(Str::headline(Str::plural($model))) + ->columnSpan(1) + ->collapsible() + ->collapsed() + ->icon($icon) + ->headerActions([ + Action::make('count') + ->label(fn (Get $get) => count($get(strtolower($model) . '_list'))) + ->badge(), + ]) + ->schema([ + CheckboxList::make(strtolower($model) . '_list') + ->label('') + ->options($options) + ->columns() + ->gridDirection('row') + ->bulkToggleable() + ->live() + ->afterStateHydrated( + function (Component $component, string $operation, ?Role $record) use ($options) { + if (in_array($operation, ['edit', 'view'])) { + + if (blank($record)) { + return; + } + + if ($component->isVisible()) { + $component->state( + collect($options) + ->filter(fn ($value, $key) => $record->checkPermissionTo($key)) + ->keys() + ->toArray() + ); + } + } + } + ) + ->dehydrated(fn ($state) => !blank($state)), + ]); + } + public static function getPages(): array { return [ From de717a3079482c4c0fdc8c41b8dc1bbd1ce2dce8 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Thu, 1 Aug 2024 08:24:31 +0200 Subject: [PATCH 16/47] remove "restore" and "forceDelete" permissions --- app/Filament/Resources/RoleResource.php | 2 -- app/Policies/DefaultPolicies.php | 16 ---------------- 2 files changed, 18 deletions(-) diff --git a/app/Filament/Resources/RoleResource.php b/app/Filament/Resources/RoleResource.php index 7e9307a7f4..1b1ccac1e6 100644 --- a/app/Filament/Resources/RoleResource.php +++ b/app/Filament/Resources/RoleResource.php @@ -48,8 +48,6 @@ public static function getNavigationBadge(): ?string 'create', 'update', 'delete', - 'restore', - 'forceDelete', ]; private const MODEL_SPECIFIC_PERMISSIONS = [ diff --git a/app/Policies/DefaultPolicies.php b/app/Policies/DefaultPolicies.php index d20fcf9293..2415bfa704 100644 --- a/app/Policies/DefaultPolicies.php +++ b/app/Policies/DefaultPolicies.php @@ -46,20 +46,4 @@ public function delete(User $user, Model $model): bool { return $user->can('delete ' . $this->modelName, $model); } - - /** - * Determine whether the user can restore the model. - */ - public function restore(User $user, Model $model): bool - { - return $user->can('restore ' . $this->modelName, $model); - } - - /** - * Determine whether the user can permanently delete the model. - */ - public function forceDelete(User $user, Model $model): bool - { - return $user->can('forceDelete ' . $this->modelName, $model); - } } From 2fdd62eaf3e27276d92c93c76e10b72614034efd Mon Sep 17 00:00:00 2001 From: Boy132 Date: Thu, 1 Aug 2024 08:36:20 +0200 Subject: [PATCH 17/47] add user count to list --- app/Filament/Resources/RoleResource/Pages/ListRoles.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Filament/Resources/RoleResource/Pages/ListRoles.php b/app/Filament/Resources/RoleResource/Pages/ListRoles.php index 775482f39a..828f58c630 100644 --- a/app/Filament/Resources/RoleResource/Pages/ListRoles.php +++ b/app/Filament/Resources/RoleResource/Pages/ListRoles.php @@ -33,6 +33,10 @@ public function table(Table $table): Table ->badge() ->counts('permissions') ->formatStateUsing(fn (Role $role, $state) => $role->isRootAdmin() ? 'All' : $state), + TextColumn::make('users_count') + ->label('Users') + ->counts('users') + ->icon('tabler-users'), ]) ->actions([ EditAction::make(), From 231ed5ad494255f5a6fca7756eef6c897c9a79ca Mon Sep 17 00:00:00 2001 From: Boy132 Date: Thu, 1 Aug 2024 08:49:38 +0200 Subject: [PATCH 18/47] prevent deletion if role has users --- app/Filament/Resources/RoleResource/Pages/EditRole.php | 4 ++-- app/Filament/Resources/RoleResource/Pages/ListRoles.php | 2 +- app/Models/Role.php | 9 +++++++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/Filament/Resources/RoleResource/Pages/EditRole.php b/app/Filament/Resources/RoleResource/Pages/EditRole.php index 5e34eb0c2b..2ccab7ab92 100644 --- a/app/Filament/Resources/RoleResource/Pages/EditRole.php +++ b/app/Filament/Resources/RoleResource/Pages/EditRole.php @@ -49,8 +49,8 @@ protected function getHeaderActions(): array { return [ Actions\DeleteAction::make() - ->disabled(fn (Role $role) => $role->isRootAdmin()) - ->label(fn (Role $role) => $role->isRootAdmin() ? 'Can\'t delete Root Admin' : 'Delete'), + ->disabled(fn (Role $role) => $role->isRootAdmin() || $role->users_count >= 1) + ->label(fn (Role $role) => $role->isRootAdmin() ? 'Can\'t delete Root Admin' : ($role->users_count >= 1 ? 'In Use' : 'Delete')), ]; } } diff --git a/app/Filament/Resources/RoleResource/Pages/ListRoles.php b/app/Filament/Resources/RoleResource/Pages/ListRoles.php index 828f58c630..9ab76ac093 100644 --- a/app/Filament/Resources/RoleResource/Pages/ListRoles.php +++ b/app/Filament/Resources/RoleResource/Pages/ListRoles.php @@ -41,7 +41,7 @@ public function table(Table $table): Table ->actions([ EditAction::make(), ]) - ->checkIfRecordIsSelectableUsing(fn (Role $role) => !$role->isRootAdmin()) + ->checkIfRecordIsSelectableUsing(fn (Role $role) => !$role->isRootAdmin() && $role->users_count <= 0) ->bulkActions([ BulkActionGroup::make([ DeleteBulkAction::make(), diff --git a/app/Models/Role.php b/app/Models/Role.php index 0dc2705fe6..c4eef288cf 100644 --- a/app/Models/Role.php +++ b/app/Models/Role.php @@ -4,6 +4,15 @@ use Spatie\Permission\Models\Role as BaseRole; +/** + * @property int $id + * @property string $name + * @property string $guard_name + * @property \Illuminate\Database\Eloquent\Collection|\Spatie\Permission\Models\Permission[] $permissions + * @property int|null $permissions_count + * @property \Illuminate\Database\Eloquent\Collection|\App\Models\User[] $users + * @property int|null $users_count + */ class Role extends BaseRole { public const RESOURCE_NAME = 'role'; From 98733e9ca595053c871b92ef78f6496c3b4bce46 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Thu, 1 Aug 2024 08:54:50 +0200 Subject: [PATCH 19/47] update user list --- .../Resources/UserResource/Pages/ListUsers.php | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/app/Filament/Resources/UserResource/Pages/ListUsers.php b/app/Filament/Resources/UserResource/Pages/ListUsers.php index 6c820ebf6b..264544035b 100644 --- a/app/Filament/Resources/UserResource/Pages/ListUsers.php +++ b/app/Filament/Resources/UserResource/Pages/ListUsers.php @@ -38,19 +38,16 @@ public function table(Table $table): Table Tables\Columns\TextColumn::make('email') ->searchable() ->icon('tabler-mail'), - Tables\Columns\IconColumn::make('root_admin') - ->visibleFrom('md') - ->label('Root Admin') - ->boolean() - ->trueIcon('tabler-star-filled') - ->falseIcon('tabler-star-off') - ->sortable() - ->state(fn (User $user) => $user->isRootAdmin()), Tables\Columns\IconColumn::make('use_totp') ->label('2FA') ->visibleFrom('lg') ->icon(fn (User $user) => $user->use_totp ? 'tabler-lock' : 'tabler-lock-open-off') ->boolean()->sortable(), + Tables\Columns\TextColumn::make('roles_count') + ->counts('roles') + ->icon('tabler-users-group') + ->label('Roles') + ->formatStateUsing(fn (User $user, $state) => $state . ($user->isRootAdmin() ? ' (Root Admin)' : '')), Tables\Columns\TextColumn::make('servers_count') ->counts('servers') ->icon('tabler-server') From ac8e184c26fa48fcd943dc554a6563a9bb561288 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Thu, 1 Aug 2024 09:11:55 +0200 Subject: [PATCH 20/47] fix server policy --- app/Policies/ServerPolicy.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/Policies/ServerPolicy.php b/app/Policies/ServerPolicy.php index f2d9e5411f..463cee1bac 100644 --- a/app/Policies/ServerPolicy.php +++ b/app/Policies/ServerPolicy.php @@ -14,8 +14,13 @@ class ServerPolicy /** * Runs before any of the functions are called. Used to determine if the (sub-)user has permissions. */ - public function before(User $user, string $ability, Server $server): bool + public function before(User $user, string $ability, string|Server $server): ?bool { + // For "viewAny" the $server param is the class name + if (is_string($server)) { + return null; + } + if ($server->owner_id === $user->id) { return true; } From ebd63d87ec47a6cbf3fddd7592327957b4aa61cc Mon Sep 17 00:00:00 2001 From: Boy132 Date: Fri, 2 Aug 2024 09:09:34 +0200 Subject: [PATCH 21/47] remove old `root_admin` column --- .../ServerResource/Pages/CreateServer.php | 15 -------- .../Client/Servers/ActivityLogController.php | 6 ++-- app/Http/Middleware/AdminAuthenticate.php | 2 +- .../AuthenticateApplicationUser.php | 2 +- .../Requests/Admin/NewUserFormRequest.php | 1 - app/Http/Requests/Admin/UserFormRequest.php | 1 - .../Application/Users/StoreUserRequest.php | 2 -- app/Models/User.php | 8 +---- database/Factories/UserFactory.php | 9 ----- ..._08_01_114538_remove_root_admin_column.php | 35 +++++++++++++++++++ database/schema/mysql-schema.sql | 1 - lang/en/admin/user.php | 1 - .../ApplicationApiIntegrationTestCase.php | 8 +++-- .../Users/ExternalUserControllerTest.php | 2 +- .../Application/Users/UserControllerTest.php | 4 +-- .../Api/Client/ClientControllerTest.php | 7 ++-- .../SftpAuthenticationControllerTest.php | 5 +-- .../Http/Middleware/AdminAuthenticateTest.php | 6 ++-- .../Api/Application/AuthenticateUserTest.php | 6 ++-- 19 files changed, 65 insertions(+), 56 deletions(-) create mode 100644 database/migrations/2024_08_01_114538_remove_root_admin_column.php diff --git a/app/Filament/Resources/ServerResource/Pages/CreateServer.php b/app/Filament/Resources/ServerResource/Pages/CreateServer.php index 0d12c4cb95..925f79b47e 100644 --- a/app/Filament/Resources/ServerResource/Pages/CreateServer.php +++ b/app/Filament/Resources/ServerResource/Pages/CreateServer.php @@ -98,21 +98,6 @@ public function form(Form $form): Form ->hintIcon('tabler-question-mark') ->hintIconTooltip('Providing a user password is optional. New user email will prompt users to create a password the first time they login.') ->password(), - - Forms\Components\ToggleButtons::make('root_admin') - ->label('Administrator (Root)') - ->options([ - false => 'No', - true => 'Admin', - ]) - ->colors([ - false => 'primary', - true => 'danger', - ]) - ->inline() - ->required() - ->default(false) - ->hidden(), ]) ->createOptionUsing(function ($data) { resolve(UserCreationService::class)->handle($data); diff --git a/app/Http/Controllers/Api/Client/Servers/ActivityLogController.php b/app/Http/Controllers/Api/Client/Servers/ActivityLogController.php index efef026813..3c315729b1 100644 --- a/app/Http/Controllers/Api/Client/Servers/ActivityLogController.php +++ b/app/Http/Controllers/Api/Client/Servers/ActivityLogController.php @@ -13,6 +13,7 @@ use App\Http\Requests\Api\Client\ClientApiRequest; use App\Transformers\Api\Client\ActivityLogTransformer; use App\Http\Controllers\Api\Client\ClientApiController; +use App\Models\Role; class ActivityLogController extends ClientApiController { @@ -32,15 +33,16 @@ public function __invoke(ClientApiRequest $request, Server $server): array // We could do this with a query and a lot of joins, but that gets pretty // painful so for now we'll execute a simpler query. $subusers = $server->subusers()->pluck('user_id')->merge([$server->owner_id]); + $rootAdmins = Role::findOrCreate(Role::ROOT_ADMIN)->users()->pluck('id'); $builder->select('activity_logs.*') ->leftJoin('users', function (JoinClause $join) { $join->on('users.id', 'activity_logs.actor_id') ->where('activity_logs.actor_type', (new User())->getMorphClass()); }) - ->where(function (Builder $builder) use ($subusers) { + ->where(function (Builder $builder) use ($subusers, $rootAdmins) { $builder->whereNull('users.id') - ->orWhere('users.root_admin', 0) + ->orWhereNotIn('users.id', $rootAdmins) ->orWhereIn('users.id', $subusers); }); }) diff --git a/app/Http/Middleware/AdminAuthenticate.php b/app/Http/Middleware/AdminAuthenticate.php index dc3296b06c..6b86a86ee0 100644 --- a/app/Http/Middleware/AdminAuthenticate.php +++ b/app/Http/Middleware/AdminAuthenticate.php @@ -14,7 +14,7 @@ class AdminAuthenticate */ public function handle(Request $request, \Closure $next): mixed { - if (!$request->user() || !$request->user()->root_admin) { + if (!$request->user() || !$request->user()->isRootAdmin()) { throw new AccessDeniedHttpException(); } diff --git a/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php b/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php index a18c58baff..054739d27a 100644 --- a/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php +++ b/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php @@ -15,7 +15,7 @@ public function handle(Request $request, \Closure $next): mixed { /** @var \App\Models\User|null $user */ $user = $request->user(); - if (!$user || !$user->root_admin) { + if (!$user || !$user->isRootAdmin()) { throw new AccessDeniedHttpException('This account does not have permission to access the API.'); } diff --git a/app/Http/Requests/Admin/NewUserFormRequest.php b/app/Http/Requests/Admin/NewUserFormRequest.php index db2f1ebe35..e3b91f597f 100644 --- a/app/Http/Requests/Admin/NewUserFormRequest.php +++ b/app/Http/Requests/Admin/NewUserFormRequest.php @@ -22,7 +22,6 @@ public function rules(): array 'name_last', 'password', 'language', - 'root_admin', ])->toArray(); } } diff --git a/app/Http/Requests/Admin/UserFormRequest.php b/app/Http/Requests/Admin/UserFormRequest.php index f9fc7e6796..78a0a1bbd2 100644 --- a/app/Http/Requests/Admin/UserFormRequest.php +++ b/app/Http/Requests/Admin/UserFormRequest.php @@ -22,7 +22,6 @@ public function rules(): array 'name_last', 'password', 'language', - 'root_admin', ])->toArray(); } } diff --git a/app/Http/Requests/Api/Application/Users/StoreUserRequest.php b/app/Http/Requests/Api/Application/Users/StoreUserRequest.php index 24e6e8940c..43603639c4 100644 --- a/app/Http/Requests/Api/Application/Users/StoreUserRequest.php +++ b/app/Http/Requests/Api/Application/Users/StoreUserRequest.php @@ -26,7 +26,6 @@ public function rules(array $rules = null): array 'password', 'language', 'timezone', - 'root_admin', ])->toArray(); $response['first_name'] = $rules['name_first']; @@ -56,7 +55,6 @@ public function attributes(): array 'external_id' => 'Third Party Identifier', 'name_first' => 'First Name', 'name_last' => 'Last Name', - 'root_admin' => 'Root Administrator Status', ]; } } diff --git a/app/Models/User.php b/app/Models/User.php index 7743b695cd..7c32765f94 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -41,7 +41,6 @@ * @property string|null $remember_token * @property string $language * @property string $timezone - * @property bool $root_admin * @property bool $use_totp * @property string|null $totp_secret * @property \Illuminate\Support\Carbon|null $totp_authenticated_at @@ -78,7 +77,6 @@ * @method static Builder|User whereNameLast($value) * @method static Builder|User wherePassword($value) * @method static Builder|User whereRememberToken($value) - * @method static Builder|User whereRootAdmin($value) * @method static Builder|User whereTotpAuthenticatedAt($value) * @method static Builder|User whereTotpSecret($value) * @method static Builder|User whereUpdatedAt($value) @@ -133,7 +131,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac 'totp_secret', 'totp_authenticated_at', 'gravatar', - 'root_admin', 'oauth', ]; @@ -147,7 +144,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac */ protected $attributes = [ 'external_id' => null, - 'root_admin' => false, 'language' => 'en', 'timezone' => 'UTC', 'use_totp' => false, @@ -168,7 +164,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac 'name_first' => 'nullable|string|between:0,255', 'name_last' => 'nullable|string|between:0,255', 'password' => 'sometimes|nullable|string', - 'root_admin' => 'boolean', 'language' => 'string', 'timezone' => 'string', 'use_totp' => 'boolean', @@ -179,7 +174,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac protected function casts(): array { return [ - 'root_admin' => 'boolean', 'use_totp' => 'boolean', 'gravatar' => 'boolean', 'totp_authenticated_at' => 'datetime', @@ -360,7 +354,7 @@ public function isLastRootAdmin(): bool public function isRootAdmin(): bool { - return $this->root_admin || $this->hasRole(Role::ROOT_ADMIN); + return $this->hasRole(Role::ROOT_ADMIN); } public function canAccessPanel(Panel $panel): bool diff --git a/database/Factories/UserFactory.php b/database/Factories/UserFactory.php index 510edaef1e..3c6003cc24 100644 --- a/database/Factories/UserFactory.php +++ b/database/Factories/UserFactory.php @@ -33,19 +33,10 @@ public function definition(): array 'name_last' => $this->faker->lastName(), 'password' => $password ?: $password = bcrypt('password'), 'language' => 'en', - 'root_admin' => false, 'use_totp' => false, 'oauth' => [], 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]; } - - /** - * Indicate that the user is an admin. - */ - public function admin(): static - { - return $this->state(['root_admin' => true]); - } } diff --git a/database/migrations/2024_08_01_114538_remove_root_admin_column.php b/database/migrations/2024_08_01_114538_remove_root_admin_column.php new file mode 100644 index 0000000000..dbd901961f --- /dev/null +++ b/database/migrations/2024_08_01_114538_remove_root_admin_column.php @@ -0,0 +1,35 @@ +get(); + foreach ($adminUsers as $adminUser) { + $adminUser->syncRoles(Role::findOrCreate(Role::ROOT_ADMIN)); + } + + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('root_admin'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->tinyInteger('root_admin')->unsigned()->default(0); + }); + } +}; diff --git a/database/schema/mysql-schema.sql b/database/schema/mysql-schema.sql index 79d4548398..e172152820 100644 --- a/database/schema/mysql-schema.sql +++ b/database/schema/mysql-schema.sql @@ -630,7 +630,6 @@ CREATE TABLE `users` ( `password` text COLLATE utf8mb4_unicode_ci NOT NULL, `remember_token` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `language` char(5) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'en', - `root_admin` tinyint unsigned NOT NULL DEFAULT '0', `use_totp` tinyint unsigned NOT NULL, `totp_secret` text COLLATE utf8mb4_unicode_ci, `totp_authenticated_at` timestamp NULL DEFAULT NULL, diff --git a/lang/en/admin/user.php b/lang/en/admin/user.php index 2fb03e00d6..2bf52b77af 100644 --- a/lang/en/admin/user.php +++ b/lang/en/admin/user.php @@ -13,7 +13,6 @@ 'hint' => 'This is the last root administrator!', 'helper_text' => 'You must have at least one root administrator in your system.', ], - 'root_admin' => 'Administrator (Root)', 'language' => [ 'helper_text1' => 'Your language (:state) has not been translated yet!\nBut never fear, you can help fix that by', 'helper_text2' => 'contributing directly here', diff --git a/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php b/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php index e4fe09570b..4a1c68207f 100644 --- a/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php +++ b/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php @@ -6,6 +6,7 @@ use App\Models\User; use PHPUnit\Framework\Assert; use App\Models\ApiKey; +use App\Models\Role; use App\Services\Acl\Api\AdminAcl; use App\Tests\Integration\IntegrationTestCase; use Illuminate\Foundation\Testing\DatabaseTransactions; @@ -67,9 +68,10 @@ protected function createNewDefaultApiKey(User $user, array $permissions = []): */ protected function createApiUser(): User { - return User::factory()->create([ - 'root_admin' => true, - ]); + $user = User::factory()->create(); + $user->syncRoles(Role::findOrCreate(Role::ROOT_ADMIN)); + + return $user; } /** diff --git a/tests/Integration/Api/Application/Users/ExternalUserControllerTest.php b/tests/Integration/Api/Application/Users/ExternalUserControllerTest.php index 26506b7a50..24f3996ebf 100644 --- a/tests/Integration/Api/Application/Users/ExternalUserControllerTest.php +++ b/tests/Integration/Api/Application/Users/ExternalUserControllerTest.php @@ -38,7 +38,7 @@ public function testGetRemoteUser(): void 'first_name' => $user->name_first, 'last_name' => $user->name_last, 'language' => $user->language, - 'root_admin' => (bool) $user->root_admin, + 'root_admin' => (bool) $user->isRootAdmin(), '2fa' => (bool) $user->totp_enabled, 'created_at' => $this->formatTimestamp($user->created_at), 'updated_at' => $this->formatTimestamp($user->updated_at), diff --git a/tests/Integration/Api/Application/Users/UserControllerTest.php b/tests/Integration/Api/Application/Users/UserControllerTest.php index b52c1c2c67..6787ff1f4e 100644 --- a/tests/Integration/Api/Application/Users/UserControllerTest.php +++ b/tests/Integration/Api/Application/Users/UserControllerTest.php @@ -55,7 +55,7 @@ public function testGetUsers(): void 'first_name' => $this->getApiUser()->name_first, 'last_name' => $this->getApiUser()->name_last, 'language' => $this->getApiUser()->language, - 'root_admin' => $this->getApiUser()->root_admin, + 'root_admin' => $this->getApiUser()->isRootAdmin(), '2fa_enabled' => (bool) $this->getApiUser()->totp_enabled, '2fa' => (bool) $this->getApiUser()->totp_enabled, 'created_at' => $this->formatTimestamp($this->getApiUser()->created_at), @@ -73,7 +73,7 @@ public function testGetUsers(): void 'first_name' => $user->name_first, 'last_name' => $user->name_last, 'language' => $user->language, - 'root_admin' => (bool) $user->root_admin, + 'root_admin' => (bool) $user->isRootAdmin(), '2fa_enabled' => (bool) $user->totp_enabled, '2fa' => (bool) $user->totp_enabled, 'created_at' => $this->formatTimestamp($user->created_at), diff --git a/tests/Integration/Api/Client/ClientControllerTest.php b/tests/Integration/Api/Client/ClientControllerTest.php index 29805711f5..73848dd5bf 100644 --- a/tests/Integration/Api/Client/ClientControllerTest.php +++ b/tests/Integration/Api/Client/ClientControllerTest.php @@ -7,6 +7,7 @@ use App\Models\Subuser; use App\Models\Allocation; use App\Models\Permission; +use App\Models\Role; class ClientControllerTest extends ClientApiIntegrationTestCase { @@ -47,7 +48,7 @@ public function testServersAreFilteredUsingNameAndUuidInformation(): void { /** @var \App\Models\User[] $users */ $users = User::factory()->times(2)->create(); - $users[0]->update(['root_admin' => true]); + $users[0]->syncRoles(Role::findOrCreate(Role::ROOT_ADMIN)); /** @var \App\Models\Server[] $servers */ $servers = [ @@ -225,7 +226,7 @@ public function testOnlyAdminLevelServersAreReturned(): void { /** @var \App\Models\User[] $users */ $users = User::factory()->times(4)->create(); - $users[0]->update(['root_admin' => true]); + $users[0]->syncRoles(Role::findOrCreate(Role::ROOT_ADMIN)); $servers = [ $this->createServerModel(['user_id' => $users[0]->id]), @@ -260,7 +261,7 @@ public function testAllServersAreReturnedToAdmin(): void { /** @var \App\Models\User[] $users */ $users = User::factory()->times(4)->create(); - $users[0]->update(['root_admin' => true]); + $users[0]->syncRoles(Role::findOrCreate(Role::ROOT_ADMIN)); $servers = [ $this->createServerModel(['user_id' => $users[0]->id]), diff --git a/tests/Integration/Api/Remote/SftpAuthenticationControllerTest.php b/tests/Integration/Api/Remote/SftpAuthenticationControllerTest.php index 2affbef733..4006416a8b 100644 --- a/tests/Integration/Api/Remote/SftpAuthenticationControllerTest.php +++ b/tests/Integration/Api/Remote/SftpAuthenticationControllerTest.php @@ -7,6 +7,7 @@ use App\Models\User; use App\Models\Server; use App\Models\Permission; +use App\Models\Role; use App\Models\UserSSHKey; use App\Tests\Integration\IntegrationTestCase; @@ -180,7 +181,7 @@ public function testUserPermissionsAreReturnedCorrectly(): void ->assertOk() ->assertJsonPath('permissions', [Permission::ACTION_FILE_READ, Permission::ACTION_FILE_SFTP]); - $user->update(['root_admin' => true]); + $user->syncRoles(Role::findOrCreate(Role::ROOT_ADMIN)); $this->postJson('/api/remote/sftp/auth', $data) ->assertOk() @@ -193,7 +194,7 @@ public function testUserPermissionsAreReturnedCorrectly(): void ->assertOk() ->assertJsonPath('permissions.0', '*'); - $user->update(['root_admin' => false]); + $user->syncRoles(); $this->post('/api/remote/sftp/auth', $data)->assertForbidden(); } diff --git a/tests/Unit/Http/Middleware/AdminAuthenticateTest.php b/tests/Unit/Http/Middleware/AdminAuthenticateTest.php index 87f647819b..fdffcf9545 100644 --- a/tests/Unit/Http/Middleware/AdminAuthenticateTest.php +++ b/tests/Unit/Http/Middleware/AdminAuthenticateTest.php @@ -4,6 +4,7 @@ use App\Models\User; use App\Http\Middleware\AdminAuthenticate; +use App\Models\Role; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; class AdminAuthenticateTest extends MiddlewareTestCase @@ -13,7 +14,8 @@ class AdminAuthenticateTest extends MiddlewareTestCase */ public function testAdminsAreAuthenticated(): void { - $user = User::factory()->make(['root_admin' => 1]); + $user = User::factory()->create(); + $user->syncRoles(Role::findOrCreate(Role::ROOT_ADMIN)); $this->request->shouldReceive('user')->withNoArgs()->twice()->andReturn($user); @@ -39,7 +41,7 @@ public function testExceptionIsThrownIfUserIsNotAnAdmin(): void { $this->expectException(AccessDeniedHttpException::class); - $user = User::factory()->make(['root_admin' => 0]); + $user = User::factory()->create(); $this->request->shouldReceive('user')->withNoArgs()->twice()->andReturn($user); diff --git a/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php b/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php index bfaf4cd75e..a03c9f698a 100644 --- a/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php +++ b/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php @@ -5,6 +5,7 @@ use App\Tests\Unit\Http\Middleware\MiddlewareTestCase; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use App\Http\Middleware\Api\Application\AuthenticateApplicationUser; +use App\Models\Role; class AuthenticateUserTest extends MiddlewareTestCase { @@ -27,7 +28,7 @@ public function testNonAdminUser(): void { $this->expectException(AccessDeniedHttpException::class); - $this->generateRequestUserModel(['root_admin' => false]); + $this->generateRequestUserModel(); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); } @@ -37,7 +38,8 @@ public function testNonAdminUser(): void */ public function testAdminUser(): void { - $this->generateRequestUserModel(['root_admin' => true]); + $user = $this->generateRequestUserModel(); + $user->syncRoles(Role::findOrCreate(Role::ROOT_ADMIN)); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); } From 409c90a270770667751e619b2611b0a606286b24 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Fri, 2 Aug 2024 09:12:14 +0200 Subject: [PATCH 22/47] small refactor --- .../Api/Client/Servers/ActivityLogController.php | 2 +- app/Models/Role.php | 5 +++++ app/Services/Users/UserCreationService.php | 2 +- .../2024_08_01_114538_remove_root_admin_column.php | 2 +- .../Api/Application/ApplicationApiIntegrationTestCase.php | 2 +- tests/Integration/Api/Client/ClientControllerTest.php | 6 +++--- .../Api/Remote/SftpAuthenticationControllerTest.php | 2 +- tests/Unit/Http/Middleware/AdminAuthenticateTest.php | 2 +- .../Middleware/Api/Application/AuthenticateUserTest.php | 2 +- 9 files changed, 15 insertions(+), 10 deletions(-) diff --git a/app/Http/Controllers/Api/Client/Servers/ActivityLogController.php b/app/Http/Controllers/Api/Client/Servers/ActivityLogController.php index 3c315729b1..53272eb70d 100644 --- a/app/Http/Controllers/Api/Client/Servers/ActivityLogController.php +++ b/app/Http/Controllers/Api/Client/Servers/ActivityLogController.php @@ -33,7 +33,7 @@ public function __invoke(ClientApiRequest $request, Server $server): array // We could do this with a query and a lot of joins, but that gets pretty // painful so for now we'll execute a simpler query. $subusers = $server->subusers()->pluck('user_id')->merge([$server->owner_id]); - $rootAdmins = Role::findOrCreate(Role::ROOT_ADMIN)->users()->pluck('id'); + $rootAdmins = Role::getRootAdmin()->users()->pluck('id'); $builder->select('activity_logs.*') ->leftJoin('users', function (JoinClause $join) { diff --git a/app/Models/Role.php b/app/Models/Role.php index c4eef288cf..130229301a 100644 --- a/app/Models/Role.php +++ b/app/Models/Role.php @@ -23,4 +23,9 @@ public function isRootAdmin(): bool { return $this->name === self::ROOT_ADMIN; } + + public static function getRootAdmin(): self + { + return self::findOrCreate(self::ROOT_ADMIN); + } } diff --git a/app/Services/Users/UserCreationService.php b/app/Services/Users/UserCreationService.php index b55f1a4508..3b35eddf46 100644 --- a/app/Services/Users/UserCreationService.php +++ b/app/Services/Users/UserCreationService.php @@ -45,7 +45,7 @@ public function handle(array $data): User ])); if (array_key_exists('root_admin', $data) && $data['root_admin']) { - $user->syncRoles(Role::findOrCreate(Role::ROOT_ADMIN)); + $user->syncRoles(Role::getRootAdmin()); } if (isset($generateResetToken)) { diff --git a/database/migrations/2024_08_01_114538_remove_root_admin_column.php b/database/migrations/2024_08_01_114538_remove_root_admin_column.php index dbd901961f..62329d201d 100644 --- a/database/migrations/2024_08_01_114538_remove_root_admin_column.php +++ b/database/migrations/2024_08_01_114538_remove_root_admin_column.php @@ -15,7 +15,7 @@ public function up(): void { $adminUsers = User::whereRootAdmin(1)->get(); foreach ($adminUsers as $adminUser) { - $adminUser->syncRoles(Role::findOrCreate(Role::ROOT_ADMIN)); + $adminUser->syncRoles(Role::getRootAdmin()); } Schema::table('users', function (Blueprint $table) { diff --git a/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php b/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php index 4a1c68207f..c104a3eae9 100644 --- a/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php +++ b/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php @@ -69,7 +69,7 @@ protected function createNewDefaultApiKey(User $user, array $permissions = []): protected function createApiUser(): User { $user = User::factory()->create(); - $user->syncRoles(Role::findOrCreate(Role::ROOT_ADMIN)); + $user->syncRoles(Role::getRootAdmin()); return $user; } diff --git a/tests/Integration/Api/Client/ClientControllerTest.php b/tests/Integration/Api/Client/ClientControllerTest.php index 73848dd5bf..e4059f6947 100644 --- a/tests/Integration/Api/Client/ClientControllerTest.php +++ b/tests/Integration/Api/Client/ClientControllerTest.php @@ -48,7 +48,7 @@ public function testServersAreFilteredUsingNameAndUuidInformation(): void { /** @var \App\Models\User[] $users */ $users = User::factory()->times(2)->create(); - $users[0]->syncRoles(Role::findOrCreate(Role::ROOT_ADMIN)); + $users[0]->syncRoles(Role::getRootAdmin()); /** @var \App\Models\Server[] $servers */ $servers = [ @@ -226,7 +226,7 @@ public function testOnlyAdminLevelServersAreReturned(): void { /** @var \App\Models\User[] $users */ $users = User::factory()->times(4)->create(); - $users[0]->syncRoles(Role::findOrCreate(Role::ROOT_ADMIN)); + $users[0]->syncRoles(Role::getRootAdmin()); $servers = [ $this->createServerModel(['user_id' => $users[0]->id]), @@ -261,7 +261,7 @@ public function testAllServersAreReturnedToAdmin(): void { /** @var \App\Models\User[] $users */ $users = User::factory()->times(4)->create(); - $users[0]->syncRoles(Role::findOrCreate(Role::ROOT_ADMIN)); + $users[0]->syncRoles(Role::getRootAdmin()); $servers = [ $this->createServerModel(['user_id' => $users[0]->id]), diff --git a/tests/Integration/Api/Remote/SftpAuthenticationControllerTest.php b/tests/Integration/Api/Remote/SftpAuthenticationControllerTest.php index 4006416a8b..031eedce70 100644 --- a/tests/Integration/Api/Remote/SftpAuthenticationControllerTest.php +++ b/tests/Integration/Api/Remote/SftpAuthenticationControllerTest.php @@ -181,7 +181,7 @@ public function testUserPermissionsAreReturnedCorrectly(): void ->assertOk() ->assertJsonPath('permissions', [Permission::ACTION_FILE_READ, Permission::ACTION_FILE_SFTP]); - $user->syncRoles(Role::findOrCreate(Role::ROOT_ADMIN)); + $user->syncRoles(Role::getRootAdmin()); $this->postJson('/api/remote/sftp/auth', $data) ->assertOk() diff --git a/tests/Unit/Http/Middleware/AdminAuthenticateTest.php b/tests/Unit/Http/Middleware/AdminAuthenticateTest.php index fdffcf9545..463784155d 100644 --- a/tests/Unit/Http/Middleware/AdminAuthenticateTest.php +++ b/tests/Unit/Http/Middleware/AdminAuthenticateTest.php @@ -15,7 +15,7 @@ class AdminAuthenticateTest extends MiddlewareTestCase public function testAdminsAreAuthenticated(): void { $user = User::factory()->create(); - $user->syncRoles(Role::findOrCreate(Role::ROOT_ADMIN)); + $user->syncRoles(Role::getRootAdmin()); $this->request->shouldReceive('user')->withNoArgs()->twice()->andReturn($user); diff --git a/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php b/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php index a03c9f698a..594d852d53 100644 --- a/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php +++ b/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php @@ -39,7 +39,7 @@ public function testNonAdminUser(): void public function testAdminUser(): void { $user = $this->generateRequestUserModel(); - $user->syncRoles(Role::findOrCreate(Role::ROOT_ADMIN)); + $user->syncRoles(Role::getRootAdmin()); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); } From 1e1e02c8a0259e948a5f52af49ad70344012fa28 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Fri, 2 Aug 2024 11:05:13 +0200 Subject: [PATCH 23/47] fix tests --- app/Models/Role.php | 5 ++++- app/Services/Users/UserCreationService.php | 5 ++++- database/schema/mysql-schema.sql | 1 + tests/Traits/Http/RequestMockHelpers.php | 9 +++++---- tests/Unit/Http/Middleware/AdminAuthenticateTest.php | 11 +++++++---- .../Api/Application/AuthenticateUserTest.php | 6 ++---- 6 files changed, 23 insertions(+), 14 deletions(-) diff --git a/app/Models/Role.php b/app/Models/Role.php index 130229301a..9fcfaba5ac 100644 --- a/app/Models/Role.php +++ b/app/Models/Role.php @@ -26,6 +26,9 @@ public function isRootAdmin(): bool public static function getRootAdmin(): self { - return self::findOrCreate(self::ROOT_ADMIN); + /** @var self $role */ + $role = self::findOrCreate(self::ROOT_ADMIN); + + return $role; } } diff --git a/app/Services/Users/UserCreationService.php b/app/Services/Users/UserCreationService.php index 3b35eddf46..958d84442b 100644 --- a/app/Services/Users/UserCreationService.php +++ b/app/Services/Users/UserCreationService.php @@ -40,11 +40,14 @@ public function handle(array $data): User $data['password'] = $this->hasher->make(str_random(30)); } + $isRootAdmin = array_key_exists('root_admin', $data) && $data['root_admin']; + unset($data['root_admin']); + $user = User::query()->forceCreate(array_merge($data, [ 'uuid' => Uuid::uuid4()->toString(), ])); - if (array_key_exists('root_admin', $data) && $data['root_admin']) { + if ($isRootAdmin) { $user->syncRoles(Role::getRootAdmin()); } diff --git a/database/schema/mysql-schema.sql b/database/schema/mysql-schema.sql index e172152820..79d4548398 100644 --- a/database/schema/mysql-schema.sql +++ b/database/schema/mysql-schema.sql @@ -630,6 +630,7 @@ CREATE TABLE `users` ( `password` text COLLATE utf8mb4_unicode_ci NOT NULL, `remember_token` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `language` char(5) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'en', + `root_admin` tinyint unsigned NOT NULL DEFAULT '0', `use_totp` tinyint unsigned NOT NULL, `totp_secret` text COLLATE utf8mb4_unicode_ci, `totp_authenticated_at` timestamp NULL DEFAULT NULL, diff --git a/tests/Traits/Http/RequestMockHelpers.php b/tests/Traits/Http/RequestMockHelpers.php index a0564ea491..22f60e74f6 100644 --- a/tests/Traits/Http/RequestMockHelpers.php +++ b/tests/Traits/Http/RequestMockHelpers.php @@ -35,13 +35,14 @@ public function setRequestUserModel(User $user = null): void /** * Generates a new request user model and also returns the generated model. */ - public function generateRequestUserModel(array $args = []): User + public function generateRequestUserModel(bool $isRootAdmin, array $args = []): void { - /** @var \App\Models\User $user */ $user = User::factory()->make($args); - $this->setRequestUserModel($user); + $user = m::mock($user)->makePartial(); + $user->shouldReceive('isRootAdmin')->andReturn($isRootAdmin); - return $user; + /** @var User|Mock $user */ + $this->setRequestUserModel($user); } /** diff --git a/tests/Unit/Http/Middleware/AdminAuthenticateTest.php b/tests/Unit/Http/Middleware/AdminAuthenticateTest.php index 463784155d..40b15775e8 100644 --- a/tests/Unit/Http/Middleware/AdminAuthenticateTest.php +++ b/tests/Unit/Http/Middleware/AdminAuthenticateTest.php @@ -4,7 +4,7 @@ use App\Models\User; use App\Http\Middleware\AdminAuthenticate; -use App\Models\Role; +use Mockery; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; class AdminAuthenticateTest extends MiddlewareTestCase @@ -14,8 +14,9 @@ class AdminAuthenticateTest extends MiddlewareTestCase */ public function testAdminsAreAuthenticated(): void { - $user = User::factory()->create(); - $user->syncRoles(Role::getRootAdmin()); + $user = User::factory()->make(); + $user = Mockery::mock($user)->makePartial(); + $user->shouldReceive('isRootAdmin')->andReturnTrue(); $this->request->shouldReceive('user')->withNoArgs()->twice()->andReturn($user); @@ -41,7 +42,9 @@ public function testExceptionIsThrownIfUserIsNotAnAdmin(): void { $this->expectException(AccessDeniedHttpException::class); - $user = User::factory()->create(); + $user = User::factory()->make(); + $user = Mockery::mock($user)->makePartial(); + $user->shouldReceive('isRootAdmin')->andReturnFalse(); $this->request->shouldReceive('user')->withNoArgs()->twice()->andReturn($user); diff --git a/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php b/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php index 594d852d53..6b0bdd2775 100644 --- a/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php +++ b/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php @@ -5,7 +5,6 @@ use App\Tests\Unit\Http\Middleware\MiddlewareTestCase; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use App\Http\Middleware\Api\Application\AuthenticateApplicationUser; -use App\Models\Role; class AuthenticateUserTest extends MiddlewareTestCase { @@ -28,7 +27,7 @@ public function testNonAdminUser(): void { $this->expectException(AccessDeniedHttpException::class); - $this->generateRequestUserModel(); + $this->generateRequestUserModel(false); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); } @@ -38,8 +37,7 @@ public function testNonAdminUser(): void */ public function testAdminUser(): void { - $user = $this->generateRequestUserModel(); - $user->syncRoles(Role::getRootAdmin()); + $this->generateRequestUserModel(true); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); } From 8d6e3585dcbdb82473d21a56f392593af6e93392 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Fri, 2 Aug 2024 11:32:32 +0200 Subject: [PATCH 24/47] forgot can checks here --- .../Resources/EggResource/Pages/EditEgg.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/Filament/Resources/EggResource/Pages/EditEgg.php b/app/Filament/Resources/EggResource/Pages/EditEgg.php index b4af8bc0ae..fbcb33d26d 100644 --- a/app/Filament/Resources/EggResource/Pages/EditEgg.php +++ b/app/Filament/Resources/EggResource/Pages/EditEgg.php @@ -228,7 +228,13 @@ protected function getHeaderActions(): array ->color('primary') ->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) { echo $service->handle($egg->id); - }, 'egg-' . $egg->getKebabName() . '.json')), + }, 'egg-' . $egg->getKebabName() . '.json')) + ->visible(function () { + /** @var User $user */ + $user = auth()->user(); + + return $user->can('export egg'); + }), Actions\Action::make('importEgg') ->label('Import') @@ -300,6 +306,12 @@ protected function getHeaderActions(): array ->title('Import Success') ->success() ->send(); + }) + ->visible(function () { + /** @var User $user */ + $user = auth()->user(); + + return $user->can('import egg'); }), $this->getSaveFormAction()->formId('form'), From 507048e11231707c394acb50638c1be19e69cd9f Mon Sep 17 00:00:00 2001 From: Boy132 Date: Fri, 2 Aug 2024 11:34:55 +0200 Subject: [PATCH 25/47] forgot use --- app/Filament/Resources/EggResource/Pages/EditEgg.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Filament/Resources/EggResource/Pages/EditEgg.php b/app/Filament/Resources/EggResource/Pages/EditEgg.php index fbcb33d26d..61987a6478 100644 --- a/app/Filament/Resources/EggResource/Pages/EditEgg.php +++ b/app/Filament/Resources/EggResource/Pages/EditEgg.php @@ -25,6 +25,7 @@ use Filament\Notifications\Notification; use Filament\Resources\Pages\EditRecord; use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor; +use App\Models\User; use App\Services\Eggs\Sharing\EggExporterService; use Filament\Forms; use Filament\Forms\Form; From 7af4db012953c3f17fb73cffcc49d312ec5f0c53 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 19 Aug 2024 22:53:22 +0200 Subject: [PATCH 26/47] disable editing own roles & disable assigning root admin --- app/Filament/Resources/UserResource/Pages/EditUser.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/Filament/Resources/UserResource/Pages/EditUser.php b/app/Filament/Resources/UserResource/Pages/EditUser.php index 529cc255d5..822fef0b7b 100644 --- a/app/Filament/Resources/UserResource/Pages/EditUser.php +++ b/app/Filament/Resources/UserResource/Pages/EditUser.php @@ -3,13 +3,14 @@ namespace App\Filament\Resources\UserResource\Pages; use App\Filament\Resources\UserResource; +use App\Models\Role; +use App\Models\User; use App\Services\Exceptions\FilamentExceptionHandler; use Filament\Actions; -use Filament\Resources\Pages\EditRecord; -use App\Models\User; use Filament\Forms; use Filament\Forms\Components\Section; use Filament\Forms\Form; +use Filament\Resources\Pages\EditRecord; use Illuminate\Support\Facades\Hash; class EditUser extends EditRecord @@ -34,6 +35,8 @@ public function form(Form $form): Form ->options(fn (User $user) => $user->getAvailableLanguages()), Forms\Components\Hidden::make('skipValidation')->default(true), Forms\Components\CheckboxList::make('roles') + ->disabled(fn (User $user) => $user->id === auth()->user()->id) + ->disableOptionWhen(fn (string $value): bool => $value === Role::ROOT_ADMIN) ->relationship('roles', 'name') ->label('Admin Roles') ->columnSpanFull() From 38a3f43a9c815d074905d941a0dcaf068a58802d Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 19 Aug 2024 22:56:46 +0200 Subject: [PATCH 27/47] don't allow to rename root admin role --- app/Filament/Resources/RoleResource.php | 3 ++- app/Filament/Resources/RoleResource/Pages/EditRole.php | 4 ++-- app/Filament/Resources/RoleResource/Pages/ListRoles.php | 8 ++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/Filament/Resources/RoleResource.php b/app/Filament/Resources/RoleResource.php index 1b1ccac1e6..16513d40e7 100644 --- a/app/Filament/Resources/RoleResource.php +++ b/app/Filament/Resources/RoleResource.php @@ -99,7 +99,8 @@ public static function form(Form $form): Form ->schema([ TextInput::make('name') ->label('Role Name') - ->required(), + ->required() + ->disabled(fn (Get $get) => $get('name') === Role::ROOT_ADMIN), TextInput::make('guard_name') ->label('Guard Name') ->default(Filament::getCurrentPanel()?->getAuthGuard() ?? '') diff --git a/app/Filament/Resources/RoleResource/Pages/EditRole.php b/app/Filament/Resources/RoleResource/Pages/EditRole.php index 2ccab7ab92..c62e126913 100644 --- a/app/Filament/Resources/RoleResource/Pages/EditRole.php +++ b/app/Filament/Resources/RoleResource/Pages/EditRole.php @@ -4,7 +4,7 @@ use App\Filament\Resources\RoleResource; use App\Models\Role; -use Filament\Actions; +use Filament\Actions\DeleteAction; use Filament\Resources\Pages\EditRecord; use Illuminate\Support\Arr; use Illuminate\Support\Collection; @@ -48,7 +48,7 @@ protected function afterSave(): void protected function getHeaderActions(): array { return [ - Actions\DeleteAction::make() + DeleteAction::make() ->disabled(fn (Role $role) => $role->isRootAdmin() || $role->users_count >= 1) ->label(fn (Role $role) => $role->isRootAdmin() ? 'Can\'t delete Root Admin' : ($role->users_count >= 1 ? 'In Use' : 'Delete')), ]; diff --git a/app/Filament/Resources/RoleResource/Pages/ListRoles.php b/app/Filament/Resources/RoleResource/Pages/ListRoles.php index 9ab76ac093..bc81fd539b 100644 --- a/app/Filament/Resources/RoleResource/Pages/ListRoles.php +++ b/app/Filament/Resources/RoleResource/Pages/ListRoles.php @@ -4,10 +4,10 @@ use App\Filament\Resources\RoleResource; use App\Models\Role; -use Filament\Actions; +use Filament\Actions\CreateAction; use Filament\Resources\Pages\ListRecords; use Filament\Tables\Actions\BulkActionGroup; -use Filament\Tables\Actions\CreateAction; +use Filament\Tables\Actions\CreateAction as CreateActionTable; use Filament\Tables\Actions\DeleteBulkAction; use Filament\Tables\Actions\EditAction; use Filament\Tables\Columns\TextColumn; @@ -51,7 +51,7 @@ public function table(Table $table): Table ->emptyStateDescription('') ->emptyStateHeading('No Roles') ->emptyStateActions([ - CreateAction::make('create') + CreateActionTable::make('create') ->label('Create Role') ->button(), ]); @@ -60,7 +60,7 @@ public function table(Table $table): Table protected function getHeaderActions(): array { return [ - Actions\CreateAction::make() + CreateAction::make() ->label('Create Role'), ]; } From 6067076ec46db5b2c9175e206ea59683f690cbac Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 19 Aug 2024 23:05:42 +0200 Subject: [PATCH 28/47] remove php bombing exception handler --- .../Resources/UserResource/Pages/EditUser.php | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/app/Filament/Resources/UserResource/Pages/EditUser.php b/app/Filament/Resources/UserResource/Pages/EditUser.php index 822fef0b7b..6c81900c62 100644 --- a/app/Filament/Resources/UserResource/Pages/EditUser.php +++ b/app/Filament/Resources/UserResource/Pages/EditUser.php @@ -5,10 +5,12 @@ use App\Filament\Resources\UserResource; use App\Models\Role; use App\Models\User; -use App\Services\Exceptions\FilamentExceptionHandler; -use Filament\Actions; -use Filament\Forms; +use Filament\Actions\DeleteAction; +use Filament\Forms\Components\CheckboxList; +use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Section; +use Filament\Forms\Components\Select; +use Filament\Forms\Components\TextInput; use Filament\Forms\Form; use Filament\Resources\Pages\EditRecord; use Illuminate\Support\Facades\Hash; @@ -21,20 +23,20 @@ public function form(Form $form): Form return $form ->schema([ Section::make()->schema([ - Forms\Components\TextInput::make('username')->required()->maxLength(255), - Forms\Components\TextInput::make('email')->email()->required()->maxLength(255), - Forms\Components\TextInput::make('password') + TextInput::make('username')->required()->maxLength(255), + TextInput::make('email')->email()->required()->maxLength(255), + TextInput::make('password') ->dehydrateStateUsing(fn (string $state): string => Hash::make($state)) ->dehydrated(fn (?string $state): bool => filled($state)) ->required(fn (string $operation): bool => $operation === 'create') ->password(), - Forms\Components\Select::make('language') + Select::make('language') ->required() ->hidden() ->default('en') ->options(fn (User $user) => $user->getAvailableLanguages()), - Forms\Components\Hidden::make('skipValidation')->default(true), - Forms\Components\CheckboxList::make('roles') + Hidden::make('skipValidation')->default(true), + CheckboxList::make('roles') ->disabled(fn (User $user) => $user->id === auth()->user()->id) ->disableOptionWhen(fn (string $value): bool => $value === Role::ROOT_ADMIN) ->relationship('roles', 'name') @@ -47,7 +49,7 @@ public function form(Form $form): Form protected function getHeaderActions(): array { return [ - Actions\DeleteAction::make() + DeleteAction::make() ->label(fn (User $user) => auth()->user()->id === $user->id ? 'Can\'t Delete Yourself' : ($user->servers()->count() > 0 ? 'User Has Servers' : 'Delete')) ->disabled(fn (User $user) => auth()->user()->id === $user->id || $user->servers()->count() > 0), $this->getSaveFormAction()->formId('form'), @@ -58,9 +60,4 @@ protected function getFormActions(): array { return []; } - - public function exception($exception, $stopPropagation): void - { - (new FilamentExceptionHandler())->handle($exception, $stopPropagation); - } } From f3efe7ad188e39659bf33700698177fce03bed80 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 19 Aug 2024 23:18:51 +0200 Subject: [PATCH 29/47] fix role assignment when creating a user --- .../UserResource/Pages/ListUsers.php | 71 ++++++++++++------- 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/app/Filament/Resources/UserResource/Pages/ListUsers.php b/app/Filament/Resources/UserResource/Pages/ListUsers.php index 264544035b..3778fb6279 100644 --- a/app/Filament/Resources/UserResource/Pages/ListUsers.php +++ b/app/Filament/Resources/UserResource/Pages/ListUsers.php @@ -3,14 +3,22 @@ namespace App\Filament\Resources\UserResource\Pages; use App\Filament\Resources\UserResource; +use App\Models\Role; use App\Models\User; use App\Services\Users\UserCreationService; -use Filament\Actions; +use Filament\Actions\CreateAction; +use Filament\Forms\Components\CheckboxList; +use Filament\Forms\Components\Grid; +use Filament\Forms\Components\TextInput; use Filament\Notifications\Notification; use Filament\Resources\Pages\ListRecords; +use Filament\Tables\Actions\BulkActionGroup; +use Filament\Tables\Actions\DeleteBulkAction; +use Filament\Tables\Actions\EditAction; +use Filament\Tables\Columns\IconColumn; +use Filament\Tables\Columns\ImageColumn; +use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; -use Filament\Tables; -use Filament\Forms; class ListUsers extends ListRecords { @@ -21,81 +29,80 @@ public function table(Table $table): Table return $table ->searchable(false) ->columns([ - Tables\Columns\ImageColumn::make('picture') + ImageColumn::make('picture') ->visibleFrom('lg') ->label('') ->extraImgAttributes(['class' => 'rounded-full']) ->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))), - Tables\Columns\TextColumn::make('external_id') + TextColumn::make('external_id') ->searchable() ->hidden(), - Tables\Columns\TextColumn::make('uuid') + TextColumn::make('uuid') ->label('UUID') ->hidden() ->searchable(), - Tables\Columns\TextColumn::make('username') + TextColumn::make('username') ->searchable(), - Tables\Columns\TextColumn::make('email') + TextColumn::make('email') ->searchable() ->icon('tabler-mail'), - Tables\Columns\IconColumn::make('use_totp') + IconColumn::make('use_totp') ->label('2FA') ->visibleFrom('lg') ->icon(fn (User $user) => $user->use_totp ? 'tabler-lock' : 'tabler-lock-open-off') ->boolean()->sortable(), - Tables\Columns\TextColumn::make('roles_count') + TextColumn::make('roles_count') ->counts('roles') ->icon('tabler-users-group') ->label('Roles') ->formatStateUsing(fn (User $user, $state) => $state . ($user->isRootAdmin() ? ' (Root Admin)' : '')), - Tables\Columns\TextColumn::make('servers_count') + TextColumn::make('servers_count') ->counts('servers') ->icon('tabler-server') ->label('Servers'), - Tables\Columns\TextColumn::make('subusers_count') + TextColumn::make('subusers_count') ->visibleFrom('sm') ->label('Subusers') ->counts('subusers') ->icon('tabler-users'), // ->formatStateUsing(fn (string $state, $record): string => (string) ($record->servers_count + $record->subusers_count)) ]) - ->filters([ - // - ]) ->actions([ - Tables\Actions\EditAction::make(), + EditAction::make(), ]) ->checkIfRecordIsSelectableUsing(fn (User $user) => auth()->user()->id !== $user->id && !$user->servers_count) ->bulkActions([ - Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), + BulkActionGroup::make([ + DeleteBulkAction::make(), ]), ]); } protected function getHeaderActions(): array { return [ - Actions\CreateAction::make('create') + CreateAction::make('create') ->label('Create User') ->createAnother(false) ->form([ - Forms\Components\Grid::make() + Grid::make() ->schema([ - Forms\Components\TextInput::make('username') + TextInput::make('username') ->alphaNum() ->required() ->maxLength(255), - Forms\Components\TextInput::make('email') + TextInput::make('email') ->email() ->required() ->unique() ->maxLength(255), - Forms\Components\TextInput::make('password') + TextInput::make('password') ->hintIcon('tabler-question-mark') ->hintIconTooltip('Providing a user password is optional. New user email will prompt users to create a password the first time they login.') ->password(), - Forms\Components\CheckboxList::make('roles') + CheckboxList::make('roles') + ->disableOptionWhen(fn (string $value): bool => $value === Role::ROOT_ADMIN) ->relationship('roles', 'name') + ->dehydrated() ->label('Admin Roles') ->columnSpanFull() ->bulkToggleable(false), @@ -103,8 +110,20 @@ protected function getHeaderActions(): array ]) ->successRedirectUrl(route('filament.admin.resources.users.index')) ->action(function (array $data) { - resolve(UserCreationService::class)->handle($data); - Notification::make()->title('User Created!')->success()->send(); + $roles = $data['roles']; + $roles = collect($roles)->map(fn ($role) => Role::findById($role)); + unset($data['roles']); + + /** @var UserCreationService $creationService */ + $creationService = resolve(UserCreationService::class); + $user = $creationService->handle($data); + + $user->syncRoles($roles); + + Notification::make() + ->title('User Created!') + ->success() + ->send(); return redirect()->route('filament.admin.resources.users.index'); }), From f3a1ae822fe9244b3acf0c8241aaceae0d256a22 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 19 Aug 2024 23:21:13 +0200 Subject: [PATCH 30/47] fix disableOptionWhen --- app/Filament/Resources/UserResource/Pages/EditUser.php | 2 +- app/Filament/Resources/UserResource/Pages/ListUsers.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Filament/Resources/UserResource/Pages/EditUser.php b/app/Filament/Resources/UserResource/Pages/EditUser.php index 6c81900c62..777f20d730 100644 --- a/app/Filament/Resources/UserResource/Pages/EditUser.php +++ b/app/Filament/Resources/UserResource/Pages/EditUser.php @@ -38,7 +38,7 @@ public function form(Form $form): Form Hidden::make('skipValidation')->default(true), CheckboxList::make('roles') ->disabled(fn (User $user) => $user->id === auth()->user()->id) - ->disableOptionWhen(fn (string $value): bool => $value === Role::ROOT_ADMIN) + ->disableOptionWhen(fn (string $value): bool => $value == Role::getRootAdmin()->id) ->relationship('roles', 'name') ->label('Admin Roles') ->columnSpanFull() diff --git a/app/Filament/Resources/UserResource/Pages/ListUsers.php b/app/Filament/Resources/UserResource/Pages/ListUsers.php index 3778fb6279..6a2df2c981 100644 --- a/app/Filament/Resources/UserResource/Pages/ListUsers.php +++ b/app/Filament/Resources/UserResource/Pages/ListUsers.php @@ -100,7 +100,7 @@ protected function getHeaderActions(): array ->hintIconTooltip('Providing a user password is optional. New user email will prompt users to create a password the first time they login.') ->password(), CheckboxList::make('roles') - ->disableOptionWhen(fn (string $value): bool => $value === Role::ROOT_ADMIN) + ->disableOptionWhen(fn (string $value): bool => $value == Role::getRootAdmin()->id) ->relationship('roles', 'name') ->dehydrated() ->label('Admin Roles') From 8de90f2649ecca1f81eb356bd9491e98d245364f Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 20 Aug 2024 09:48:55 +0200 Subject: [PATCH 31/47] fix missing `root_admin` attribute on react frontend --- app/Models/User.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/Models/User.php b/app/Models/User.php index 7c32765f94..1f94342d2e 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -222,7 +222,9 @@ public static function getRules(): array */ public function toReactObject(): array { - return collect($this->toArray())->except(['id', 'external_id'])->toArray(); + return array_merge(collect($this->toArray())->except(['id', 'external_id'])->toArray(), [ + 'root_admin' => $this->isRootAdmin(), + ]); } /** From 46cbaa0250a86693dde40c65bd413ab79ba01d4f Mon Sep 17 00:00:00 2001 From: Boy132 Date: Sun, 1 Sep 2024 20:45:23 +0200 Subject: [PATCH 32/47] add permission check for bulk delete --- .../Pages/ListDatabaseHosts.php | 3 ++- .../DatabaseResource/Pages/ListDatabases.php | 5 ++-- .../Resources/EggResource/Pages/EditEgg.php | 26 +++++-------------- .../Resources/EggResource/Pages/ListEggs.php | 20 ++++---------- .../MountResource/Pages/ListMounts.php | 3 ++- .../NodeResource/Pages/ListNodes.php | 3 ++- .../AllocationsRelationManager.php | 9 ++++--- .../RoleResource/Pages/ListRoles.php | 3 ++- .../UserResource/Pages/ListUsers.php | 3 ++- 9 files changed, 29 insertions(+), 46 deletions(-) diff --git a/app/Filament/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php b/app/Filament/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php index 9307488f88..18c4b2fd71 100644 --- a/app/Filament/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php +++ b/app/Filament/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php @@ -42,7 +42,8 @@ public function table(Table $table): Table ]) ->bulkActions([ BulkActionGroup::make([ - DeleteBulkAction::make(), + DeleteBulkAction::make() + ->authorize(fn () => auth()->user()->can('delete databasehost')), ]), ]); } diff --git a/app/Filament/Resources/DatabaseResource/Pages/ListDatabases.php b/app/Filament/Resources/DatabaseResource/Pages/ListDatabases.php index 45df4a5970..6dfe7762a1 100644 --- a/app/Filament/Resources/DatabaseResource/Pages/ListDatabases.php +++ b/app/Filament/Resources/DatabaseResource/Pages/ListDatabases.php @@ -4,10 +4,10 @@ use App\Filament\Resources\DatabaseResource; use Filament\Actions; -use Filament\Tables\Actions\EditAction; use Filament\Resources\Pages\ListRecords; use Filament\Tables\Actions\BulkActionGroup; use Filament\Tables\Actions\DeleteBulkAction; +use Filament\Tables\Actions\EditAction; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; @@ -48,7 +48,8 @@ public function table(Table $table): Table ]) ->bulkActions([ BulkActionGroup::make([ - DeleteBulkAction::make(), + DeleteBulkAction::make() + ->authorize(fn () => auth()->user()->can('delete database')), ]), ]); } diff --git a/app/Filament/Resources/EggResource/Pages/EditEgg.php b/app/Filament/Resources/EggResource/Pages/EditEgg.php index 49f91c6746..212aec8f2b 100644 --- a/app/Filament/Resources/EggResource/Pages/EditEgg.php +++ b/app/Filament/Resources/EggResource/Pages/EditEgg.php @@ -2,12 +2,15 @@ namespace App\Filament\Resources\EggResource\Pages; +use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor; use App\Filament\Resources\EggResource; use App\Filament\Resources\EggResource\RelationManagers\ServersRelationManager; use App\Models\Egg; +use App\Services\Eggs\Sharing\EggExporterService; use App\Services\Eggs\Sharing\EggImporterService; use Exception; use Filament\Actions; +use Filament\Forms; use Filament\Forms\Components\Checkbox; use Filament\Forms\Components\Fieldset; use Filament\Forms\Components\FileUpload; @@ -22,13 +25,9 @@ use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; +use Filament\Forms\Form; use Filament\Notifications\Notification; use Filament\Resources\Pages\EditRecord; -use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor; -use App\Models\User; -use App\Services\Eggs\Sharing\EggExporterService; -use Filament\Forms; -use Filament\Forms\Form; class EditEgg extends EditRecord { @@ -246,20 +245,13 @@ protected function getHeaderActions(): array Actions\DeleteAction::make('deleteEgg') ->disabled(fn (Egg $egg): bool => $egg->servers()->count() > 0) ->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? 'Delete' : 'In Use'), - Actions\Action::make('exportEgg') ->label('Export') ->color('primary') ->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) { echo $service->handle($egg->id); }, 'egg-' . $egg->getKebabName() . '.json')) - ->visible(function () { - /** @var User $user */ - $user = auth()->user(); - - return $user->can('export egg'); - }), - + ->authorize(fn () => auth()->user()->can('export egg')), Actions\Action::make('importEgg') ->label('Import') ->form([ @@ -329,13 +321,7 @@ protected function getHeaderActions(): array ->success() ->send(); }) - ->visible(function () { - /** @var User $user */ - $user = auth()->user(); - - return $user->can('import egg'); - }), - + ->authorize(fn () => auth()->user()->can('import egg')), $this->getSaveFormAction()->formId('form'), ]; } diff --git a/app/Filament/Resources/EggResource/Pages/ListEggs.php b/app/Filament/Resources/EggResource/Pages/ListEggs.php index 68ddb4804e..1a6a3b6509 100644 --- a/app/Filament/Resources/EggResource/Pages/ListEggs.php +++ b/app/Filament/Resources/EggResource/Pages/ListEggs.php @@ -4,7 +4,6 @@ use App\Filament\Resources\EggResource; use App\Models\Egg; -use App\Models\User; use App\Services\Eggs\Sharing\EggExporterService; use App\Services\Eggs\Sharing\EggImporterService; use Exception; @@ -15,13 +14,13 @@ use Filament\Forms\Components\TextInput; use Filament\Notifications\Notification; use Filament\Resources\Pages\ListRecords; +use Filament\Tables; use Filament\Tables\Actions\BulkActionGroup; use Filament\Tables\Actions\DeleteBulkAction; use Filament\Tables\Actions\EditAction; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; use Livewire\Features\SupportFileUploads\TemporaryUploadedFile; -use Filament\Tables; class ListEggs extends ListRecords { @@ -57,16 +56,12 @@ public function table(Table $table): Table ->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) { echo $service->handle($egg->id); }, 'egg-' . $egg->getKebabName() . '.json')) - ->visible(function () { - /** @var User $user */ - $user = auth()->user(); - - return $user->can('export egg'); - }), + ->authorize(fn () => auth()->user()->can('export egg')), ]) ->bulkActions([ BulkActionGroup::make([ - DeleteBulkAction::make(), + DeleteBulkAction::make() + ->authorize(fn () => auth()->user()->can('delete egg')), ]), ]); } @@ -146,12 +141,7 @@ protected function getHeaderActions(): array ->success() ->send(); }) - ->visible(function () { - /** @var User $user */ - $user = auth()->user(); - - return $user->can('import egg'); - }), + ->authorize(fn () => auth()->user()->can('import egg')), ]; } } diff --git a/app/Filament/Resources/MountResource/Pages/ListMounts.php b/app/Filament/Resources/MountResource/Pages/ListMounts.php index ea3970df5c..d39c5d972d 100644 --- a/app/Filament/Resources/MountResource/Pages/ListMounts.php +++ b/app/Filament/Resources/MountResource/Pages/ListMounts.php @@ -43,7 +43,8 @@ public function table(Table $table): Table ]) ->bulkActions([ BulkActionGroup::make([ - DeleteBulkAction::make(), + DeleteBulkAction::make() + ->authorize(fn () => auth()->user()->can('delete mount')), ]), ]) ->emptyStateIcon('tabler-layers-linked') diff --git a/app/Filament/Resources/NodeResource/Pages/ListNodes.php b/app/Filament/Resources/NodeResource/Pages/ListNodes.php index 6ecb6b690a..62f0254b89 100644 --- a/app/Filament/Resources/NodeResource/Pages/ListNodes.php +++ b/app/Filament/Resources/NodeResource/Pages/ListNodes.php @@ -84,7 +84,8 @@ public function table(Table $table): Table ]) ->bulkActions([ BulkActionGroup::make([ - DeleteBulkAction::make(), + DeleteBulkAction::make() + ->authorize(fn () => auth()->user()->can('delete node')), ]), ]) ->emptyStateIcon('tabler-server-2') diff --git a/app/Filament/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php b/app/Filament/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php index d30df84450..01cc5e50e5 100644 --- a/app/Filament/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php +++ b/app/Filament/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php @@ -7,12 +7,12 @@ use App\Services\Allocations\AssignmentService; use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\TextInput; -use Filament\Forms\Set; -use Filament\Tables\Actions\BulkActionGroup; -use Filament\Tables\Actions\DeleteBulkAction; use Filament\Forms\Form; +use Filament\Forms\Set; use Filament\Resources\RelationManagers\RelationManager; use Filament\Tables; +use Filament\Tables\Actions\BulkActionGroup; +use Filament\Tables\Actions\DeleteBulkAction; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextInputColumn; use Filament\Tables\Table; @@ -152,7 +152,8 @@ public function table(Table $table): Table ]) ->bulkActions([ BulkActionGroup::make([ - DeleteBulkAction::make(), + DeleteBulkAction::make() + ->authorize(fn () => auth()->user()->can('delete allocation')), ]), ]); } diff --git a/app/Filament/Resources/RoleResource/Pages/ListRoles.php b/app/Filament/Resources/RoleResource/Pages/ListRoles.php index bc81fd539b..ac83be15db 100644 --- a/app/Filament/Resources/RoleResource/Pages/ListRoles.php +++ b/app/Filament/Resources/RoleResource/Pages/ListRoles.php @@ -44,7 +44,8 @@ public function table(Table $table): Table ->checkIfRecordIsSelectableUsing(fn (Role $role) => !$role->isRootAdmin() && $role->users_count <= 0) ->bulkActions([ BulkActionGroup::make([ - DeleteBulkAction::make(), + DeleteBulkAction::make() + ->authorize(fn () => auth()->user()->can('delete role')), ]), ]) ->emptyStateIcon('tabler-users-group') diff --git a/app/Filament/Resources/UserResource/Pages/ListUsers.php b/app/Filament/Resources/UserResource/Pages/ListUsers.php index 6a2df2c981..3d9a1af185 100644 --- a/app/Filament/Resources/UserResource/Pages/ListUsers.php +++ b/app/Filament/Resources/UserResource/Pages/ListUsers.php @@ -73,7 +73,8 @@ public function table(Table $table): Table ->checkIfRecordIsSelectableUsing(fn (User $user) => auth()->user()->id !== $user->id && !$user->servers_count) ->bulkActions([ BulkActionGroup::make([ - DeleteBulkAction::make(), + DeleteBulkAction::make() + ->authorize(fn () => auth()->user()->can('delete user')), ]), ]); } From 9b80665d8cb31d131c3065547a3d156229a6bef4 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Thu, 5 Sep 2024 08:43:40 +0200 Subject: [PATCH 33/47] rename viewAny to viewList --- app/Filament/Resources/RoleResource.php | 2 +- app/Policies/DefaultPolicies.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Filament/Resources/RoleResource.php b/app/Filament/Resources/RoleResource.php index 16513d40e7..0a97ad25f6 100644 --- a/app/Filament/Resources/RoleResource.php +++ b/app/Filament/Resources/RoleResource.php @@ -43,7 +43,7 @@ public static function getNavigationBadge(): ?string ]; private const PERMISSION_PREFIXES = [ - 'viewAny', + 'viewList', 'view', 'create', 'update', diff --git a/app/Policies/DefaultPolicies.php b/app/Policies/DefaultPolicies.php index 2415bfa704..3035cae198 100644 --- a/app/Policies/DefaultPolicies.php +++ b/app/Policies/DefaultPolicies.php @@ -12,7 +12,7 @@ trait DefaultPolicies */ public function viewAny(User $user): bool { - return $user->can('viewAny ' . $this->modelName); + return $user->can('viewList ' . $this->modelName); } /** From db7db979fb41e22d2b60f8ba364a250a8e766bf5 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Thu, 5 Sep 2024 08:55:14 +0200 Subject: [PATCH 34/47] improve canAccessPanel check --- app/Models/User.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/Models/User.php b/app/Models/User.php index 1f94342d2e..899678bbec 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -361,8 +361,11 @@ public function isRootAdmin(): bool public function canAccessPanel(Panel $panel): bool { - // TODO: better check - return $this->isRootAdmin() || $this->roles()->count() >= 1; + if ($this->isRootAdmin()) { + return true; + } + + return $this->roles()->count() >= 1 && $this->getAllPermissions()->count() >= 1; } public function getFilamentName(): string From 5481b5b23d54704387f47e87848a1966b3987035 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Thu, 5 Sep 2024 09:04:20 +0200 Subject: [PATCH 35/47] fix admin not displaying for non-root admins --- app/Models/User.php | 2 ++ resources/scripts/components/App.tsx | 2 ++ resources/scripts/components/NavigationBar.tsx | 4 ++-- resources/scripts/state/user.ts | 1 + 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/Models/User.php b/app/Models/User.php index 899678bbec..fbd6d96e1c 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -25,6 +25,7 @@ use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; use App\Notifications\SendPasswordReset as ResetPasswordNotification; +use Filament\Facades\Filament; use Spatie\Permission\Traits\HasRoles; /** @@ -224,6 +225,7 @@ public function toReactObject(): array { return array_merge(collect($this->toArray())->except(['id', 'external_id'])->toArray(), [ 'root_admin' => $this->isRootAdmin(), + 'admin' => $this->canAccessPanel(Filament::getPanel('admin')), ]); } diff --git a/resources/scripts/components/App.tsx b/resources/scripts/components/App.tsx index 881ff4c013..8a211b4633 100644 --- a/resources/scripts/components/App.tsx +++ b/resources/scripts/components/App.tsx @@ -27,6 +27,7 @@ interface ExtendedWindow extends Window { email: string; /* eslint-disable camelcase */ root_admin: boolean; + admin: boolean; use_totp: boolean; language: string; updated_at: string; @@ -46,6 +47,7 @@ const App = () => { email: PanelUser.email, language: PanelUser.language, rootAdmin: PanelUser.root_admin, + admin: PanelUser.admin, useTotp: PanelUser.use_totp, createdAt: new Date(PanelUser.created_at), updatedAt: new Date(PanelUser.updated_at), diff --git a/resources/scripts/components/NavigationBar.tsx b/resources/scripts/components/NavigationBar.tsx index f784ddfff6..7372ccbeed 100644 --- a/resources/scripts/components/NavigationBar.tsx +++ b/resources/scripts/components/NavigationBar.tsx @@ -37,7 +37,7 @@ export default () => { const { t } = useTranslation('strings'); const name = useStoreState((state: ApplicationStore) => state.settings.data!.name); - const rootAdmin = useStoreState((state: ApplicationStore) => state.user.data!.rootAdmin); + const isAdmin = useStoreState((state: ApplicationStore) => state.user.data!.admin); const [isLoggingOut, setIsLoggingOut] = useState(false); const onTriggerLogout = () => { @@ -69,7 +69,7 @@ export default () => { - {rootAdmin && ( + {isAdmin && ( ('admin')}> diff --git a/resources/scripts/state/user.ts b/resources/scripts/state/user.ts index 0ec7851377..76ba5b92f9 100644 --- a/resources/scripts/state/user.ts +++ b/resources/scripts/state/user.ts @@ -7,6 +7,7 @@ export interface UserData { email: string; language: string; rootAdmin: boolean; + admin: boolean; useTotp: boolean; createdAt: Date; updatedAt: Date; From f71b0b2ecd21a51b256f72b5c65a28ce492ef61e Mon Sep 17 00:00:00 2001 From: Boy132 Date: Fri, 6 Sep 2024 08:29:37 +0200 Subject: [PATCH 36/47] make sure non root admins can't edit root admins --- app/Models/User.php | 10 ++++++++++ app/Policies/UserPolicy.php | 18 +++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/app/Models/User.php b/app/Models/User.php index fbd6d96e1c..caa3aebca4 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -26,6 +26,7 @@ use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; use App\Notifications\SendPasswordReset as ResetPasswordNotification; use Filament\Facades\Filament; +use Illuminate\Database\Eloquent\Model; use Spatie\Permission\Traits\HasRoles; /** @@ -379,4 +380,13 @@ public function getFilamentAvatarUrl(): ?string { return 'https://gravatar.com/avatar/' . md5(strtolower($this->email)); } + + public function canTarget(Model $user): bool + { + if ($this->isRootAdmin()) { + return true; + } + + return $user instanceof User && !$user->isRootAdmin(); + } } diff --git a/app/Policies/UserPolicy.php b/app/Policies/UserPolicy.php index 442f5147d4..6f975b474d 100644 --- a/app/Policies/UserPolicy.php +++ b/app/Policies/UserPolicy.php @@ -2,9 +2,25 @@ namespace App\Policies; +use App\Models\User; +use Illuminate\Database\Eloquent\Model; + class UserPolicy { - use DefaultPolicies; + use DefaultPolicies { + update as defaultUpdate; + delete as defaultDelete; + } protected string $modelName = 'user'; + + public function update(User $user, Model $model): bool + { + return $user->canTarget($model) && $this->defaultUpdate($user, $model); + } + + public function delete(User $user, Model $model): bool + { + return $user->canTarget($model) && $this->defaultDelete($user, $model); + } } From 08abba4490f021c30171e351a96063e60813b49c Mon Sep 17 00:00:00 2001 From: Boy132 Date: Fri, 6 Sep 2024 08:33:35 +0200 Subject: [PATCH 37/47] fix import --- app/Models/User.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Models/User.php b/app/Models/User.php index caa3aebca4..395f4994fa 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -26,7 +26,7 @@ use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; use App\Notifications\SendPasswordReset as ResetPasswordNotification; use Filament\Facades\Filament; -use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Model as IlluminateModel; use Spatie\Permission\Traits\HasRoles; /** @@ -381,7 +381,7 @@ public function getFilamentAvatarUrl(): ?string return 'https://gravatar.com/avatar/' . md5(strtolower($this->email)); } - public function canTarget(Model $user): bool + public function canTarget(IlluminateModel $user): bool { if ($this->isRootAdmin()) { return true; From d709509c4859b3253f0f045ada06518cc26e2399 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Sat, 7 Sep 2024 00:12:39 +0200 Subject: [PATCH 38/47] fix settings page permission check --- app/Filament/Pages/Settings.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Filament/Pages/Settings.php b/app/Filament/Pages/Settings.php index 3d7c42e64d..682b7451cf 100644 --- a/app/Filament/Pages/Settings.php +++ b/app/Filament/Pages/Settings.php @@ -55,7 +55,7 @@ public static function canAccess(): bool /** @var User $user */ $user = auth()->user(); - return $user->can('view Settings'); + return $user->can('view settings'); } protected function getFormSchema(): array @@ -68,7 +68,7 @@ protected function getFormSchema(): array /** @var User $user */ $user = auth()->user(); - return !$user->can('update Settings'); + return !$user->can('update settings'); }) ->tabs([ Tab::make('general') From 20aab5a380daca87607f9cd36a2596ebc97c780a Mon Sep 17 00:00:00 2001 From: Boy132 Date: Wed, 18 Sep 2024 08:51:05 +0200 Subject: [PATCH 39/47] fix server permissions for non-subusers --- app/Policies/ServerPolicy.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/Policies/ServerPolicy.php b/app/Policies/ServerPolicy.php index 463cee1bac..dbc2ae4028 100644 --- a/app/Policies/ServerPolicy.php +++ b/app/Policies/ServerPolicy.php @@ -21,16 +21,19 @@ public function before(User $user, string $ability, string|Server $server): ?boo return null; } + // Owner has full server permissions if ($server->owner_id === $user->id) { return true; } $subuser = $server->subusers->where('user_id', $user->id)->first(); - if (!$subuser || empty($ability)) { - return false; + // If the user is a subuser check their permissions + if ($subuser) { + return in_array($ability, $subuser->permissions); } - return in_array($ability, $subuser->permissions); + // Return null to let default policies take over + return null; } /** From 22442cb38033e8b28da3b73e6c980504d44858b0 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Wed, 18 Sep 2024 10:02:35 +0200 Subject: [PATCH 40/47] fix settings page permission check v2 --- app/Filament/Pages/Settings.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Filament/Pages/Settings.php b/app/Filament/Pages/Settings.php index 682b7451cf..489e419d88 100644 --- a/app/Filament/Pages/Settings.php +++ b/app/Filament/Pages/Settings.php @@ -166,7 +166,7 @@ private function generalSettings(): array /** @var User $user */ $user = auth()->user(); - return !$user->can('update Settings'); + return !$user->can('update settings'); }) ->action(fn (Set $set) => $set('TRUSTED_PROXIES', [])), FormAction::make('cloudflare') @@ -176,7 +176,7 @@ private function generalSettings(): array /** @var User $user */ $user = auth()->user(); - return !$user->can('update Settings'); + return !$user->can('update settings'); }) ->action(fn (Set $set) => $set('TRUSTED_PROXIES', [ '173.245.48.0/20', @@ -257,7 +257,7 @@ private function mailSettings(): array /** @var User $user */ $user = auth()->user(); - return !$user->can('update Settings'); + return !$user->can('update settings'); }) ->action(function () { try { @@ -598,7 +598,7 @@ protected function getHeaderActions(): array /** @var User $user */ $user = auth()->user(); - return !$user->can('update Settings'); + return !$user->can('update settings'); }) ->keyBindings(['mod+s']), ]; From 4f63cdaee4d858895b6e7453c6e03b74e200906a Mon Sep 17 00:00:00 2001 From: Boy132 Date: Wed, 18 Sep 2024 10:31:27 +0200 Subject: [PATCH 41/47] small cleanup --- app/Filament/Pages/Settings.php | 41 +++++---------------------------- 1 file changed, 6 insertions(+), 35 deletions(-) diff --git a/app/Filament/Pages/Settings.php b/app/Filament/Pages/Settings.php index 489e419d88..4c766a53ee 100644 --- a/app/Filament/Pages/Settings.php +++ b/app/Filament/Pages/Settings.php @@ -3,7 +3,6 @@ namespace App\Filament\Pages; use App\Models\Backup; -use App\Models\User; use App\Notifications\MailTested; use App\Traits\EnvironmentWriterTrait; use Exception; @@ -52,10 +51,7 @@ public function mount(): void public static function canAccess(): bool { - /** @var User $user */ - $user = auth()->user(); - - return $user->can('view settings'); + return auth()->user()->can('view settings'); } protected function getFormSchema(): array @@ -64,12 +60,7 @@ protected function getFormSchema(): array Tabs::make('Tabs') ->columns() ->persistTabInQueryString() - ->disabled(function () { - /** @var User $user */ - $user = auth()->user(); - - return !$user->can('update settings'); - }) + ->disabled(fn () => !auth()->user()->can('update settings')) ->tabs([ Tab::make('general') ->label('General') @@ -162,22 +153,12 @@ private function generalSettings(): array ->color('danger') ->icon('tabler-trash') ->requiresConfirmation() - ->disabled(function () { - /** @var User $user */ - $user = auth()->user(); - - return !$user->can('update settings'); - }) + ->authorize(fn () => auth()->user()->can('update settings')) ->action(fn (Set $set) => $set('TRUSTED_PROXIES', [])), FormAction::make('cloudflare') ->label('Set to Cloudflare IPs') ->icon('tabler-brand-cloudflare') - ->disabled(function () { - /** @var User $user */ - $user = auth()->user(); - - return !$user->can('update settings'); - }) + ->authorize(fn () => auth()->user()->can('update settings')) ->action(fn (Set $set) => $set('TRUSTED_PROXIES', [ '173.245.48.0/20', '103.21.244.0/22', @@ -253,12 +234,7 @@ private function mailSettings(): array ->label('Send Test Mail') ->icon('tabler-send') ->hidden(fn (Get $get) => $get('MAIL_MAILER') === 'log') - ->disabled(function () { - /** @var User $user */ - $user = auth()->user(); - - return !$user->can('update settings'); - }) + ->authorize(fn () => auth()->user()->can('update settings')) ->action(function () { try { MailNotification::route('mail', auth()->user()->email) @@ -594,12 +570,7 @@ protected function getHeaderActions(): array return [ Action::make('save') ->action('save') - ->hidden(function () { - /** @var User $user */ - $user = auth()->user(); - - return !$user->can('update settings'); - }) + ->authorize(fn () => auth()->user()->can('update settings')) ->keyBindings(['mod+s']), ]; From 16dbb534dc781785ff2d29c11f1bd8698002a8f7 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Thu, 19 Sep 2024 09:01:02 +0200 Subject: [PATCH 42/47] cleanup config file --- config/permission.php | 175 ------------------------------------------ 1 file changed, 175 deletions(-) diff --git a/config/permission.php b/config/permission.php index f9309973cc..44c964e278 100644 --- a/config/permission.php +++ b/config/permission.php @@ -4,183 +4,8 @@ 'models' => [ - /* - * When using the "HasPermissions" trait from this package, we need to know which - * Eloquent model should be used to retrieve your permissions. Of course, it - * is often just the "Permission" model but you may use whatever you like. - * - * The model you want to use as a Permission model needs to implement the - * `Spatie\Permission\Contracts\Permission` contract. - */ - - 'permission' => Spatie\Permission\Models\Permission::class, - - /* - * When using the "HasRoles" trait from this package, we need to know which - * Eloquent model should be used to retrieve your roles. Of course, it - * is often just the "Role" model but you may use whatever you like. - * - * The model you want to use as a Role model needs to implement the - * `Spatie\Permission\Contracts\Role` contract. - */ - 'role' => \App\Models\Role::class, ], - 'table_names' => [ - - /* - * When using the "HasRoles" trait from this package, we need to know which - * table should be used to retrieve your roles. We have chosen a basic - * default value but you may easily change it to any table you like. - */ - - 'roles' => 'roles', - - /* - * When using the "HasPermissions" trait from this package, we need to know which - * table should be used to retrieve your permissions. We have chosen a basic - * default value but you may easily change it to any table you like. - */ - - 'permissions' => 'permissions', - - /* - * When using the "HasPermissions" trait from this package, we need to know which - * table should be used to retrieve your models permissions. We have chosen a - * basic default value but you may easily change it to any table you like. - */ - - 'model_has_permissions' => 'model_has_permissions', - - /* - * When using the "HasRoles" trait from this package, we need to know which - * table should be used to retrieve your models roles. We have chosen a - * basic default value but you may easily change it to any table you like. - */ - - 'model_has_roles' => 'model_has_roles', - - /* - * When using the "HasRoles" trait from this package, we need to know which - * table should be used to retrieve your roles permissions. We have chosen a - * basic default value but you may easily change it to any table you like. - */ - - 'role_has_permissions' => 'role_has_permissions', - ], - - 'column_names' => [ - /* - * Change this if you want to name the related pivots other than defaults - */ - 'role_pivot_key' => null, //default 'role_id', - 'permission_pivot_key' => null, //default 'permission_id', - - /* - * Change this if you want to name the related model primary key other than - * `model_id`. - * - * For example, this would be nice if your primary keys are all UUIDs. In - * that case, name this `model_uuid`. - */ - - 'model_morph_key' => 'model_id', - - /* - * Change this if you want to use the teams feature and your related model's - * foreign key is other than `team_id`. - */ - - 'team_foreign_key' => 'team_id', - ], - - /* - * When set to true, the method for checking permissions will be registered on the gate. - * Set this to false if you want to implement custom logic for checking permissions. - */ - - 'register_permission_check_method' => true, - - /* - * When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered - * this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated - * NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it. - */ - 'register_octane_reset_listener' => false, - - /* - * Teams Feature. - * When set to true the package implements teams using the 'team_foreign_key'. - * If you want the migrations to register the 'team_foreign_key', you must - * set this to true before doing the migration. - * If you already did the migration then you must make a new migration to also - * add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions' - * (view the latest version of this package's migration file) - */ - - 'teams' => false, - - /* - * Passport Client Credentials Grant - * When set to true the package will use Passports Client to check permissions - */ - - 'use_passport_client_credentials' => false, - - /* - * When set to true, the required permission names are added to exception messages. - * This could be considered an information leak in some contexts, so the default - * setting is false here for optimum safety. - */ - - 'display_permission_in_exception' => false, - - /* - * When set to true, the required role names are added to exception messages. - * This could be considered an information leak in some contexts, so the default - * setting is false here for optimum safety. - */ - - 'display_role_in_exception' => false, - - /* - * By default wildcard permission lookups are disabled. - * See documentation to understand supported syntax. - */ - - 'enable_wildcard_permission' => false, - - /* - * The class to use for interpreting wildcard permissions. - * If you need to modify delimiters, override the class and specify its name here. - */ - // 'permission.wildcard_permission' => Spatie\Permission\WildcardPermission::class, - - /* Cache-specific settings */ - - 'cache' => [ - - /* - * By default all permissions are cached for 24 hours to speed up performance. - * When permissions or roles are updated the cache is flushed automatically. - */ - - 'expiration_time' => \DateInterval::createFromDateString('24 hours'), - - /* - * The cache key used to store all permissions. - */ - - 'key' => 'spatie.permission.cache', - - /* - * You may optionally indicate a specific cache driver to use for permission and - * role caching using any of the `store` drivers listed in the cache.php config - * file. Using 'default' here means to use the `default` set in cache.php. - */ - - 'store' => 'default', - ], ]; From 6ff4083ae4dcda887b05944cfc9e7ab83049f178 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Thu, 19 Sep 2024 09:10:57 +0200 Subject: [PATCH 43/47] move consts from resouce into enum & model --- app/Enums/RolePermissionModels.php | 16 ++++++++ app/Enums/RolePermissionPrefixes.php | 12 ++++++ app/Filament/Resources/RoleResource.php | 52 +++++-------------------- app/Models/Role.php | 14 +++++++ 4 files changed, 52 insertions(+), 42 deletions(-) create mode 100644 app/Enums/RolePermissionModels.php create mode 100644 app/Enums/RolePermissionPrefixes.php diff --git a/app/Enums/RolePermissionModels.php b/app/Enums/RolePermissionModels.php new file mode 100644 index 0000000000..0a80d5a56c --- /dev/null +++ b/app/Enums/RolePermissionModels.php @@ -0,0 +1,16 @@ + [ - 'import', - 'export', - ], - ]; - - private const SPECIAL_PERMISSIONS = [ - 'Settings' => [ - 'view', - 'update', - ], - ]; - public static function form(Form $form): Form { $permissions = []; - foreach (self::PERMISSION_MODELS as $model) { + foreach (RolePermissionModels::cases() as $model) { $options = []; - foreach (self::PERMISSION_PREFIXES as $prefix) { - $options[$prefix . ' ' . strtolower($model)] = Str::headline($prefix); + foreach (RolePermissionPrefixes::cases() as $prefix) { + $options[$prefix->value . ' ' . strtolower($model->value)] = Str::headline($prefix); } - if (array_key_exists($model, self::MODEL_SPECIFIC_PERMISSIONS)) { - foreach (self::MODEL_SPECIFIC_PERMISSIONS[$model] as $permission) { - $options[$permission . ' ' . strtolower($model)] = Str::headline($permission); + if (array_key_exists($model, Role::MODEL_SPECIFIC_PERMISSIONS)) { + foreach (Role::MODEL_SPECIFIC_PERMISSIONS[$model] as $permission) { + $options[$permission . ' ' . strtolower($model->value)] = Str::headline($permission); } } - $permissions[] = self::makeSection($model, $options); + $permissions[] = self::makeSection($model->value, $options); } - foreach (self::SPECIAL_PERMISSIONS as $model => $prefixes) { + foreach (Role::SPECIAL_PERMISSIONS as $model => $prefixes) { $options = []; foreach ($prefixes as $prefix) { diff --git a/app/Models/Role.php b/app/Models/Role.php index 9fcfaba5ac..1274b2d6c3 100644 --- a/app/Models/Role.php +++ b/app/Models/Role.php @@ -19,6 +19,20 @@ class Role extends BaseRole public const ROOT_ADMIN = 'Root Admin'; + public const MODEL_SPECIFIC_PERMISSIONS = [ + 'egg' => [ + 'import', + 'export', + ], + ]; + + public const SPECIAL_PERMISSIONS = [ + 'settings' => [ + 'view', + 'update', + ], + ]; + public function isRootAdmin(): bool { return $this->name === self::ROOT_ADMIN; From 5a17393160e67697023c2263861c876ac2cb68b2 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Thu, 19 Sep 2024 09:12:48 +0200 Subject: [PATCH 44/47] Update database/migrations/2024_08_01_114538_remove_root_admin_column.php Co-authored-by: Lance Pioch --- .../migrations/2024_08_01_114538_remove_root_admin_column.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/2024_08_01_114538_remove_root_admin_column.php b/database/migrations/2024_08_01_114538_remove_root_admin_column.php index 62329d201d..128063ed16 100644 --- a/database/migrations/2024_08_01_114538_remove_root_admin_column.php +++ b/database/migrations/2024_08_01_114538_remove_root_admin_column.php @@ -13,7 +13,7 @@ */ public function up(): void { - $adminUsers = User::whereRootAdmin(1)->get(); + $adminUsers = User::whereRootAdmin(true)->get(); foreach ($adminUsers as $adminUser) { $adminUser->syncRoles(Role::getRootAdmin()); } From a7b8b346d131fc0d85f070035a8f007d4c0602f8 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Thu, 19 Sep 2024 09:20:13 +0200 Subject: [PATCH 45/47] fix config --- config/permission.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/permission.php b/config/permission.php index 44c964e278..4d4b6e16f6 100644 --- a/config/permission.php +++ b/config/permission.php @@ -4,6 +4,8 @@ 'models' => [ + 'permission' => Spatie\Permission\Models\Permission::class, + 'role' => \App\Models\Role::class, ], From ea66fc76f14d4963baddfdac31e175f4973ab8a4 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Thu, 19 Sep 2024 09:22:52 +0200 Subject: [PATCH 46/47] fix phpstan --- app/Filament/Resources/RoleResource.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Filament/Resources/RoleResource.php b/app/Filament/Resources/RoleResource.php index 4edb480515..efe4ea418c 100644 --- a/app/Filament/Resources/RoleResource.php +++ b/app/Filament/Resources/RoleResource.php @@ -40,10 +40,10 @@ public static function form(Form $form): Form $options = []; foreach (RolePermissionPrefixes::cases() as $prefix) { - $options[$prefix->value . ' ' . strtolower($model->value)] = Str::headline($prefix); + $options[$prefix->value . ' ' . strtolower($model->value)] = Str::headline($prefix->value); } - if (array_key_exists($model, Role::MODEL_SPECIFIC_PERMISSIONS)) { + if (array_key_exists($model->value, Role::MODEL_SPECIFIC_PERMISSIONS)) { foreach (Role::MODEL_SPECIFIC_PERMISSIONS[$model] as $permission) { $options[$permission . ' ' . strtolower($model->value)] = Str::headline($permission); } From 2bc7c2797c6f2e2f21443821f363d30fa9d4e3ed Mon Sep 17 00:00:00 2001 From: Boy132 Date: Thu, 19 Sep 2024 09:25:46 +0200 Subject: [PATCH 47/47] fix phpstan 2.0 --- app/Filament/Resources/RoleResource.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Filament/Resources/RoleResource.php b/app/Filament/Resources/RoleResource.php index efe4ea418c..306c9676da 100644 --- a/app/Filament/Resources/RoleResource.php +++ b/app/Filament/Resources/RoleResource.php @@ -44,7 +44,7 @@ public static function form(Form $form): Form } if (array_key_exists($model->value, Role::MODEL_SPECIFIC_PERMISSIONS)) { - foreach (Role::MODEL_SPECIFIC_PERMISSIONS[$model] as $permission) { + foreach (Role::MODEL_SPECIFIC_PERMISSIONS[$model->value] as $permission) { $options[$permission . ' ' . strtolower($model->value)] = Str::headline($permission); } }