mirror of
https://github.com/pelican-dev/panel.git
synced 2025-05-20 00:34:44 +02:00
Fix server access for admins without subuser (#919)
* fix server access for admins without subuser * add permission checks to power buttons * add permission check for console command sending * fix tests * fix websocket token permissions * fix sftp access * fix server api + small cleanup * it's "update", not "edit"... * fix tests * fix permission const for "activity read" * fix activity subuser permission
This commit is contained in:
parent
61bdf0dcd7
commit
03eaddb126
@ -19,7 +19,7 @@ class ListServers extends ListRecords
|
|||||||
|
|
||||||
public function table(Table $table): Table
|
public function table(Table $table): Table
|
||||||
{
|
{
|
||||||
$baseQuery = auth()->user()->can('viewList server') ? Server::query() : auth()->user()->accessibleServers();
|
$baseQuery = auth()->user()->accessibleServers();
|
||||||
|
|
||||||
return $table
|
return $table
|
||||||
->paginated(false)
|
->paginated(false)
|
||||||
|
@ -10,6 +10,7 @@ use App\Filament\Server\Widgets\ServerMemoryChart;
|
|||||||
// use App\Filament\Server\Widgets\ServerNetworkChart;
|
// use App\Filament\Server\Widgets\ServerNetworkChart;
|
||||||
use App\Filament\Server\Widgets\ServerOverview;
|
use App\Filament\Server\Widgets\ServerOverview;
|
||||||
use App\Livewire\AlertBanner;
|
use App\Livewire\AlertBanner;
|
||||||
|
use App\Models\Permission;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Facades\Filament;
|
use Filament\Facades\Filament;
|
||||||
@ -94,16 +95,19 @@ class Console extends Page
|
|||||||
->color('primary')
|
->color('primary')
|
||||||
->size(ActionSize::ExtraLarge)
|
->size(ActionSize::ExtraLarge)
|
||||||
->action(fn () => $this->dispatch('setServerState', state: 'start', uuid: $server->uuid))
|
->action(fn () => $this->dispatch('setServerState', state: 'start', uuid: $server->uuid))
|
||||||
|
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_START, $server))
|
||||||
->disabled(fn () => $server->isInConflictState() || !$this->status->isStartable()),
|
->disabled(fn () => $server->isInConflictState() || !$this->status->isStartable()),
|
||||||
Action::make('restart')
|
Action::make('restart')
|
||||||
->color('gray')
|
->color('gray')
|
||||||
->size(ActionSize::ExtraLarge)
|
->size(ActionSize::ExtraLarge)
|
||||||
->action(fn () => $this->dispatch('setServerState', state: 'restart', uuid: $server->uuid))
|
->action(fn () => $this->dispatch('setServerState', state: 'restart', uuid: $server->uuid))
|
||||||
|
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_RESTART, $server))
|
||||||
->disabled(fn () => $server->isInConflictState() || !$this->status->isRestartable()),
|
->disabled(fn () => $server->isInConflictState() || !$this->status->isRestartable()),
|
||||||
Action::make('stop')
|
Action::make('stop')
|
||||||
->color('danger')
|
->color('danger')
|
||||||
->size(ActionSize::ExtraLarge)
|
->size(ActionSize::ExtraLarge)
|
||||||
->action(fn () => $this->dispatch('setServerState', state: 'stop', uuid: $server->uuid))
|
->action(fn () => $this->dispatch('setServerState', state: 'stop', uuid: $server->uuid))
|
||||||
|
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
|
||||||
->hidden(fn () => $this->status->isStartingOrStopping() || $this->status->isKillable())
|
->hidden(fn () => $this->status->isStartingOrStopping() || $this->status->isKillable())
|
||||||
->disabled(fn () => $server->isInConflictState() || !$this->status->isStoppable()),
|
->disabled(fn () => $server->isInConflictState() || !$this->status->isStoppable()),
|
||||||
Action::make('kill')
|
Action::make('kill')
|
||||||
@ -114,6 +118,7 @@ class Console extends Page
|
|||||||
->modalSubmitActionLabel('Kill Server')
|
->modalSubmitActionLabel('Kill Server')
|
||||||
->size(ActionSize::ExtraLarge)
|
->size(ActionSize::ExtraLarge)
|
||||||
->action(fn () => $this->dispatch('setServerState', state: 'kill', uuid: $server->uuid))
|
->action(fn () => $this->dispatch('setServerState', state: 'kill', uuid: $server->uuid))
|
||||||
|
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
|
||||||
->hidden(fn () => $server->isInConflictState() || !$this->status->isKillable()),
|
->hidden(fn () => $server->isInConflictState() || !$this->status->isKillable()),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -115,7 +115,9 @@ class ListUsers extends ListRecords
|
|||||||
'settings' => [
|
'settings' => [
|
||||||
'rename',
|
'rename',
|
||||||
'reinstall',
|
'reinstall',
|
||||||
'activity',
|
],
|
||||||
|
'activity' => [
|
||||||
|
'read',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -357,6 +359,24 @@ class ListUsers extends ListRecords
|
|||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
|
Tabs\Tab::make('Activity')
|
||||||
|
->schema([
|
||||||
|
Section::make()
|
||||||
|
->description(trans('server/users.permissions.activity_desc'))
|
||||||
|
->icon('tabler-stack')
|
||||||
|
->schema([
|
||||||
|
CheckboxList::make('activity')
|
||||||
|
->bulkToggleable()
|
||||||
|
->label('')
|
||||||
|
->columns(2)
|
||||||
|
->options([
|
||||||
|
'read' => 'Read',
|
||||||
|
])
|
||||||
|
->descriptions([
|
||||||
|
'read' => trans('server/users.permissions.activity_read'),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
]),
|
]),
|
||||||
|
@ -67,9 +67,14 @@ class ServerConsole extends Widget
|
|||||||
return $socket;
|
return $socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function authorizeSendCommand(): bool
|
||||||
|
{
|
||||||
|
return $this->user->can(Permission::ACTION_CONTROL_CONSOLE, $this->server);
|
||||||
|
}
|
||||||
|
|
||||||
protected function canSendCommand(): bool
|
protected function canSendCommand(): bool
|
||||||
{
|
{
|
||||||
return !$this->server->isInConflictState() && $this->server->retrieveStatus() === 'running';
|
return $this->authorizeSendCommand() && !$this->server->isInConflictState() && $this->server->retrieveStatus() === 'running';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function up(): void
|
public function up(): void
|
||||||
|
@ -53,12 +53,12 @@ class ClientController extends ClientApiController
|
|||||||
} else {
|
} else {
|
||||||
$builder = $type === 'admin-all'
|
$builder = $type === 'admin-all'
|
||||||
? $builder
|
? $builder
|
||||||
: $builder->whereNotIn('servers.id', $user->accessibleServers()->pluck('id')->all());
|
: $builder->whereNotIn('servers.id', $user->directAccessibleServers()->pluck('id')->all());
|
||||||
}
|
}
|
||||||
} elseif ($type === 'owner') {
|
} elseif ($type === 'owner') {
|
||||||
$builder = $builder->where('servers.owner_id', $user->id);
|
$builder = $builder->where('servers.owner_id', $user->id);
|
||||||
} else {
|
} else {
|
||||||
$builder = $builder->whereIn('servers.id', $user->accessibleServers()->pluck('id')->all());
|
$builder = $builder->whereIn('servers.id', $user->directAccessibleServers()->pluck('id')->all());
|
||||||
}
|
}
|
||||||
|
|
||||||
$servers = $builder->paginate(min($request->query('per_page', '50'), 100))->appends($request->query());
|
$servers = $builder->paginate(min($request->query('per_page', '50'), 100))->appends($request->query());
|
||||||
|
@ -138,7 +138,7 @@ class SftpAuthenticationController extends Controller
|
|||||||
*/
|
*/
|
||||||
protected function validateSftpAccess(User $user, Server $server): void
|
protected function validateSftpAccess(User $user, Server $server): void
|
||||||
{
|
{
|
||||||
if (!$user->isRootAdmin() && $server->owner_id !== $user->id) {
|
if ($user->cannot('update server', $server) && $server->owner_id !== $user->id) {
|
||||||
$permissions = $this->permissions->handle($server, $user);
|
$permissions = $this->permissions->handle($server, $user);
|
||||||
|
|
||||||
if (!in_array(Permission::ACTION_FILE_SFTP, $permissions)) {
|
if (!in_array(Permission::ACTION_FILE_SFTP, $permissions)) {
|
||||||
|
@ -35,9 +35,9 @@ class AuthenticateServerAccess
|
|||||||
}
|
}
|
||||||
|
|
||||||
// At the very least, ensure that the user trying to make this request is the
|
// 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
|
// server owner, a subuser, or an admin. We'll leave it up to the controllers
|
||||||
// to authenticate more detailed permissions if needed.
|
// to authenticate more detailed permissions if needed.
|
||||||
if ($user->id !== $server->owner_id && !$user->isRootAdmin()) {
|
if ($user->id !== $server->owner_id && $user->cannot('update server', $server)) {
|
||||||
// Check for subuser status.
|
// Check for subuser status.
|
||||||
if (!$server->subusers->contains('user_id', $user->id)) {
|
if (!$server->subusers->contains('user_id', $user->id)) {
|
||||||
throw new NotFoundHttpException(trans('exceptions.api.resource_not_found'));
|
throw new NotFoundHttpException(trans('exceptions.api.resource_not_found'));
|
||||||
@ -53,7 +53,7 @@ class AuthenticateServerAccess
|
|||||||
if (($server->isSuspended() || $server->node->isUnderMaintenance()) && !$request->routeIs('api:client:server.resources')) {
|
if (($server->isSuspended() || $server->node->isUnderMaintenance()) && !$request->routeIs('api:client:server.resources')) {
|
||||||
throw $exception;
|
throw $exception;
|
||||||
}
|
}
|
||||||
if (!$user->isRootAdmin() || !$request->routeIs($this->except)) {
|
if ($user->cannot('update server', $server) || !$request->routeIs($this->except)) {
|
||||||
throw $exception;
|
throw $exception;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,8 +55,8 @@ abstract class SubuserRequest extends ClientApiRequest
|
|||||||
/** @var \App\Models\Server $server */
|
/** @var \App\Models\Server $server */
|
||||||
$server = $this->route()->parameter('server');
|
$server = $this->route()->parameter('server');
|
||||||
|
|
||||||
// If we are a root admin or the server owner, no need to perform these checks.
|
// If we are an admin or the server owner, no need to perform these checks.
|
||||||
if ($user->isRootAdmin() || $user->id === $server->owner_id) {
|
if ($user->can('update server', $server) || $user->id === $server->owner_id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ class Permission extends Model
|
|||||||
|
|
||||||
public const ACTION_SETTINGS_REINSTALL = 'settings.reinstall';
|
public const ACTION_SETTINGS_REINSTALL = 'settings.reinstall';
|
||||||
|
|
||||||
public const ACTION_ACTIVITY_READ = 'settings.activity';
|
public const ACTION_ACTIVITY_READ = 'activity.read';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should timestamps be used on this model.
|
* Should timestamps be used on this model.
|
||||||
|
@ -289,10 +289,23 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all the servers that a user can access by way of being the owner of the
|
* Returns all the servers that a user can access.
|
||||||
* server, or because they are assigned as a subuser for that server.
|
* Either because they are an admin or because they are the owner/ a subuser of the server.
|
||||||
*/
|
*/
|
||||||
public function accessibleServers(): Builder
|
public function accessibleServers(): Builder
|
||||||
|
{
|
||||||
|
if ($this->canned('viewList server')) {
|
||||||
|
return Server::query();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->directAccessibleServers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all the servers that a user can access "directly".
|
||||||
|
* This means either because they are the owner or a subuser of the server.
|
||||||
|
*/
|
||||||
|
public function directAccessibleServers(): Builder
|
||||||
{
|
{
|
||||||
return Server::query()
|
return Server::query()
|
||||||
->select('servers.*')
|
->select('servers.*')
|
||||||
@ -315,7 +328,12 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
|||||||
|
|
||||||
protected function checkPermission(Server $server, string $permission = ''): bool
|
protected function checkPermission(Server $server, string $permission = ''): bool
|
||||||
{
|
{
|
||||||
if ($this->isRootAdmin() || $server->owner_id === $this->id) {
|
if ($this->canned('update server', $server) || $server->owner_id === $this->id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user only has "view" permissions allow viewing the console
|
||||||
|
if ($permission === Permission::ACTION_WEBSOCKET_CONNECT && $this->canned('view server', $server)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -361,6 +379,11 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
|||||||
return $this->hasRole(Role::ROOT_ADMIN);
|
return $this->hasRole(Role::ROOT_ADMIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isAdmin(): bool
|
||||||
|
{
|
||||||
|
return $this->isRootAdmin() || ($this->roles()->count() >= 1 && $this->getAllPermissions()->count() >= 1);
|
||||||
|
}
|
||||||
|
|
||||||
public function canAccessPanel(Panel $panel): bool
|
public function canAccessPanel(Panel $panel): bool
|
||||||
{
|
{
|
||||||
if ($this->isRootAdmin()) {
|
if ($this->isRootAdmin()) {
|
||||||
@ -368,7 +391,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($panel->getId() === 'admin') {
|
if ($panel->getId() === 'admin') {
|
||||||
return $this->roles()->count() >= 1 && $this->getAllPermissions()->count() >= 1;
|
return $this->isAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -401,7 +424,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
|||||||
public function canAccessTenant(IlluminateModel $tenant): bool
|
public function canAccessTenant(IlluminateModel $tenant): bool
|
||||||
{
|
{
|
||||||
if ($tenant instanceof Server) {
|
if ($tenant instanceof Server) {
|
||||||
if ($this->isRootAdmin() || $tenant->owner_id === $this->id) {
|
if ($this->canned('view server', $tenant) || $tenant->owner_id === $this->id) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,23 +9,25 @@ class GetUserPermissionsService
|
|||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Returns the server specific permissions that a user has. This checks
|
* Returns the server specific permissions that a user has. This checks
|
||||||
* if they are an admin or a subuser for the server. If no permissions are
|
* if they are an admin, the owner or a subuser for the server. If no
|
||||||
* found, an empty array is returned.
|
* permissions are found, an empty array is returned.
|
||||||
*/
|
*/
|
||||||
public function handle(Server $server, User $user): array
|
public function handle(Server $server, User $user): array
|
||||||
{
|
{
|
||||||
if ($user->isRootAdmin() || $user->id === $server->owner_id) {
|
if ($user->isAdmin() && ($user->can('view server', $server) || $user->can('update server', $server))) {
|
||||||
$permissions = ['*'];
|
$permissions = $user->can('update server', $server) ? ['*'] : ['websocket.connect', 'backup.read'];
|
||||||
|
|
||||||
if ($user->isRootAdmin()) {
|
|
||||||
$permissions[] = 'admin.websocket.errors';
|
$permissions[] = 'admin.websocket.errors';
|
||||||
$permissions[] = 'admin.websocket.install';
|
$permissions[] = 'admin.websocket.install';
|
||||||
$permissions[] = 'admin.websocket.transfer';
|
$permissions[] = 'admin.websocket.transfer';
|
||||||
}
|
|
||||||
|
|
||||||
return $permissions;
|
return $permissions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($user->id === $server->owner_id) {
|
||||||
|
return ['*'];
|
||||||
|
}
|
||||||
|
|
||||||
/** @var \App\Models\Subuser|null $subuserPermissions */
|
/** @var \App\Models\Subuser|null $subuserPermissions */
|
||||||
$subuserPermissions = $server->subusers()->where('user_id', $user->id)->first();
|
$subuserPermissions = $server->subusers()->where('user_id', $user->id)->first();
|
||||||
|
|
||||||
|
@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'permissions' => [
|
'permissions' => [
|
||||||
|
'activity_desc' => 'Permissions that control a user\'s access to the server activity logs.',
|
||||||
'startup_desc' => 'Permissions that control a user\'s ability to view this server\'s startup parameters.',
|
'startup_desc' => 'Permissions that control a user\'s ability to view this server\'s startup parameters.',
|
||||||
'settings_desc' => 'Permissions that control a user\'s access to the schedule management for this server.',
|
'settings_desc' => 'Permissions that control a user\'s ability to modify this server\'s settings.',
|
||||||
'control_desc' => 'Permissions that control a user\'s ability to control the power state of a server, or send commands.',
|
'control_desc' => 'Permissions that control a user\'s ability to control the power state of a server, or send commands.',
|
||||||
'user_desc' => 'Permissions that allow a user to manage other subusers on a server. They will never be able to edit their own account, or assign permissions they do not have themselves.',
|
'user_desc' => 'Permissions that allow a user to manage other subusers on a server. They will never be able to edit their own account, or assign permissions they do not have themselves.',
|
||||||
'file_desc' => 'Permissions that control a user\'s ability to modify the filesystem for this server.',
|
'file_desc' => 'Permissions that control a user\'s ability to modify the filesystem for this server.',
|
||||||
@ -16,7 +17,7 @@ return [
|
|||||||
'startup_docker_image' => 'Allows a user to modify the Docker image used when running the server.',
|
'startup_docker_image' => 'Allows a user to modify the Docker image used when running the server.',
|
||||||
'setting_reinstall' => 'Allows a user to trigger a reinstall of this server.',
|
'setting_reinstall' => 'Allows a user to trigger a reinstall of this server.',
|
||||||
'setting_rename' => 'Allows a user to rename this server and change the description of it.',
|
'setting_rename' => 'Allows a user to rename this server and change the description of it.',
|
||||||
'setting_activity' => 'Allows a user to view the activity logs for the server.',
|
'activity_read' => 'Allows a user to view the activity logs for the server.',
|
||||||
'websocket_*' => 'Allows a user access to the websocket for this server.',
|
'websocket_*' => 'Allows a user access to the websocket for this server.',
|
||||||
'control_console' => 'Allows a user to send data to the server console.',
|
'control_console' => 'Allows a user to send data to the server console.',
|
||||||
'control_start' => 'Allows a user to start the server instance.',
|
'control_start' => 'Allows a user to start the server instance.',
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
<div id="terminal" wire:ignore></div>
|
<div id="terminal" wire:ignore></div>
|
||||||
|
|
||||||
|
@if ($this->authorizeSendCommand())
|
||||||
<div class="flex items-center w-full border-top overflow-hidden dark:bg-gray-900"
|
<div class="flex items-center w-full border-top overflow-hidden dark:bg-gray-900"
|
||||||
style="border-bottom-right-radius: 10px; border-bottom-left-radius: 10px;">
|
style="border-bottom-right-radius: 10px; border-bottom-left-radius: 10px;">
|
||||||
<x-filament::icon
|
<x-filament::icon
|
||||||
@ -28,6 +29,7 @@
|
|||||||
wire:keydown.down="down"
|
wire:keydown.down="down"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
@script
|
@script
|
||||||
<script>
|
<script>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user