202 lines
7.5 KiB
PHP
202 lines
7.5 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @author Ball Studios <https://git.balls.studio>
|
|
* @license MIT
|
|
*/
|
|
|
|
namespace Pterodactyl\Addons\AdvancedAdmin\Services\Roles;
|
|
|
|
use Illuminate\Validation\ValidationException;
|
|
use Pterodactyl\Addons\AdvancedAdmin\Models\Role;
|
|
use Pterodactyl\Addons\AdvancedAdmin\Models\RoleAuditLog;
|
|
use Pterodactyl\Addons\AdvancedAdmin\Models\RolePermission;
|
|
use Pterodactyl\Addons\AdvancedAdmin\Models\ServerPermissionOverride;
|
|
use Pterodactyl\Addons\AdvancedAdmin\Models\ServerRoleAssignment;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class RoleService
|
|
{
|
|
/**
|
|
* All known Pterodactyl permission keys (validated against).
|
|
*/
|
|
public const KNOWN_PERMISSIONS = [
|
|
'websocket.connect',
|
|
'control.console', 'control.start', 'control.stop', 'control.restart',
|
|
'user.read', 'user.create', 'user.update', 'user.delete',
|
|
'file.read', 'file.read-content', 'file.create', 'file.update',
|
|
'file.delete', 'file.archive', 'file.sftp',
|
|
'backup.read', 'backup.create', 'backup.delete', 'backup.download', 'backup.restore',
|
|
'allocation.read', 'allocation.create', 'allocation.update', 'allocation.delete',
|
|
'startup.read', 'startup.update', 'startup.docker-image',
|
|
'database.read', 'database.create', 'database.update', 'database.delete', 'database.view-password',
|
|
'schedule.read', 'schedule.create', 'schedule.update', 'schedule.delete',
|
|
'settings.read', 'settings.rename', 'settings.reinstall',
|
|
'activity.read',
|
|
];
|
|
|
|
public function create(array $data, int $actorId): Role
|
|
{
|
|
return DB::transaction(function () use ($data, $actorId) {
|
|
$role = Role::create([
|
|
'name' => $data['name'],
|
|
'description' => $data['description'] ?? null,
|
|
'color' => $data['color'] ?? '#6366f1',
|
|
'priority' => $data['priority'] ?? 0,
|
|
'is_default' => $data['is_default'] ?? false,
|
|
'created_by' => $actorId,
|
|
]);
|
|
|
|
if (!empty($data['permissions'])) {
|
|
$this->syncPermissions($role, $data['permissions'], $actorId);
|
|
}
|
|
|
|
RoleAuditLog::record($actorId, 'role.create', 'role', $role->id, ['name' => $role->name]);
|
|
return $role;
|
|
});
|
|
}
|
|
|
|
public function update(Role $role, array $data, int $actorId): Role
|
|
{
|
|
return DB::transaction(function () use ($role, $data, $actorId) {
|
|
$role->update(array_filter([
|
|
'name' => $data['name'] ?? null,
|
|
'description' => $data['description'] ?? null,
|
|
'color' => $data['color'] ?? null,
|
|
'priority' => $data['priority'] ?? null,
|
|
'is_default' => $data['is_default'] ?? null,
|
|
], fn ($v) => $v !== null));
|
|
|
|
if (isset($data['permissions'])) {
|
|
$this->syncPermissions($role, $data['permissions'], $actorId);
|
|
}
|
|
|
|
RoleAuditLog::record($actorId, 'role.update', 'role', $role->id, $data);
|
|
return $role->refresh();
|
|
});
|
|
}
|
|
|
|
public function delete(Role $role, int $actorId, bool $force = false): void
|
|
{
|
|
if ($role->isInUse() && !$force) {
|
|
throw ValidationException::withMessages([
|
|
'role' => ['This role is still assigned to users. Pass force=true to delete anyway.'],
|
|
]);
|
|
}
|
|
|
|
DB::transaction(function () use ($role, $actorId) {
|
|
RoleAuditLog::record($actorId, 'role.delete', 'role', $role->id, ['name' => $role->name]);
|
|
$role->delete();
|
|
});
|
|
}
|
|
|
|
public function syncPermissions(Role $role, array $permissions, int $actorId): void
|
|
{
|
|
// Validate all permission names
|
|
$invalid = array_diff(array_keys($permissions), self::KNOWN_PERMISSIONS);
|
|
if (!empty($invalid)) {
|
|
throw ValidationException::withMessages([
|
|
'permissions' => ['Unknown permissions: ' . implode(', ', $invalid)],
|
|
]);
|
|
}
|
|
|
|
DB::transaction(function () use ($role, $permissions, $actorId) {
|
|
$role->permissions()->delete();
|
|
foreach ($permissions as $perm => $value) {
|
|
if (!in_array($value, ['allow', 'deny'], true)) continue;
|
|
RolePermission::create([
|
|
'role_id' => $role->id,
|
|
'permission' => $perm,
|
|
'value' => $value,
|
|
]);
|
|
}
|
|
RoleAuditLog::record($actorId, 'role.permissions_updated', 'role', $role->id, ['permissions' => $permissions]);
|
|
});
|
|
}
|
|
|
|
public function assignToUser(int $serverId, int $userId, int $roleId, int $actorId): ServerRoleAssignment
|
|
{
|
|
// Prevent privilege escalation: actor's max role priority must be >= target role priority
|
|
$targetRole = Role::findOrFail($roleId);
|
|
$actorMaxPrio = $this->getMaxPriorityForUser($actorId, $serverId);
|
|
|
|
if ($targetRole->priority > $actorMaxPrio) {
|
|
throw ValidationException::withMessages([
|
|
'role' => ['You cannot assign a role with higher priority than your own.'],
|
|
]);
|
|
}
|
|
|
|
$assignment = ServerRoleAssignment::firstOrCreate([
|
|
'server_id' => $serverId,
|
|
'user_id' => $userId,
|
|
'role_id' => $roleId,
|
|
], [
|
|
'assigned_by' => $actorId,
|
|
]);
|
|
|
|
RoleAuditLog::record($actorId, 'role.assign', 'user', $userId, [
|
|
'server_id' => $serverId,
|
|
'role_id' => $roleId,
|
|
'role_name' => $targetRole->name,
|
|
]);
|
|
|
|
return $assignment;
|
|
}
|
|
|
|
public function unassignFromUser(int $serverId, int $userId, int $roleId, int $actorId): void
|
|
{
|
|
ServerRoleAssignment::query()
|
|
->where('server_id', $serverId)
|
|
->where('user_id', $userId)
|
|
->where('role_id', $roleId)
|
|
->delete();
|
|
|
|
RoleAuditLog::record($actorId, 'role.unassign', 'user', $userId, [
|
|
'server_id' => $serverId,
|
|
'role_id' => $roleId,
|
|
]);
|
|
}
|
|
|
|
public function setOverride(
|
|
int $serverId,
|
|
int $userId,
|
|
string $permission,
|
|
string $value,
|
|
int $actorId
|
|
): ServerPermissionOverride {
|
|
if (!in_array($permission, self::KNOWN_PERMISSIONS, true)) {
|
|
throw ValidationException::withMessages(['permission' => ['Unknown permission key.']]);
|
|
}
|
|
if (!in_array($value, ['allow', 'deny', 'unset'], true)) {
|
|
throw ValidationException::withMessages(['value' => ['Value must be allow, deny, or unset.']]);
|
|
}
|
|
|
|
$override = ServerPermissionOverride::updateOrCreate(
|
|
['server_id' => $serverId, 'user_id' => $userId, 'permission' => $permission],
|
|
['value' => $value, 'set_by' => $actorId]
|
|
);
|
|
|
|
RoleAuditLog::record($actorId, 'override.set', 'user', $userId, [
|
|
'server_id' => $serverId,
|
|
'permission' => $permission,
|
|
'value' => $value,
|
|
]);
|
|
|
|
return $override;
|
|
}
|
|
|
|
private function getMaxPriorityForUser(int $userId, int $serverId): int
|
|
{
|
|
// If admin, return PHP_INT_MAX
|
|
$user = \Pterodactyl\Models\User::find($userId);
|
|
if ($user && $user->root_admin) return PHP_INT_MAX;
|
|
|
|
return ServerRoleAssignment::query()
|
|
->where('server_id', $serverId)
|
|
->where('user_id', $userId)
|
|
->with('role')
|
|
->get()
|
|
->max(fn ($a) => $a->role->priority ?? 0) ?? 0;
|
|
}
|
|
}
|