diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
index fd1b1e6f77..c9cb05955b 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.yml
+++ b/.github/ISSUE_TEMPLATE/bug-report.yml
@@ -33,7 +33,6 @@ body:
attributes:
label: Panel Version
description: Version number of your Panel (latest is not a version)
- placeholder: 1.4.0
validations:
required: true
@@ -42,7 +41,6 @@ body:
attributes:
label: Wings Version
description: Version number of your Wings (latest is not a version)
- placeholder: 1.4.2
validations:
required: true
@@ -68,7 +66,7 @@ body:
Run the following command to collect logs on your system.
Wings: `sudo wings diagnostics`
- Panel: `tail -n 150 /var/www/pelican/storage/logs/laravel-$(date +%F).log | nc pelipaste.com 99`
+ Panel: `tail -n 150 /var/www/pelican/storage/logs/laravel-$(date +%F).log | curl -X POST -F 'c=@-' paste.pelistuff.com`
placeholder: "https://pelipaste.com/a1h6z"
render: bash
validations:
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/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 @@
+writeToEnvironment($variables);
+ // Clear config cache
+ Artisan::call('config:clear');
+
// Run migrations
Artisan::call('migrate', [
'--force' => true,
diff --git a/app/Filament/Pages/Installer/Steps/DatabaseStep.php b/app/Filament/Pages/Installer/Steps/DatabaseStep.php
index 94a7952e2f..db9b36cead 100644
--- a/app/Filament/Pages/Installer/Steps/DatabaseStep.php
+++ b/app/Filament/Pages/Installer/Steps/DatabaseStep.php
@@ -7,7 +7,7 @@
use Filament\Forms\Get;
use Filament\Notifications\Notification;
use Filament\Support\Exceptions\Halt;
-use Illuminate\Database\DatabaseManager;
+use Illuminate\Support\Facades\DB;
use PDOException;
class DatabaseStep
@@ -61,9 +61,6 @@ public static function make(): Step
->afterValidation(function (Get $get) {
$driver = $get('env.DB_CONNECTION');
if ($driver !== 'sqlite') {
- /** @var DatabaseManager $database */
- $database = app(DatabaseManager::class);
-
try {
config()->set('database.connections._panel_install_test', [
'driver' => $driver,
@@ -77,7 +74,7 @@ public static function make(): Step
'strict' => true,
]);
- $database->connection('_panel_install_test')->getPdo();
+ DB::connection('_panel_install_test')->getPdo();
} catch (PDOException $exception) {
Notification::make()
->title('Database connection failed')
@@ -85,7 +82,7 @@ public static function make(): Step
->danger()
->send();
- $database->disconnect('_panel_install_test');
+ DB::disconnect('_panel_install_test');
throw new Halt('Database connection failed');
}
diff --git a/app/Filament/Pages/Installer/Steps/RedisStep.php b/app/Filament/Pages/Installer/Steps/RedisStep.php
index 04a4b1b72f..2ab176f40c 100644
--- a/app/Filament/Pages/Installer/Steps/RedisStep.php
+++ b/app/Filament/Pages/Installer/Steps/RedisStep.php
@@ -2,8 +2,13 @@
namespace App\Filament\Pages\Installer\Steps;
+use Exception;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
+use Filament\Forms\Get;
+use Filament\Notifications\Notification;
+use Filament\Support\Exceptions\Halt;
+use Illuminate\Support\Facades\Redis;
class RedisStep
{
@@ -37,6 +42,26 @@ public static function make(): Step
->password()
->revealable()
->default(config('database.redis.default.password')),
- ]);
+ ])
+ ->afterValidation(function (Get $get) {
+ try {
+ config()->set('database.redis._panel_install_test', [
+ 'host' => $get('env.REDIS_HOST'),
+ 'username' => $get('env.REDIS_USERNAME'),
+ 'password' => $get('env.REDIS_PASSWORD'),
+ 'port' => $get('env.REDIS_PORT'),
+ ]);
+
+ Redis::connection('_panel_install_test')->command('ping');
+ } catch (Exception $exception) {
+ Notification::make()
+ ->title('Redis connection failed')
+ ->body($exception->getMessage())
+ ->danger()
+ ->send();
+
+ throw new Halt('Redis connection failed');
+ }
+ });
}
}
diff --git a/app/Filament/Pages/Settings.php b/app/Filament/Pages/Settings.php
index ad9ea96bae..4c766a53ee 100644
--- a/app/Filament/Pages/Settings.php
+++ b/app/Filament/Pages/Settings.php
@@ -49,12 +49,18 @@ public function mount(): void
$this->form->fill();
}
+ public static function canAccess(): bool
+ {
+ return auth()->user()->can('view settings');
+ }
+
protected function getFormSchema(): array
{
return [
Tabs::make('Tabs')
->columns()
->persistTabInQueryString()
+ ->disabled(fn () => !auth()->user()->can('update settings'))
->tabs([
Tab::make('general')
->label('General')
@@ -86,6 +92,7 @@ private function generalSettings(): array
TextInput::make('APP_NAME')
->label('App Name')
->required()
+ ->alphaNum()
->default(env('APP_NAME', 'Pelican')),
TextInput::make('APP_FAVICON')
->label('App Favicon')
@@ -146,10 +153,12 @@ private function generalSettings(): array
->color('danger')
->icon('tabler-trash')
->requiresConfirmation()
+ ->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')
+ ->authorize(fn () => auth()->user()->can('update settings'))
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [
'173.245.48.0/20',
'103.21.244.0/22',
@@ -225,6 +234,7 @@ private function mailSettings(): array
->label('Send Test Mail')
->icon('tabler-send')
->hidden(fn (Get $get) => $get('MAIL_MAILER') === 'log')
+ ->authorize(fn () => auth()->user()->can('update settings'))
->action(function () {
try {
MailNotification::route('mail', auth()->user()->email)
@@ -274,7 +284,6 @@ private function mailSettings(): array
->default(env('MAIL_PORT', config('mail.mailers.smtp.port'))),
TextInput::make('MAIL_USERNAME')
->label('Username')
- ->required()
->default(env('MAIL_USERNAME', config('mail.mailers.smtp.username'))),
TextInput::make('MAIL_PASSWORD')
->label('Password')
@@ -561,12 +570,9 @@ protected function getHeaderActions(): array
return [
Action::make('save')
->action('save')
+ ->authorize(fn () => auth()->user()->can('update settings'))
->keyBindings(['mod+s']),
];
}
- protected function getFormActions(): array
- {
- return [];
- }
}
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 ee7bf5bd62..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,12 +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\Services\Eggs\Sharing\EggExporterService;
-use Filament\Forms;
-use Filament\Forms\Form;
class EditEgg extends EditRecord
{
@@ -245,14 +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')),
-
+ }, 'egg-' . $egg->getKebabName() . '.json'))
+ ->authorize(fn () => auth()->user()->can('export egg')),
Actions\Action::make('importEgg')
->label('Import')
->form([
@@ -321,8 +320,8 @@ protected function getHeaderActions(): array
->title('Import Success')
->success()
->send();
- }),
-
+ })
+ ->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 947d2231a7..1a6a3b6509 100644
--- a/app/Filament/Resources/EggResource/Pages/ListEggs.php
+++ b/app/Filament/Resources/EggResource/Pages/ListEggs.php
@@ -14,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
{
@@ -55,11 +55,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'))
+ ->authorize(fn () => auth()->user()->can('export egg')),
])
->bulkActions([
BulkActionGroup::make([
- DeleteBulkAction::make(),
+ DeleteBulkAction::make()
+ ->authorize(fn () => auth()->user()->can('delete egg')),
]),
]);
}
@@ -138,7 +140,8 @@ protected function getHeaderActions(): array
->title('Import Success')
->success()
->send();
- }),
+ })
+ ->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.php b/app/Filament/Resources/RoleResource.php
new file mode 100644
index 0000000000..306c9676da
--- /dev/null
+++ b/app/Filament/Resources/RoleResource.php
@@ -0,0 +1,146 @@
+value . ' ' . strtolower($model->value)] = Str::headline($prefix->value);
+ }
+
+ if (array_key_exists($model->value, Role::MODEL_SPECIFIC_PERMISSIONS)) {
+ foreach (Role::MODEL_SPECIFIC_PERMISSIONS[$model->value] as $permission) {
+ $options[$permission . ' ' . strtolower($model->value)] = Str::headline($permission);
+ }
+ }
+
+ $permissions[] = self::makeSection($model->value, $options);
+ }
+
+ foreach (Role::SPECIAL_PERMISSIONS as $model => $prefixes) {
+ $options = [];
+
+ foreach ($prefixes as $prefix) {
+ $options[$prefix . ' ' . strtolower($model)] = Str::headline($prefix);
+ }
+
+ $permissions[] = self::makeSection($model, $options);
+ }
+
+ return $form
+ ->columns(1)
+ ->schema([
+ TextInput::make('name')
+ ->label('Role Name')
+ ->required()
+ ->disabled(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
+ TextInput::make('guard_name')
+ ->label('Guard Name')
+ ->default(Filament::getCurrentPanel()?->getAuthGuard() ?? '')
+ ->nullable()
+ ->hidden(),
+ Fieldset::make('Permissions')
+ ->columns(3)
+ ->schema($permissions)
+ ->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') === Role::ROOT_ADMIN),
+ ]);
+ }
+
+ 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 [
+ '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..2f69bba011
--- /dev/null
+++ b/app/Filament/Resources/RoleResource/Pages/CreateRole.php
@@ -0,0 +1,48 @@
+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..c62e126913
--- /dev/null
+++ b/app/Filament/Resources/RoleResource/Pages/EditRole.php
@@ -0,0 +1,56 @@
+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 [
+ 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
new file mode 100644
index 0000000000..ac83be15db
--- /dev/null
+++ b/app/Filament/Resources/RoleResource/Pages/ListRoles.php
@@ -0,0 +1,68 @@
+columns([
+ TextColumn::make('name')
+ ->sortable()
+ ->searchable(),
+ TextColumn::make('guard_name')
+ ->hidden()
+ ->sortable()
+ ->searchable(),
+ TextColumn::make('permissions_count')
+ ->label('Permissions')
+ ->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(),
+ ])
+ ->checkIfRecordIsSelectableUsing(fn (Role $role) => !$role->isRootAdmin() && $role->users_count <= 0)
+ ->bulkActions([
+ BulkActionGroup::make([
+ DeleteBulkAction::make()
+ ->authorize(fn () => auth()->user()->can('delete role')),
+ ]),
+ ])
+ ->emptyStateIcon('tabler-users-group')
+ ->emptyStateDescription('')
+ ->emptyStateHeading('No Roles')
+ ->emptyStateActions([
+ CreateActionTable::make('create')
+ ->label('Create Role')
+ ->button(),
+ ]);
+ }
+
+ protected function getHeaderActions(): array
+ {
+ return [
+ CreateAction::make()
+ ->label('Create Role'),
+ ];
+ }
+}
diff --git a/app/Filament/Resources/ServerResource/Pages/CreateServer.php b/app/Filament/Resources/ServerResource/Pages/CreateServer.php
index 9f2de64b92..8264eb9f55 100644
--- a/app/Filament/Resources/ServerResource/Pages/CreateServer.php
+++ b/app/Filament/Resources/ServerResource/Pages/CreateServer.php
@@ -6,7 +6,6 @@
use App\Models\Allocation;
use App\Models\Egg;
use App\Models\Node;
-use App\Models\ServerVariable;
use App\Models\User;
use App\Services\Allocations\AssignmentService;
use App\Services\Servers\RandomWordService;
@@ -82,7 +81,7 @@ public function form(Form $form): Form
])
->relationship('user', 'username')
->searchable(['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()
@@ -99,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);
@@ -322,9 +306,9 @@ public function form(Form $form): Form
->completedIcon('tabler-check')
->columns([
'default' => 1,
- 'sm' => 2,
- 'md' => 2,
- 'lg' => 4,
+ 'sm' => 4,
+ 'md' => 4,
+ 'lg' => 6,
])
->schema([
Forms\Components\Select::make('egg_id')
@@ -334,7 +318,7 @@ public function form(Form $form): Form
'default' => 1,
'sm' => 2,
'md' => 2,
- 'lg' => 3,
+ 'lg' => 4,
])
->searchable()
->preload()
@@ -391,29 +375,51 @@ public function form(Form $form): Form
->inline()
->required(),
+ Forms\Components\ToggleButtons::make('start_on_completion')
+ ->label('Start Server After Install?')
+ ->default(true)
+ ->required()
+ ->columnSpan([
+ 'default' => 1,
+ 'sm' => 1,
+ 'md' => 1,
+ 'lg' => 1,
+ ])
+ ->options([
+ true => 'Yes',
+ false => 'No',
+ ])
+ ->colors([
+ true => 'primary',
+ false => 'danger',
+ ])
+ ->icons([
+ true => 'tabler-code',
+ false => 'tabler-code-off',
+ ])
+ ->inline(),
+
Forms\Components\Textarea::make('startup')
->hintIcon('tabler-code')
->label('Startup Command')
->hidden(fn (Forms\Get $get) => $get('egg_id') === null)
->required()
->live()
- ->columnSpan([
- 'default' => 1,
- 'sm' => 2,
- 'md' => 2,
- 'lg' => 4,
- ])
->rows(function ($state) {
return str($state)->explode("\n")->reduce(
fn (int $carry, $line) => $carry + floor(strlen($line) / 125),
1
);
- }),
+ })
+ ->columnSpan([
+ 'default' => 1,
+ 'sm' => 4,
+ 'md' => 4,
+ 'lg' => 6,
+ ]),
Forms\Components\Hidden::make('environment')->default([]),
- Forms\Components\Hidden::make('start_on_completion')->default(true),
-
Forms\Components\Section::make('Variables')
->icon('tabler-eggs')
->iconColor('primary')
@@ -444,7 +450,7 @@ public function form(Form $form): Form
$text = Forms\Components\TextInput::make('variable_value')
->hidden($this->shouldHideComponent(...))
- ->required(fn (ServerVariable $serverVariable) => $serverVariable->variable->getRequiredAttribute())
+ ->required(fn (Forms\Get $get) => in_array('required', $get('rules')))
->rules(
fn (Forms\Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) {
$validator = Validator::make(['validatorkey' => $value], [
@@ -471,7 +477,7 @@ public function form(Form $form): Form
->live(onBlur: true)
->hintIcon('tabler-code')
->label(fn (Forms\Get $get) => $get('name'))
- ->hintIconTooltip(fn (Forms\Get $get) => $get('rules'))
+ ->hintIconTooltip(fn (Forms\Get $get) => implode('|', $get('rules')))
->prefix(fn (Forms\Get $get) => '{{' . $get('env_variable') . '}}')
->helperText(fn (Forms\Get $get) => empty($get('description')) ? '—' : $get('description'))
->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
@@ -806,9 +812,11 @@ protected function handleRecordCreation(array $data): Model
return $service->handle($data);
}
- private function shouldHideComponent(ServerVariable $serverVariable, Forms\Components\Component $component): bool
+ private function shouldHideComponent(Forms\Get $get, Forms\Components\Component $component): bool
{
- $containsRuleIn = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'), false);
+ $containsRuleIn = collect($get('rules'))->reduce(
+ fn ($result, $value) => $result === true && !str($value)->startsWith('in:'), true
+ );
if ($component instanceof Forms\Components\Select) {
return $containsRuleIn;
@@ -821,9 +829,11 @@ private function shouldHideComponent(ServerVariable $serverVariable, Forms\Compo
throw new \Exception('Component type not supported: ' . $component::class);
}
- private function getSelectOptionsFromRules(ServerVariable $serverVariable): array
+ private function getSelectOptionsFromRules(Forms\Get $get): array
{
- $inRule = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'));
+ $inRule = collect($get('rules'))->reduce(
+ fn ($result, $value) => str($value)->startsWith('in:') ? $value : $result, ''
+ );
return str($inRule)
->after('in:')
diff --git a/app/Filament/Resources/ServerResource/Pages/EditServer.php b/app/Filament/Resources/ServerResource/Pages/EditServer.php
index aa98cc9192..3765a50522 100644
--- a/app/Filament/Resources/ServerResource/Pages/EditServer.php
+++ b/app/Filament/Resources/ServerResource/Pages/EditServer.php
@@ -29,6 +29,7 @@
use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\Facades\Validator;
use Closure;
+use Illuminate\Database\Eloquent\Builder;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
class EditServer extends EditRecord
@@ -469,7 +470,21 @@ public function form(Form $form): Form
}),
Forms\Components\Repeater::make('server_variables')
- ->relationship('serverVariables')
+ ->relationship('serverVariables', function (Builder $query) {
+ /** @var Server $server */
+ $server = $this->getRecord();
+
+ foreach ($server->variables as $variable) {
+ ServerVariable::query()->firstOrCreate([
+ 'server_id' => $server->id,
+ 'variable_id' => $variable->id,
+ ], [
+ 'variable_value' => $variable->server_value ?? '',
+ ]);
+ }
+
+ return $query;
+ })
->grid()
->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array {
foreach ($data as $key => $value) {
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/EditUser.php b/app/Filament/Resources/UserResource/Pages/EditUser.php
index 6e63864053..777f20d730 100644
--- a/app/Filament/Resources/UserResource/Pages/EditUser.php
+++ b/app/Filament/Resources/UserResource/Pages/EditUser.php
@@ -3,13 +3,16 @@
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
-use App\Services\Exceptions\FilamentExceptionHandler;
-use Filament\Actions;
-use Filament\Resources\Pages\EditRecord;
+use App\Models\Role;
use App\Models\User;
-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;
class EditUser extends EditRecord
@@ -20,54 +23,33 @@ 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\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')
+ Select::make('language')
->required()
->hidden()
->default('en')
->options(fn (User $user) => $user->getAvailableLanguages()),
-
+ Hidden::make('skipValidation')->default(true),
+ CheckboxList::make('roles')
+ ->disabled(fn (User $user) => $user->id === auth()->user()->id)
+ ->disableOptionWhen(fn (string $value): bool => $value == Role::getRootAdmin()->id)
+ ->relationship('roles', 'name')
+ ->label('Admin Roles')
+ ->columnSpanFull()
+ ->bulkToggleable(false),
])->columns(),
]);
}
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'),
@@ -78,9 +60,4 @@ protected function getFormActions(): array
{
return [];
}
-
- public function exception($exception, $stopPropagation): void
- {
- (new FilamentExceptionHandler())->handle($exception, $stopPropagation);
- }
}
diff --git a/app/Filament/Resources/UserResource/Pages/ListUsers.php b/app/Filament/Resources/UserResource/Pages/ListUsers.php
index 42dc17fab2..3d9a1af185 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,101 +29,102 @@ 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('root_admin')
- ->visibleFrom('md')
- ->label('Admin')
- ->boolean()
- ->trueIcon('tabler-star-filled')
- ->falseIcon('tabler-star-off')
- ->sortable(),
- Tables\Columns\IconColumn::make('use_totp')->label('2FA')
+ 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('servers_count')
+ TextColumn::make('roles_count')
+ ->counts('roles')
+ ->icon('tabler-users-group')
+ ->label('Roles')
+ ->formatStateUsing(fn (User $user, $state) => $state . ($user->isRootAdmin() ? ' (Root Admin)' : '')),
+ 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()
+ ->authorize(fn () => auth()->user()->can('delete user')),
]),
]);
}
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\ToggleButtons::make('root_admin')
- ->label('Administrator (Root)')
- ->options([
- false => 'No',
- true => 'Admin',
- ])
- ->colors([
- false => 'primary',
- true => 'danger',
- ])
- ->inline()
- ->required()
- ->default(false),
+ CheckboxList::make('roles')
+ ->disableOptionWhen(fn (string $value): bool => $value == Role::getRootAdmin()->id)
+ ->relationship('roles', 'name')
+ ->dehydrated()
+ ->label('Admin Roles')
+ ->columnSpanFull()
+ ->bulkToggleable(false),
]),
])
->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');
}),
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/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/Client/Servers/ActivityLogController.php b/app/Http/Controllers/Api/Client/Servers/ActivityLogController.php
index efef026813..53272eb70d 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::getRootAdmin()->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/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/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/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 @@
+ '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
new file mode 100644
index 0000000000..48dc3d04e9
--- /dev/null
+++ b/app/Http/Requests/Api/Application/Roles/UpdateRoleRequest.php
@@ -0,0 +1,7 @@
+ 'array',
+ 'roles.*' => 'string',
+ ];
+ }
+}
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/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/Node.php b/app/Models/Node.php
index 473d3a4a2e..a322aa999e 100644
--- a/app/Models/Node.php
+++ b/app/Models/Node.php
@@ -244,21 +244,21 @@ public function allocations(): HasMany
*/
public function isViable(int $memory, int $disk, int $cpu): bool
{
- if ($this->memory_overallocate >= 0) {
+ if ($this->memory > 0 && $this->memory_overallocate >= 0) {
$memoryLimit = $this->memory * (1 + ($this->memory_overallocate / 100));
if ($this->servers_sum_memory + $memory > $memoryLimit) {
return false;
}
}
- if ($this->disk_overallocate >= 0) {
+ if ($this->disk > 0 && $this->disk_overallocate >= 0) {
$diskLimit = $this->disk * (1 + ($this->disk_overallocate / 100));
if ($this->servers_sum_disk + $disk > $diskLimit) {
return false;
}
}
- if ($this->cpu_overallocate >= 0) {
+ if ($this->cpu > 0 && $this->cpu_overallocate >= 0) {
$cpuLimit = $this->cpu * (1 + ($this->cpu_overallocate / 100));
if ($this->servers_sum_cpu + $cpu > $cpuLimit) {
return false;
diff --git a/app/Models/Role.php b/app/Models/Role.php
new file mode 100644
index 0000000000..1274b2d6c3
--- /dev/null
+++ b/app/Models/Role.php
@@ -0,0 +1,48 @@
+ [
+ 'import',
+ 'export',
+ ],
+ ];
+
+ public const SPECIAL_PERMISSIONS = [
+ 'settings' => [
+ 'view',
+ 'update',
+ ],
+ ];
+
+ public function isRootAdmin(): bool
+ {
+ return $this->name === self::ROOT_ADMIN;
+ }
+
+ public static function getRootAdmin(): self
+ {
+ /** @var self $role */
+ $role = self::findOrCreate(self::ROOT_ADMIN);
+
+ return $role;
+ }
+}
diff --git a/app/Models/User.php b/app/Models/User.php
index f2abd802cb..96f7043602 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -28,6 +28,9 @@
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 Illuminate\Database\Eloquent\Model as IlluminateModel;
+use Spatie\Permission\Traits\HasRoles;
/**
* App\Models\User.
@@ -43,7 +46,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
@@ -80,7 +82,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)
@@ -97,6 +98,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
use AvailableLanguages;
use CanResetPassword;
use HasAccessTokens;
+ use HasRoles;
use Notifiable;
public const USER_LEVEL_USER = 0;
@@ -134,7 +136,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
'totp_secret',
'totp_authenticated_at',
'gravatar',
- 'root_admin',
'oauth',
];
@@ -148,7 +149,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
*/
protected $attributes = [
'external_id' => null,
- 'root_admin' => false,
'language' => 'en',
'timezone' => 'UTC',
'use_totp' => false,
@@ -169,7 +169,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',
@@ -180,7 +179,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',
@@ -229,7 +227,10 @@ 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(),
+ 'admin' => $this->canAccessPanel(Filament::getPanel('admin')),
+ ]);
}
/**
@@ -323,7 +324,7 @@ public function subServers(): BelongsToMany
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;
}
@@ -359,11 +360,29 @@ 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(Role::ROOT_ADMIN);
+ }
+
+ public function canAccessPanel(Panel $panel): bool
+ {
+ if ($this->isRootAdmin()) {
+ return true;
+ }
+
+ if ($panel->getId() === 'admin') {
+ return $this->roles()->count() >= 1 && $this->getAllPermissions()->count() >= 1;
+ }
+
+ return true;
+ }
+
public function getFilamentName(): string
{
return $this->name_first ?: $this->username;
@@ -374,13 +393,13 @@ public function getFilamentAvatarUrl(): ?string
return 'https://gravatar.com/avatar/' . md5(strtolower($this->email));
}
- public function canAccessPanel(Panel $panel): bool
+ public function canTarget(IlluminateModel $user): bool
{
- if ($panel->getId() === 'admin') {
- return $this->root_admin;
+ if ($this->isRootAdmin()) {
+ return true;
}
- return true;
+ return $user instanceof User && !$user->isRootAdmin();
}
public function getTenants(Panel $panel): array|Collection
@@ -388,10 +407,10 @@ public function getTenants(Panel $panel): array|Collection
return $this->accessibleServers()->get();
}
- public function canAccessTenant(\Illuminate\Database\Eloquent\Model $tenant): bool
+ public function canAccessTenant(IlluminateModel $tenant): bool
{
if ($tenant instanceof Server) {
- if ($this->root_admin || $tenant->owner_id === $this->id) {
+ if ($this->isRootAdmin() || $tenant->owner_id === $this->id) {
return true;
}
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('viewList ' . $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);
+ }
+}
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;
- }
+ use DefaultPolicies;
- return in_array($permission, $subuser->permissions);
- }
+ protected string $modelName = 'server';
/**
- * Runs before any of the functions are called. Used to determine if user is root admin, if so, ignore permissions.
+ * 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
{
- if ($user->root_admin || $server->owner_id === $user->id) {
+ // For "viewAny" the $server param is the class name
+ if (is_string($server)) {
+ return null;
+ }
+
+ // Owner has full server permissions
+ if ($server->owner_id === $user->id) {
return true;
}
- return $this->checkPermission($user, $server, $ability);
+ $subuser = $server->subusers->where('user_id', $user->id)->first();
+ // If the user is a subuser check their permissions
+ if ($subuser) {
+ return in_array($ability, $subuser->permissions);
+ }
+
+ // Return null to let default policies take over
+ return null;
}
/**
diff --git a/app/Policies/UserPolicy.php b/app/Policies/UserPolicy.php
new file mode 100644
index 0000000000..6f975b474d
--- /dev/null
+++ b/app/Policies/UserPolicy.php
@@ -0,0 +1,26 @@
+canTarget($model) && $this->defaultUpdate($user, $model);
+ }
+
+ public function delete(User $user, Model $model): bool
+ {
+ return $user->canTarget($model) && $this->defaultDelete($user, $model);
+ }
+}
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 19a43bef47..20a6b0dd51 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -6,6 +6,7 @@
use App\Models;
use App\Models\ApiKey;
use App\Models\Node;
+use App\Models\User;
use App\Services\Helpers\SoftwareVersionService;
use Dedoc\Scramble\Scramble;
use Dedoc\Scramble\Support\Generator\OpenApi;
@@ -91,6 +92,10 @@ public function boot(): void
'success' => Color::Green,
'warning' => Color::Amber,
]);
+
+ Gate::before(function (User $user, $ability) {
+ return $user->isRootAdmin() ? true : null;
+ });
}
/**
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/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..958d84442b 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;
@@ -39,10 +40,17 @@ 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 ($isRootAdmin) {
+ $user->syncRoles(Role::getRootAdmin());
+ }
+
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/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 ddec17e82b..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.
@@ -36,7 +40,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),
@@ -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/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),
diff --git a/composer.json b/composer.json
index 1f70a446d3..81bead5074 100644
--- a/composer.json
+++ b/composer.json
@@ -35,6 +35,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 9d8356321a..cc8fd7801b 100644
--- a/composer.lock
+++ b/composer.lock
@@ -7288,6 +7288,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..4d4b6e16f6
--- /dev/null
+++ b/config/permission.php
@@ -0,0 +1,13 @@
+ [
+
+ 'permission' => Spatie\Permission\Models\Permission::class,
+
+ 'role' => \App\Models\Role::class,
+
+ ],
+
+];
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/Seeders/DatabaseSeeder.php b/database/Seeders/DatabaseSeeder.php
index 3a67335f81..2f7f6694e1 100644
--- a/database/Seeders/DatabaseSeeder.php
+++ b/database/Seeders/DatabaseSeeder.php
@@ -2,6 +2,7 @@
namespace Database\Seeders;
+use App\Models\Role;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
@@ -12,5 +13,7 @@ class DatabaseSeeder extends Seeder
public function run()
{
$this->call(EggSeeder::class);
+
+ Role::firstOrCreate(['name' => Role::ROOT_ADMIN]);
}
}
diff --git a/database/Seeders/eggs/minecraft/egg-bungeecord.json b/database/Seeders/eggs/minecraft/egg-bungeecord.json
index 49bc8be50b..e71b8d1ec1 100644
--- a/database/Seeders/eggs/minecraft/egg-bungeecord.json
+++ b/database/Seeders/eggs/minecraft/egg-bungeecord.json
@@ -49,7 +49,7 @@
"alpha_num",
"between:1,6"
],
- "sort": null,
+ "sort": 1,
"field_type": "text"
},
{
@@ -63,8 +63,8 @@
"required",
"regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
],
- "sort": null,
+ "sort": 2,
"field_type": "text"
}
]
-}
\ No newline at end of file
+}
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']);
+ }
+};
diff --git a/database/migrations/2024_07_25_072050_convert_rules_to_array.php b/database/migrations/2024_07_25_072050_convert_rules_to_array.php
index 8e6473a950..6646a66614 100644
--- a/database/migrations/2024_07_25_072050_convert_rules_to_array.php
+++ b/database/migrations/2024_07_25_072050_convert_rules_to_array.php
@@ -12,13 +12,13 @@
*/
public function up(): void
{
- Schema::table('egg_variables', function (Blueprint $table) {
- $table->json('rules')->change();
- });
-
DB::table('egg_variables')->select(['id', 'rules'])->cursor()->each(function ($eggVariable) {
DB::table('egg_variables')->where('id', $eggVariable->id)->update(['rules' => explode('|', $eggVariable->rules)]);
});
+
+ Schema::table('egg_variables', function (Blueprint $table) {
+ $table->json('rules')->change();
+ });
}
/**
@@ -26,7 +26,7 @@ public function up(): void
*/
public function down(): void
{
- Schema::table('api_keys', function (Blueprint $table) {
+ Schema::table('egg_variables', function (Blueprint $table) {
$table->text('rules')->change();
});
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..128063ed16
--- /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::getRootAdmin());
+ }
+
+ 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/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/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 9a5b3556c3..63f49f6f89 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 = () => {
@@ -74,7 +74,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;
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']);
+});
diff --git a/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php b/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php
index e4fe09570b..c104a3eae9 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::getRootAdmin());
+
+ 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..e4059f6947 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::getRootAdmin());
/** @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::getRootAdmin());
$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::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 2affbef733..031eedce70 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::getRootAdmin());
$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/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();
}
/**
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 87f647819b..40b15775e8 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 Mockery;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class AdminAuthenticateTest extends MiddlewareTestCase
@@ -13,7 +14,9 @@ class AdminAuthenticateTest extends MiddlewareTestCase
*/
public function testAdminsAreAuthenticated(): void
{
- $user = User::factory()->make(['root_admin' => 1]);
+ $user = User::factory()->make();
+ $user = Mockery::mock($user)->makePartial();
+ $user->shouldReceive('isRootAdmin')->andReturnTrue();
$this->request->shouldReceive('user')->withNoArgs()->twice()->andReturn($user);
@@ -39,7 +42,9 @@ public function testExceptionIsThrownIfUserIsNotAnAdmin(): void
{
$this->expectException(AccessDeniedHttpException::class);
- $user = User::factory()->make(['root_admin' => 0]);
+ $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 bfaf4cd75e..6b0bdd2775 100644
--- a/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php
+++ b/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php
@@ -27,7 +27,7 @@ public function testNonAdminUser(): void
{
$this->expectException(AccessDeniedHttpException::class);
- $this->generateRequestUserModel(['root_admin' => false]);
+ $this->generateRequestUserModel(false);
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
}
@@ -37,7 +37,7 @@ public function testNonAdminUser(): void
*/
public function testAdminUser(): void
{
- $this->generateRequestUserModel(['root_admin' => true]);
+ $this->generateRequestUserModel(true);
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
}