Admin Roles (#502)
* add spatie/permissions * add policies * add role resource * add root admin role handling * replace some "root_admin" with function * add model specific permissions * make permission selection nicer * fix user creation * fix tests * add back subuser checks in server policy * add custom model for role * assign new users to role if root_admin is set * add api for roles * fix phpstan * add permissions for settings page * remove "restore" and "forceDelete" permissions * add user count to list * prevent deletion if role has users * update user list * fix server policy * remove old `root_admin` column * small refactor * fix tests * forgot can checks here * forgot use * disable editing own roles & disable assigning root admin * don't allow to rename root admin role * remove php bombing exception handler * fix role assignment when creating a user * fix disableOptionWhen * fix missing `root_admin` attribute on react frontend * add permission check for bulk delete * rename viewAny to viewList * improve canAccessPanel check * fix admin not displaying for non-root admins * make sure non root admins can't edit root admins * fix import * fix settings page permission check * fix server permissions for non-subusers * fix settings page permission check v2 * small cleanup * cleanup config file * move consts from resouce into enum & model * Update database/migrations/2024_08_01_114538_remove_root_admin_column.php Co-authored-by: Lance Pioch <lancepioch@gmail.com> * fix config * fix phpstan * fix phpstan 2.0 --------- Co-authored-by: Lance Pioch <lancepioch@gmail.com>
This commit is contained in:
parent
68a0cbbf10
commit
fc643f57f9
@ -52,7 +52,7 @@ class MakeUserCommand extends Command
|
||||
['UUID', $user->uuid],
|
||||
['Email', $user->email],
|
||||
['Username', $user->username],
|
||||
['Admin', $user->root_admin ? 'Yes' : 'No'],
|
||||
['Admin', $user->isRootAdmin() ? 'Yes' : 'No'],
|
||||
]);
|
||||
|
||||
return 0;
|
||||
|
16
app/Enums/RolePermissionModels.php
Normal file
16
app/Enums/RolePermissionModels.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum RolePermissionModels: string
|
||||
{
|
||||
case ApiKey = 'apikey';
|
||||
case DatabaseHost = 'databasehost';
|
||||
case Database = 'database';
|
||||
case Egg = 'egg';
|
||||
case Mount = 'mount';
|
||||
case Node = 'node';
|
||||
case Role = 'role';
|
||||
case Server = 'server';
|
||||
case User = 'user';
|
||||
}
|
12
app/Enums/RolePermissionPrefixes.php
Normal file
12
app/Enums/RolePermissionPrefixes.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum RolePermissionPrefixes: string
|
||||
{
|
||||
case ViewAny = 'viewList';
|
||||
case View = 'view';
|
||||
case Create = 'create';
|
||||
case Update = 'update';
|
||||
case Delete = 'delete';
|
||||
}
|
@ -49,12 +49,18 @@ class Settings extends Page implements HasForms
|
||||
$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')
|
||||
@ -147,10 +153,12 @@ class Settings extends Page implements HasForms
|
||||
->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',
|
||||
@ -226,6 +234,7 @@ class Settings extends Page implements HasForms
|
||||
->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)
|
||||
@ -561,12 +570,9 @@ class Settings extends Page implements HasForms
|
||||
return [
|
||||
Action::make('save')
|
||||
->action('save')
|
||||
->authorize(fn () => auth()->user()->can('update settings'))
|
||||
->keyBindings(['mod+s']),
|
||||
];
|
||||
|
||||
}
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,8 @@ class ListDatabaseHosts extends ListRecords
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete databasehost')),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
@ -4,10 +4,10 @@ namespace App\Filament\Resources\DatabaseResource\Pages;
|
||||
|
||||
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 @@ class ListDatabases extends ListRecords
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete database')),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
@ -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\TagsInput;
|
||||
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 @@ class EditEgg extends EditRecord
|
||||
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 @@ class EditEgg extends EditRecord
|
||||
->title('Import Success')
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
|
||||
})
|
||||
->authorize(fn () => auth()->user()->can('import egg')),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
|
@ -14,13 +14,13 @@ use Filament\Forms\Components\Tabs\Tab;
|
||||
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 @@ class ListEggs extends ListRecords
|
||||
->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 @@ class ListEggs extends ListRecords
|
||||
->title('Import Success')
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
})
|
||||
->authorize(fn () => auth()->user()->can('import egg')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,8 @@ class ListMounts extends ListRecords
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete mount')),
|
||||
]),
|
||||
])
|
||||
->emptyStateIcon('tabler-layers-linked')
|
||||
|
@ -84,7 +84,8 @@ class ListNodes extends ListRecords
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete node')),
|
||||
]),
|
||||
])
|
||||
->emptyStateIcon('tabler-server-2')
|
||||
|
@ -7,12 +7,12 @@ use App\Models\Node;
|
||||
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 @@ class AllocationsRelationManager extends RelationManager
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete allocation')),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
146
app/Filament/Resources/RoleResource.php
Normal file
146
app/Filament/Resources/RoleResource.php
Normal file
@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Enums\RolePermissionModels;
|
||||
use App\Enums\RolePermissionPrefixes;
|
||||
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;
|
||||
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;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Resources\Resource;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class RoleResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Role::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-users-group';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
$permissions = [];
|
||||
|
||||
foreach (RolePermissionModels::cases() as $model) {
|
||||
$options = [];
|
||||
|
||||
foreach (RolePermissionPrefixes::cases() as $prefix) {
|
||||
$options[$prefix->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'),
|
||||
];
|
||||
}
|
||||
}
|
48
app/Filament/Resources/RoleResource/Pages/CreateRole.php
Normal file
48
app/Filament/Resources/RoleResource/Pages/CreateRole.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
|
||||
protected static bool $canCreateAnother = false;
|
||||
|
||||
public Collection $permissions;
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
56
app/Filament/Resources/RoleResource/Pages/EditRole.php
Normal file
56
app/Filament/Resources/RoleResource/Pages/EditRole.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\RoleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\RoleResource;
|
||||
use App\Models\Role;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
/**
|
||||
* @property Role $record
|
||||
*/
|
||||
class EditRole extends EditRecord
|
||||
{
|
||||
protected static string $resource = RoleResource::class;
|
||||
|
||||
public Collection $permissions;
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
$this->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')),
|
||||
];
|
||||
}
|
||||
}
|
68
app/Filament/Resources/RoleResource/Pages/ListRoles.php
Normal file
68
app/Filament/Resources/RoleResource/Pages/ListRoles.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\RoleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\RoleResource;
|
||||
use App\Models\Role;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\CreateAction as CreateActionTable;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ListRoles extends ListRecords
|
||||
{
|
||||
protected static string $resource = RoleResource::class;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->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'),
|
||||
];
|
||||
}
|
||||
}
|
@ -81,7 +81,7 @@ class CreateServer extends CreateRecord
|
||||
])
|
||||
->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()
|
||||
@ -98,21 +98,6 @@ class CreateServer extends CreateRecord
|
||||
->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);
|
||||
|
@ -4,6 +4,7 @@ namespace App\Filament\Resources\ServerResource\Pages;
|
||||
|
||||
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 @@ class ListServers extends ListRecords
|
||||
->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')
|
||||
|
@ -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 @@ class EditUser extends EditRecord
|
||||
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 @@ class EditUser extends EditRecord
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function exception($exception, $stopPropagation): void
|
||||
{
|
||||
(new FilamentExceptionHandler())->handle($exception, $stopPropagation);
|
||||
}
|
||||
}
|
||||
|
@ -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 @@ class ListUsers extends ListRecords
|
||||
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');
|
||||
}),
|
||||
|
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Application\Roles;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Models\Role;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
use App\Transformers\Api\Application\RoleTransformer;
|
||||
use App\Http\Controllers\Api\Application\ApplicationApiController;
|
||||
use App\Http\Requests\Api\Application\Roles\GetRoleRequest;
|
||||
use App\Http\Requests\Api\Application\Roles\StoreRoleRequest;
|
||||
use App\Http\Requests\Api\Application\Roles\DeleteRoleRequest;
|
||||
use App\Http\Requests\Api\Application\Roles\UpdateRoleRequest;
|
||||
|
||||
class RoleController extends ApplicationApiController
|
||||
{
|
||||
/**
|
||||
* Return all the roles currently registered on the Panel.
|
||||
*/
|
||||
public function index(GetRoleRequest $request): array
|
||||
{
|
||||
$roles = QueryBuilder::for(Role::query())
|
||||
->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();
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ use App\Http\Requests\Api\Application\Users\StoreUserRequest;
|
||||
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 @@ class UserController extends ApplicationApiController
|
||||
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.
|
||||
|
@ -48,7 +48,7 @@ class ClientController extends ClientApiController
|
||||
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'
|
||||
|
@ -13,6 +13,7 @@ use Illuminate\Database\Query\JoinClause;
|
||||
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 @@ class ActivityLogController extends ClientApiController
|
||||
// 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);
|
||||
});
|
||||
})
|
||||
|
@ -140,7 +140,7 @@ class SftpAuthenticationController extends Controller
|
||||
*/
|
||||
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)) {
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@ class AuthenticateApplicationUser
|
||||
{
|
||||
/** @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.');
|
||||
}
|
||||
|
||||
|
@ -39,7 +39,7 @@ class AuthenticateServerAccess
|
||||
// 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 @@ class AuthenticateServerAccess
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ class RequireTwoFactorAuthentication
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ abstract class AdminFormRequest extends FormRequest
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) $this->user()->root_admin;
|
||||
return $this->user()->isRootAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -22,7 +22,6 @@ class NewUserFormRequest extends AdminFormRequest
|
||||
'name_last',
|
||||
'password',
|
||||
'language',
|
||||
'root_admin',
|
||||
])->toArray();
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,6 @@ class UserFormRequest extends AdminFormRequest
|
||||
'name_last',
|
||||
'password',
|
||||
'language',
|
||||
'root_admin',
|
||||
])->toArray();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\Roles;
|
||||
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
use App\Http\Requests\Api\Application\ApplicationApiRequest;
|
||||
|
||||
class DeleteRoleRequest extends ApplicationApiRequest
|
||||
{
|
||||
protected ?string $resource = AdminAcl::RESOURCE_ROLES;
|
||||
|
||||
protected int $permission = AdminAcl::WRITE;
|
||||
}
|
13
app/Http/Requests/Api/Application/Roles/GetRoleRequest.php
Normal file
13
app/Http/Requests/Api/Application/Roles/GetRoleRequest.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\Roles;
|
||||
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
use App\Http\Requests\Api\Application\ApplicationApiRequest;
|
||||
|
||||
class GetRoleRequest extends ApplicationApiRequest
|
||||
{
|
||||
protected ?string $resource = AdminAcl::RESOURCE_ROLES;
|
||||
|
||||
protected int $permission = AdminAcl::READ;
|
||||
}
|
21
app/Http/Requests/Api/Application/Roles/StoreRoleRequest.php
Normal file
21
app/Http/Requests/Api/Application/Roles/StoreRoleRequest.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\Roles;
|
||||
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
use App\Http\Requests\Api\Application\ApplicationApiRequest;
|
||||
|
||||
class StoreRoleRequest extends ApplicationApiRequest
|
||||
{
|
||||
protected ?string $resource = AdminAcl::RESOURCE_ROLES;
|
||||
|
||||
protected int $permission = AdminAcl::WRITE;
|
||||
|
||||
public function rules(array $rules = null): array
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string',
|
||||
'guard_name' => 'nullable|string',
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\Roles;
|
||||
|
||||
class UpdateRoleRequest extends StoreRoleRequest
|
||||
{
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\Users;
|
||||
|
||||
class AssignUserRolesRequest extends StoreUserRequest
|
||||
{
|
||||
/**
|
||||
* Return the validation rules for this request.
|
||||
*/
|
||||
public function rules(array $rules = null): array
|
||||
{
|
||||
return [
|
||||
'roles' => 'array',
|
||||
'roles.*' => 'string',
|
||||
];
|
||||
}
|
||||
}
|
@ -26,7 +26,6 @@ class StoreUserRequest extends ApplicationApiRequest
|
||||
'password',
|
||||
'language',
|
||||
'timezone',
|
||||
'root_admin',
|
||||
])->toArray();
|
||||
|
||||
$response['first_name'] = $rules['name_first'];
|
||||
@ -56,7 +55,6 @@ class StoreUserRequest extends ApplicationApiRequest
|
||||
'external_id' => 'Third Party Identifier',
|
||||
'name_first' => 'First Name',
|
||||
'name_last' => 'Last Name',
|
||||
'root_admin' => 'Root Administrator Status',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ abstract class SubuserRequest extends ClientApiRequest
|
||||
$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;
|
||||
}
|
||||
|
||||
|
48
app/Models/Role.php
Normal file
48
app/Models/Role.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
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';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public static function getRootAdmin(): self
|
||||
{
|
||||
/** @var self $role */
|
||||
$role = self::findOrCreate(self::ROOT_ADMIN);
|
||||
|
||||
return $role;
|
||||
}
|
||||
}
|
@ -25,6 +25,9 @@ use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
||||
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.
|
||||
@ -40,7 +43,6 @@ use App\Notifications\SendPasswordReset as ResetPasswordNotification;
|
||||
* @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
|
||||
@ -77,7 +79,6 @@ use App\Notifications\SendPasswordReset as ResetPasswordNotification;
|
||||
* @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)
|
||||
@ -94,6 +95,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
use AvailableLanguages;
|
||||
use CanResetPassword;
|
||||
use HasAccessTokens;
|
||||
use HasRoles;
|
||||
use Notifiable;
|
||||
|
||||
public const USER_LEVEL_USER = 0;
|
||||
@ -131,7 +133,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
'totp_secret',
|
||||
'totp_authenticated_at',
|
||||
'gravatar',
|
||||
'root_admin',
|
||||
'oauth',
|
||||
];
|
||||
|
||||
@ -145,7 +146,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
*/
|
||||
protected $attributes = [
|
||||
'external_id' => null,
|
||||
'root_admin' => false,
|
||||
'language' => 'en',
|
||||
'timezone' => 'UTC',
|
||||
'use_totp' => false,
|
||||
@ -166,7 +166,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',
|
||||
@ -177,7 +176,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',
|
||||
@ -226,7 +224,10 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
*/
|
||||
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')),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -315,7 +316,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -351,14 +352,23 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
|
||||
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
|
||||
{
|
||||
return $this->root_admin;
|
||||
if ($this->isRootAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->roles()->count() >= 1 && $this->getAllPermissions()->count() >= 1;
|
||||
}
|
||||
|
||||
public function getFilamentName(): string
|
||||
@ -370,4 +380,13 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
{
|
||||
return 'https://gravatar.com/avatar/' . md5(strtolower($this->email));
|
||||
}
|
||||
|
||||
public function canTarget(IlluminateModel $user): bool
|
||||
{
|
||||
if ($this->isRootAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $user instanceof User && !$user->isRootAdmin();
|
||||
}
|
||||
}
|
||||
|
10
app/Policies/ApiKeyPolicy.php
Normal file
10
app/Policies/ApiKeyPolicy.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
class ApiKeyPolicy
|
||||
{
|
||||
use DefaultPolicies;
|
||||
|
||||
protected string $modelName = 'apikey';
|
||||
}
|
10
app/Policies/DatabaseHostPolicy.php
Normal file
10
app/Policies/DatabaseHostPolicy.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
class DatabaseHostPolicy
|
||||
{
|
||||
use DefaultPolicies;
|
||||
|
||||
protected string $modelName = 'databasehost';
|
||||
}
|
10
app/Policies/DatabasePolicy.php
Normal file
10
app/Policies/DatabasePolicy.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
class DatabasePolicy
|
||||
{
|
||||
use DefaultPolicies;
|
||||
|
||||
protected string $modelName = 'database';
|
||||
}
|
49
app/Policies/DefaultPolicies.php
Normal file
49
app/Policies/DefaultPolicies.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
trait DefaultPolicies
|
||||
{
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->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);
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
||||
|
10
app/Policies/MountPolicy.php
Normal file
10
app/Policies/MountPolicy.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
class MountPolicy
|
||||
{
|
||||
use DefaultPolicies;
|
||||
|
||||
protected string $modelName = 'mount';
|
||||
}
|
10
app/Policies/NodePolicy.php
Normal file
10
app/Policies/NodePolicy.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
class NodePolicy
|
||||
{
|
||||
use DefaultPolicies;
|
||||
|
||||
protected string $modelName = 'node';
|
||||
}
|
10
app/Policies/RolePolicy.php
Normal file
10
app/Policies/RolePolicy.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
class RolePolicy
|
||||
{
|
||||
use DefaultPolicies;
|
||||
|
||||
protected string $modelName = 'role';
|
||||
}
|
@ -2,34 +2,38 @@
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
|
||||
class ServerPolicy
|
||||
{
|
||||
use DefaultPolicies;
|
||||
|
||||
protected string $modelName = 'server';
|
||||
|
||||
/**
|
||||
* Checks if the user has the given permission on/for the server.
|
||||
* Runs before any of the functions are called. Used to determine if the (sub-)user has permissions.
|
||||
*/
|
||||
protected function checkPermission(User $user, Server $server, string $permission): bool
|
||||
public function before(User $user, string $ability, string|Server $server): ?bool
|
||||
{
|
||||
$subuser = $server->subusers->where('user_id', $user->id)->first();
|
||||
if (!$subuser || empty($permission)) {
|
||||
return false;
|
||||
// For "viewAny" the $server param is the class name
|
||||
if (is_string($server)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
26
app/Policies/UserPolicy.php
Normal file
26
app/Policies/UserPolicy.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UserPolicy
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ use App\Extensions\Themes\Theme;
|
||||
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 @@ class AppServiceProvider extends ServiceProvider
|
||||
'success' => Color::Green,
|
||||
'warning' => Color::Amber,
|
||||
]);
|
||||
|
||||
Gate::before(function (User $user, $ability) {
|
||||
return $user->isRootAdmin() ? true : null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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.
|
||||
|
@ -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';
|
||||
|
@ -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 @@ class UserCreationService
|
||||
$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);
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ abstract class BaseTransformer extends TransformerAbstract
|
||||
// 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);
|
||||
|
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Transformers\Api\Application;
|
||||
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
class RolePermissionTransformer extends BaseTransformer
|
||||
{
|
||||
public function getResourceName(): string
|
||||
{
|
||||
return 'permissions';
|
||||
}
|
||||
|
||||
public function transform(Permission $model): array
|
||||
{
|
||||
return [
|
||||
'name' => $model->name,
|
||||
'guard_name' => $model->guard_name,
|
||||
'created_at' => $model->created_at->toAtomString(),
|
||||
'updated_at' => $model->updated_at->toAtomString(),
|
||||
];
|
||||
}
|
||||
}
|
47
app/Transformers/Api/Application/RoleTransformer.php
Normal file
47
app/Transformers/Api/Application/RoleTransformer.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Transformers\Api\Application;
|
||||
|
||||
use App\Models\Role;
|
||||
use League\Fractal\Resource\Collection;
|
||||
use League\Fractal\Resource\NullResource;
|
||||
|
||||
class RoleTransformer extends BaseTransformer
|
||||
{
|
||||
protected array $availableIncludes = [
|
||||
'permissions',
|
||||
];
|
||||
|
||||
/**
|
||||
* Return the resource name for the JSONAPI output.
|
||||
*/
|
||||
public function getResourceName(): string
|
||||
{
|
||||
return Role::RESOURCE_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform role into a representation for the application API.
|
||||
*/
|
||||
public function transform(Role $model): array
|
||||
{
|
||||
return [
|
||||
'name' => $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');
|
||||
}
|
||||
}
|
@ -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 @@ class UserTransformer extends BaseTransformer
|
||||
'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 @@ class UserTransformer extends BaseTransformer
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -113,6 +113,6 @@ class ActivityLogTransformer extends BaseClientTransformer
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -29,8 +29,8 @@ class UserTransformer extends BaseClientTransformer
|
||||
'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),
|
||||
|
@ -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",
|
||||
|
84
composer.lock
generated
84
composer.lock
generated
@ -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",
|
||||
|
13
config/permission.php
Normal file
13
config/permission.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'models' => [
|
||||
|
||||
'permission' => Spatie\Permission\Models\Permission::class,
|
||||
|
||||
'role' => \App\Models\Role::class,
|
||||
|
||||
],
|
||||
|
||||
];
|
@ -33,19 +33,10 @@ class UserFactory extends Factory
|
||||
'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]);
|
||||
}
|
||||
}
|
||||
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$teams = config('permission.teams');
|
||||
$tableNames = config('permission.table_names');
|
||||
$columnNames = config('permission.column_names');
|
||||
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
|
||||
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
|
||||
|
||||
if (empty($tableNames)) {
|
||||
throw new \Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||
}
|
||||
if ($teams && empty($columnNames['team_foreign_key'] ?? null)) {
|
||||
throw new \Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||
}
|
||||
|
||||
Schema::create($tableNames['permissions'], function (Blueprint $table) {
|
||||
//$table->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']);
|
||||
}
|
||||
};
|
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$adminUsers = User::whereRootAdmin(true)->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);
|
||||
});
|
||||
}
|
||||
};
|
@ -13,7 +13,6 @@ return [
|
||||
'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',
|
||||
|
@ -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),
|
||||
|
@ -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 () => {
|
||||
<FontAwesomeIcon icon={faLayerGroup} />
|
||||
</NavLink>
|
||||
</Tooltip>
|
||||
{rootAdmin && (
|
||||
{isAdmin && (
|
||||
<Tooltip placement={'bottom'} content={t<string>('admin')}>
|
||||
<a href={'/admin'} rel={'noreferrer'}>
|
||||
<FontAwesomeIcon icon={faCogs} />
|
||||
|
@ -7,6 +7,7 @@ export interface UserData {
|
||||
email: string;
|
||||
language: string;
|
||||
rootAdmin: boolean;
|
||||
admin: boolean;
|
||||
useTotp: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
|
@ -19,6 +19,8 @@ Route::prefix('/users')->group(function () {
|
||||
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::prefix('mounts')->group(function () {
|
||||
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']);
|
||||
});
|
||||
|
@ -6,6 +6,7 @@ use Illuminate\Http\Request;
|
||||
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 @@ abstract class ApplicationApiIntegrationTestCase extends IntegrationTestCase
|
||||
*/
|
||||
protected function createApiUser(): User
|
||||
{
|
||||
return User::factory()->create([
|
||||
'root_admin' => true,
|
||||
]);
|
||||
$user = User::factory()->create();
|
||||
$user->syncRoles(Role::getRootAdmin());
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -38,7 +38,7 @@ class ExternalUserControllerTest extends ApplicationApiIntegrationTestCase
|
||||
'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),
|
||||
|
@ -55,7 +55,7 @@ class UserControllerTest extends ApplicationApiIntegrationTestCase
|
||||
'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 @@ class UserControllerTest extends ApplicationApiIntegrationTestCase
|
||||
'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),
|
||||
|
@ -7,6 +7,7 @@ use App\Models\Server;
|
||||
use App\Models\Subuser;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
|
||||
class ClientControllerTest extends ClientApiIntegrationTestCase
|
||||
{
|
||||
@ -47,7 +48,7 @@ class ClientControllerTest extends ClientApiIntegrationTestCase
|
||||
{
|
||||
/** @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 @@ class ClientControllerTest extends ClientApiIntegrationTestCase
|
||||
{
|
||||
/** @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 @@ class ClientControllerTest extends ClientApiIntegrationTestCase
|
||||
{
|
||||
/** @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]),
|
||||
|
@ -7,6 +7,7 @@ use App\Models\Node;
|
||||
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 @@ class SftpAuthenticationControllerTest extends IntegrationTestCase
|
||||
->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 @@ class SftpAuthenticationControllerTest extends IntegrationTestCase
|
||||
->assertOk()
|
||||
->assertJsonPath('permissions.0', '*');
|
||||
|
||||
$user->update(['root_admin' => false]);
|
||||
$user->syncRoles();
|
||||
$this->post('/api/remote/sftp/auth', $data)->assertForbidden();
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ namespace App\Tests;
|
||||
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 @@ abstract class TestCase extends BaseTestCase
|
||||
config()->set('app.debug', false);
|
||||
|
||||
$this->setKnownUuidFactory();
|
||||
|
||||
$this->app->make(PermissionRegistrar::class)->forgetCachedPermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -35,13 +35,14 @@ trait RequestMockHelpers
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -4,6 +4,7 @@ namespace App\Tests\Unit\Http\Middleware;
|
||||
|
||||
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 @@ class AdminAuthenticateTest extends MiddlewareTestCase
|
||||
{
|
||||
$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);
|
||||
|
||||
|
@ -27,7 +27,7 @@ class AuthenticateUserTest extends MiddlewareTestCase
|
||||
{
|
||||
$this->expectException(AccessDeniedHttpException::class);
|
||||
|
||||
$this->generateRequestUserModel(['root_admin' => false]);
|
||||
$this->generateRequestUserModel(false);
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||
}
|
||||
@ -37,7 +37,7 @@ class AuthenticateUserTest extends MiddlewareTestCase
|
||||
*/
|
||||
public function testAdminUser(): void
|
||||
{
|
||||
$this->generateRequestUserModel(['root_admin' => true]);
|
||||
$this->generateRequestUserModel(true);
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user