* @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; } }