File transfer

This commit is contained in:
2026-06-11 20:33:44 -05:00
parent 2cb1d58264
commit b362357bcb
58 changed files with 5096 additions and 4 deletions

View File

@@ -0,0 +1,123 @@
<?php
/**
* Permission Resolver — Core of the Role-Based Permission System
*
* Resolution order (first match wins):
* 1. Server-level explicit DENY → false
* 2. Server-level explicit ALLOW → true
* 3. Role DENY (highest priority role first) → false
* 4. Role ALLOW → true
* 5. Default → false
*
* @author Ball Studios <https://git.balls.studio>
* @license MIT
*/
namespace Pterodactyl\Addons\AdvancedAdmin\Services\Roles;
use Illuminate\Support\Collection;
use Pterodactyl\Addons\AdvancedAdmin\Models\Role;
use Pterodactyl\Addons\AdvancedAdmin\Models\RoleAuditLog;
use Pterodactyl\Addons\AdvancedAdmin\Models\ServerPermissionOverride;
use Pterodactyl\Addons\AdvancedAdmin\Models\ServerRoleAssignment;
class PermissionResolver
{
/**
* Resolve whether a user has a permission on a server.
*
* @return bool
*/
public function resolve(int $userId, int $serverId, string $permission): bool
{
// 1 & 2: Check server-level override
$override = ServerPermissionOverride::query()
->where('server_id', $serverId)
->where('user_id', $userId)
->where('permission', $permission)
->first();
if ($override && !$override->isUnset()) {
return $override->isAllow();
}
// 3 & 4: Check role-level (sorted by priority desc — highest wins)
$roles = $this->getUserRolesForServer($userId, $serverId);
foreach ($roles as $role) {
$value = $role->getPermission($permission);
if ($value === 'deny') return false;
if ($value === 'allow') return true;
}
// 5: Default deny
return false;
}
/**
* Get full effective permission map for a user on a server.
* Returns array of [permission => ['value' => bool, 'source' => string]]
*
* @param string[] $allPermissions
* @return array<string, array{value: bool, source: string}>
*/
public function resolveAll(int $userId, int $serverId, array $allPermissions): array
{
$overrides = ServerPermissionOverride::query()
->where('server_id', $serverId)
->where('user_id', $userId)
->get()
->keyBy('permission');
$roles = $this->getUserRolesForServer($userId, $serverId);
$result = [];
foreach ($allPermissions as $permission) {
[$value, $source] = $this->resolveWithSource($permission, $overrides, $roles);
$result[$permission] = ['value' => $value, 'source' => $source];
}
return $result;
}
/**
* Get user's assigned roles for a server, ordered by priority DESC.
*/
public function getUserRolesForServer(int $userId, int $serverId): Collection
{
return ServerRoleAssignment::query()
->where('server_id', $serverId)
->where('user_id', $userId)
->with(['role.permissions'])
->get()
->map(fn ($a) => $a->role)
->filter()
->sortByDesc('priority')
->values();
}
// ─── Internal ─────────────────────────────────────────────────────────────
private function resolveWithSource(
string $permission,
Collection $overrides,
Collection $roles
): array {
// Server override
if ($overrides->has($permission)) {
$o = $overrides->get($permission);
if (!$o->isUnset()) {
return [$o->isAllow(), 'server_override'];
}
}
// Role
foreach ($roles as $role) {
$value = $role->getPermission($permission);
if ($value === 'deny') return [false, "role:{$role->name}"];
if ($value === 'allow') return [true, "role:{$role->name}"];
}
return [false, 'default'];
}
}

View File

@@ -0,0 +1,201 @@
<?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;
}
}