Refactor api key permissions (#361)

* use RESOURCE_NAME for requests

* use RESOURCE_NAME for transformers

* add permissions field to api key

* add migration for new permissions field

* update tests

* remove debug log

* set column type to "json"

* remove default attribute to fix tests

* fix default value for permissions

* fix after merge

* fix after merge

* allow to "register" custom permissions

* add "role" to default resource names

* fix after merge

* fix phpstan

* fix migrations
This commit is contained in:
Boy132 2024-11-06 09:09:10 +01:00 committed by GitHub
parent ac67656d82
commit b3501be6ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 453 additions and 321 deletions

View File

@ -11,6 +11,7 @@ use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\ToggleButtons; use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model;
class CreateApiKey extends CreateRecord class CreateApiKey extends CreateRecord
{ {
@ -41,7 +42,7 @@ class CreateApiKey extends CreateRecord
'md' => 2, 'md' => 2,
]) ])
->schema( ->schema(
collect(ApiKey::RESOURCES)->map(fn ($resource) => ToggleButtons::make("r_$resource") collect(ApiKey::getPermissionList())->map(fn ($resource) => ToggleButtons::make('permissions_' . $resource)
->label(str($resource)->replace('_', ' ')->title())->inline() ->label(str($resource)->replace('_', ' ')->title())->inline()
->options([ ->options([
0 => 'None', 0 => 'None',
@ -87,4 +88,20 @@ class CreateApiKey extends CreateRecord
->columnSpanFull(), ->columnSpanFull(),
]); ]);
} }
protected function handleRecordCreation(array $data): Model
{
$permissions = [];
foreach (ApiKey::getPermissionList() as $permission) {
if (isset($data['permissions_' . $permission])) {
$permissions[$permission] = intval($data['permissions_' . $permission]);
unset($data['permissions_' . $permission]);
}
}
$data['permissions'] = $permissions;
return parent::handleRecordCreation($data);
}
} }

View File

@ -3,7 +3,6 @@
namespace App\Http\Requests\Admin\Api; namespace App\Http\Requests\Admin\Api;
use App\Models\ApiKey; use App\Models\ApiKey;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Admin\AdminFormRequest; use App\Http\Requests\Admin\AdminFormRequest;
class StoreApplicationApiKeyRequest extends AdminFormRequest class StoreApplicationApiKeyRequest extends AdminFormRequest
@ -16,9 +15,12 @@ class StoreApplicationApiKeyRequest extends AdminFormRequest
{ {
$modelRules = ApiKey::getRules(); $modelRules = ApiKey::getRules();
return collect(AdminAcl::getResourceList())->mapWithKeys(function ($resource) use ($modelRules) { $rules = [
return [AdminAcl::COLUMN_IDENTIFIER . $resource => $modelRules['r_' . $resource]]; 'memo' => $modelRules['memo'],
})->merge(['memo' => $modelRules['memo']])->toArray(); 'permissions' => $modelRules['permissions'],
];
return $rules;
} }
public function attributes(): array public function attributes(): array
@ -30,8 +32,8 @@ class StoreApplicationApiKeyRequest extends AdminFormRequest
public function getKeyPermissions(): array public function getKeyPermissions(): array
{ {
return collect($this->validated())->filter(function ($value, $key) { $data = $this->validated();
return substr($key, 0, strlen(AdminAcl::COLUMN_IDENTIFIER)) === AdminAcl::COLUMN_IDENTIFIER;
})->toArray(); return array_keys($data['permissions']);
} }
} }

View File

@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Allocations;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Allocation;
class DeleteAllocationRequest extends ApplicationApiRequest class DeleteAllocationRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_ALLOCATIONS; protected ?string $resource = Allocation::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE; protected int $permission = AdminAcl::WRITE;
} }

View File

@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Allocations;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Allocation;
class GetAllocationsRequest extends ApplicationApiRequest class GetAllocationsRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_ALLOCATIONS; protected ?string $resource = Allocation::RESOURCE_NAME;
protected int $permission = AdminAcl::READ; protected int $permission = AdminAcl::READ;
} }

View File

@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Allocations;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Allocation;
class StoreAllocationRequest extends ApplicationApiRequest class StoreAllocationRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_ALLOCATIONS; protected ?string $resource = Allocation::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE; protected int $permission = AdminAcl::WRITE;

View File

@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\DatabaseHosts;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\DatabaseHost;
class DeleteDatabaseHostRequest extends ApplicationApiRequest class DeleteDatabaseHostRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_DATABASE_HOSTS; protected ?string $resource = DatabaseHost::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE; protected int $permission = AdminAcl::WRITE;
} }

View File

@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\DatabaseHosts;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\DatabaseHost;
class GetDatabaseHostRequest extends ApplicationApiRequest class GetDatabaseHostRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_DATABASE_HOSTS; protected ?string $resource = DatabaseHost::RESOURCE_NAME;
protected int $permission = AdminAcl::READ; protected int $permission = AdminAcl::READ;
} }

View File

@ -8,7 +8,7 @@ use App\Http\Requests\Api\Application\ApplicationApiRequest;
class StoreDatabaseHostRequest extends ApplicationApiRequest class StoreDatabaseHostRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_DATABASE_HOSTS; protected ?string $resource = DatabaseHost::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE; protected int $permission = AdminAcl::WRITE;

View File

@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Eggs;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Models\Egg;
class GetEggRequest extends ApplicationApiRequest class GetEggRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_EGGS; protected ?string $resource = Egg::RESOURCE_NAME;
protected int $permission = AdminAcl::READ; protected int $permission = AdminAcl::READ;
} }

View File

@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Eggs;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Models\Egg;
class GetEggsRequest extends ApplicationApiRequest class GetEggsRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_EGGS; protected ?string $resource = Egg::RESOURCE_NAME;
protected int $permission = AdminAcl::READ; protected int $permission = AdminAcl::READ;
} }

View File

@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Mounts;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Mount;
class DeleteMountRequest extends ApplicationApiRequest class DeleteMountRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_MOUNTS; protected ?string $resource = Mount::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE; protected int $permission = AdminAcl::WRITE;
} }

View File

@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Mounts;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Mount;
class GetMountRequest extends ApplicationApiRequest class GetMountRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_MOUNTS; protected ?string $resource = Mount::RESOURCE_NAME;
protected int $permission = AdminAcl::READ; protected int $permission = AdminAcl::READ;
} }

View File

@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Mounts;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Mount;
class StoreMountRequest extends ApplicationApiRequest class StoreMountRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_MOUNTS; protected ?string $resource = Mount::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE; protected int $permission = AdminAcl::WRITE;
} }

View File

@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Nodes;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Node;
class DeleteNodeRequest extends ApplicationApiRequest class DeleteNodeRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_NODES; protected ?string $resource = Node::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE; protected int $permission = AdminAcl::WRITE;
} }

View File

@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Nodes;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Node;
class GetNodesRequest extends ApplicationApiRequest class GetNodesRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_NODES; protected ?string $resource = Node::RESOURCE_NAME;
protected int $permission = AdminAcl::READ; protected int $permission = AdminAcl::READ;
} }

View File

@ -8,7 +8,7 @@ use App\Http\Requests\Api\Application\ApplicationApiRequest;
class StoreNodeRequest extends ApplicationApiRequest class StoreNodeRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_NODES; protected ?string $resource = Node::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE; protected int $permission = AdminAcl::WRITE;

View File

@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Roles;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Role;
class DeleteRoleRequest extends ApplicationApiRequest class DeleteRoleRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_ROLES; protected ?string $resource = Role::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE; protected int $permission = AdminAcl::WRITE;
} }

View File

@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Roles;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Role;
class GetRoleRequest extends ApplicationApiRequest class GetRoleRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_ROLES; protected ?string $resource = Role::RESOURCE_NAME;
protected int $permission = AdminAcl::READ; protected int $permission = AdminAcl::READ;
} }

View File

@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Roles;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Role;
class StoreRoleRequest extends ApplicationApiRequest class StoreRoleRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_ROLES; protected ?string $resource = Role::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE; protected int $permission = AdminAcl::WRITE;

View File

@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Servers\Databases;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Database;
class GetServerDatabaseRequest extends ApplicationApiRequest class GetServerDatabaseRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_SERVER_DATABASES; protected ?string $resource = Database::RESOURCE_NAME;
protected int $permission = AdminAcl::READ; protected int $permission = AdminAcl::READ;
} }

View File

@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Servers\Databases;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Database;
class GetServerDatabasesRequest extends ApplicationApiRequest class GetServerDatabasesRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_SERVER_DATABASES; protected ?string $resource = Database::RESOURCE_NAME;
protected int $permission = AdminAcl::READ; protected int $permission = AdminAcl::READ;
} }

View File

@ -9,10 +9,11 @@ use Illuminate\Database\Query\Builder;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Services\Databases\DatabaseManagementService; use App\Services\Databases\DatabaseManagementService;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Database;
class StoreServerDatabaseRequest extends ApplicationApiRequest class StoreServerDatabaseRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_SERVER_DATABASES; protected ?string $resource = Database::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE; protected int $permission = AdminAcl::WRITE;

View File

@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Servers;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Server;
class GetExternalServerRequest extends ApplicationApiRequest class GetExternalServerRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_SERVERS; protected ?string $resource = Server::RESOURCE_NAME;
protected int $permission = AdminAcl::READ; protected int $permission = AdminAcl::READ;
} }

View File

@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Servers;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Server;
class GetServerRequest extends ApplicationApiRequest class GetServerRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_SERVERS; protected ?string $resource = Server::RESOURCE_NAME;
protected int $permission = AdminAcl::READ; protected int $permission = AdminAcl::READ;
} }

View File

@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Servers;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Server;
class ServerWriteRequest extends ApplicationApiRequest class ServerWriteRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_SERVERS; protected ?string $resource = Server::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE; protected int $permission = AdminAcl::WRITE;
} }

View File

@ -11,7 +11,7 @@ use App\Http\Requests\Api\Application\ApplicationApiRequest;
class StoreServerRequest extends ApplicationApiRequest class StoreServerRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_SERVERS; protected ?string $resource = Server::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE; protected int $permission = AdminAcl::WRITE;

View File

@ -8,7 +8,7 @@ use App\Http\Requests\Api\Application\ApplicationApiRequest;
class UpdateServerStartupRequest extends ApplicationApiRequest class UpdateServerStartupRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_SERVERS; protected ?string $resource = Server::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE; protected int $permission = AdminAcl::WRITE;

View File

@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Users;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\User;
class DeleteUserRequest extends ApplicationApiRequest class DeleteUserRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_USERS; protected ?string $resource = User::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE; protected int $permission = AdminAcl::WRITE;
} }

View File

@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Users;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\User;
class GetExternalUserRequest extends ApplicationApiRequest class GetExternalUserRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_USERS; protected ?string $resource = User::RESOURCE_NAME;
protected int $permission = AdminAcl::READ; protected int $permission = AdminAcl::READ;
} }

View File

@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Users;
use App\Services\Acl\Api\AdminAcl as Acl; use App\Services\Acl\Api\AdminAcl as Acl;
use App\Http\Requests\Api\Application\ApplicationApiRequest; use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\User;
class GetUsersRequest extends ApplicationApiRequest class GetUsersRequest extends ApplicationApiRequest
{ {
protected ?string $resource = Acl::RESOURCE_USERS; protected ?string $resource = User::RESOURCE_NAME;
protected int $permission = Acl::READ; protected int $permission = Acl::READ;
} }

View File

@ -8,7 +8,7 @@ use App\Http\Requests\Api\Application\ApplicationApiRequest;
class StoreUserRequest extends ApplicationApiRequest class StoreUserRequest extends ApplicationApiRequest
{ {
protected ?string $resource = AdminAcl::RESOURCE_USERS; protected ?string $resource = User::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE; protected int $permission = AdminAcl::WRITE;

View File

@ -43,7 +43,7 @@ class Allocation extends Model
{ {
/** /**
* The resource name for this model when it is transformed into an * The resource name for this model when it is transformed into an
* API representation using fractal. * API representation using fractal. Also used as name for api key permissions.
*/ */
public const RESOURCE_NAME = 'allocation'; public const RESOURCE_NAME = 'allocation';

View File

@ -2,9 +2,9 @@
namespace App\Models; namespace App\Models;
use App\Services\Acl\Api\AdminAcl;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use App\Services\Acl\Api\AdminAcl;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
/** /**
@ -15,20 +15,13 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property int $key_type * @property int $key_type
* @property string $identifier * @property string $identifier
* @property string $token * @property string $token
* @property array $permissions
* @property array $allowed_ips * @property array $allowed_ips
* @property string|null $memo * @property string|null $memo
* @property \Illuminate\Support\Carbon|null $last_used_at * @property \Illuminate\Support\Carbon|null $last_used_at
* @property \Illuminate\Support\Carbon|null $expires_at * @property \Illuminate\Support\Carbon|null $expires_at
* @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at * @property \Illuminate\Support\Carbon|null $updated_at
* @property int $r_servers
* @property int $r_nodes
* @property int $r_allocations
* @property int $r_users
* @property int $r_eggs
* @property int $r_database_hosts
* @property int $r_server_databases
* @property int $r_mounts
* @property \App\Models\User $tokenable * @property \App\Models\User $tokenable
* @property \App\Models\User $user * @property \App\Models\User $user
* *
@ -84,8 +77,6 @@ class ApiKey extends Model
*/ */
public const KEY_LENGTH = 32; public const KEY_LENGTH = 32;
public const RESOURCES = ['servers', 'nodes', 'allocations', 'users', 'eggs', 'database_hosts', 'server_databases', 'mounts'];
/** /**
* The table associated with the model. * The table associated with the model.
*/ */
@ -99,18 +90,11 @@ class ApiKey extends Model
'key_type', 'key_type',
'identifier', 'identifier',
'token', 'token',
'permissions',
'allowed_ips', 'allowed_ips',
'memo', 'memo',
'last_used_at', 'last_used_at',
'expires_at', 'expires_at',
'r_' . AdminAcl::RESOURCE_USERS,
'r_' . AdminAcl::RESOURCE_ALLOCATIONS,
'r_' . AdminAcl::RESOURCE_DATABASE_HOSTS,
'r_' . AdminAcl::RESOURCE_SERVER_DATABASES,
'r_' . AdminAcl::RESOURCE_EGGS,
'r_' . AdminAcl::RESOURCE_NODES,
'r_' . AdminAcl::RESOURCE_SERVERS,
'r_' . AdminAcl::RESOURCE_MOUNTS,
]; ];
/** /**
@ -118,6 +102,7 @@ class ApiKey extends Model
*/ */
protected $attributes = [ protected $attributes = [
'allowed_ips' => '[]', 'allowed_ips' => '[]',
'permissions' => '[]',
]; ];
/** /**
@ -134,24 +119,19 @@ class ApiKey extends Model
'key_type' => 'present|integer|min:0|max:2', 'key_type' => 'present|integer|min:0|max:2',
'identifier' => 'required|string|size:16|unique:api_keys,identifier', 'identifier' => 'required|string|size:16|unique:api_keys,identifier',
'token' => 'required|string', 'token' => 'required|string',
'permissions' => 'array',
'permissions.*' => 'integer|min:0|max:3',
'memo' => 'required|nullable|string|max:500', 'memo' => 'required|nullable|string|max:500',
'allowed_ips' => 'array', 'allowed_ips' => 'array',
'allowed_ips.*' => 'string', 'allowed_ips.*' => 'string',
'last_used_at' => 'nullable|date', 'last_used_at' => 'nullable|date',
'expires_at' => 'nullable|date', 'expires_at' => 'nullable|date',
'r_' . AdminAcl::RESOURCE_USERS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_ALLOCATIONS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_DATABASE_HOSTS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_SERVER_DATABASES => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_EGGS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_NODES => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_SERVERS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_MOUNTS => 'integer|min:0|max:3',
]; ];
protected function casts(): array protected function casts(): array
{ {
return [ return [
'permissions' => 'array',
'allowed_ips' => 'array', 'allowed_ips' => 'array',
'user_id' => 'int', 'user_id' => 'int',
'last_used_at' => 'datetime', 'last_used_at' => 'datetime',
@ -159,14 +139,6 @@ class ApiKey extends Model
'token' => 'encrypted', 'token' => 'encrypted',
self::CREATED_AT => 'datetime', self::CREATED_AT => 'datetime',
self::UPDATED_AT => 'datetime', self::UPDATED_AT => 'datetime',
'r_' . AdminAcl::RESOURCE_USERS => 'int',
'r_' . AdminAcl::RESOURCE_ALLOCATIONS => 'int',
'r_' . AdminAcl::RESOURCE_DATABASE_HOSTS => 'int',
'r_' . AdminAcl::RESOURCE_SERVER_DATABASES => 'int',
'r_' . AdminAcl::RESOURCE_EGGS => 'int',
'r_' . AdminAcl::RESOURCE_NODES => 'int',
'r_' . AdminAcl::RESOURCE_SERVERS => 'int',
'r_' . AdminAcl::RESOURCE_MOUNTS => 'int',
]; ];
} }
@ -188,6 +160,41 @@ class ApiKey extends Model
return $this->user(); return $this->user();
} }
/**
* Returns the permission for the given resource.
*/
public function getPermission(string $resource): int
{
return $this->permissions[$resource] ?? AdminAcl::NONE;
}
public const DEFAULT_RESOURCE_NAMES = [
Server::RESOURCE_NAME,
Node::RESOURCE_NAME,
Allocation::RESOURCE_NAME,
User::RESOURCE_NAME,
Egg::RESOURCE_NAME,
DatabaseHost::RESOURCE_NAME,
Database::RESOURCE_NAME,
Mount::RESOURCE_NAME,
Role::RESOURCE_NAME,
];
private static array $customResourceNames = [];
public static function registerCustomResourceName(string $resourceName): void
{
$customResourceNames[] = $resourceName;
}
/**
* Returns a list of all possible permission keys.
*/
public static function getPermissionList(): array
{
return array_unique(array_merge(self::DEFAULT_RESOURCE_NAMES, self::$customResourceNames));
}
/** /**
* Finds the model matching the provided token. * Finds the model matching the provided token.
*/ */

View File

@ -23,7 +23,7 @@ class Database extends Model
{ {
/** /**
* The resource name for this model when it is transformed into an * The resource name for this model when it is transformed into an
* API representation using fractal. * API representation using fractal. Also used as name for api key permissions.
*/ */
public const RESOURCE_NAME = 'server_database'; public const RESOURCE_NAME = 'server_database';

View File

@ -21,7 +21,7 @@ class DatabaseHost extends Model
{ {
/** /**
* The resource name for this model when it is transformed into an * The resource name for this model when it is transformed into an
* API representation using fractal. * API representation using fractal. Also used as name for api key permissions.
*/ */
public const RESOURCE_NAME = 'database_host'; public const RESOURCE_NAME = 'database_host';

View File

@ -51,7 +51,7 @@ class Egg extends Model
{ {
/** /**
* The resource name for this model when it is transformed into an * The resource name for this model when it is transformed into an
* API representation using fractal. * API representation using fractal. Also used as name for api key permissions.
*/ */
public const RESOURCE_NAME = 'egg'; public const RESOURCE_NAME = 'egg';

View File

@ -49,7 +49,7 @@ class Node extends Model
/** /**
* The resource name for this model when it is transformed into an * The resource name for this model when it is transformed into an
* API representation using fractal. * API representation using fractal. Also used as name for api key permissions.
*/ */
public const RESOURCE_NAME = 'node'; public const RESOURCE_NAME = 'node';

View File

@ -124,7 +124,7 @@ class Server extends Model
/** /**
* The resource name for this model when it is transformed into an * The resource name for this model when it is transformed into an
* API representation using fractal. * API representation using fractal. Also used as name for api key permissions.
*/ */
public const RESOURCE_NAME = 'server'; public const RESOURCE_NAME = 'server';

View File

@ -104,7 +104,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
/** /**
* The resource name for this model when it is transformed into an * The resource name for this model when it is transformed into an
* API representation using fractal. * API representation using fractal. Also used as name for api key permissions.
*/ */
public const RESOURCE_NAME = 'user'; public const RESOURCE_NAME = 'user';

View File

@ -6,12 +6,6 @@ use App\Models\ApiKey;
class AdminAcl class AdminAcl
{ {
/**
* Resource permission columns in the api_keys table begin
* with this identifier.
*/
public const COLUMN_IDENTIFIER = 'r_';
/** /**
* The different types of permissions available for API keys. This * The different types of permissions available for API keys. This
* implements a read/write/none permissions scheme for all endpoints. * implements a read/write/none permissions scheme for all endpoints.
@ -22,28 +16,6 @@ class AdminAcl
public const WRITE = 2; public const WRITE = 2;
/**
* Resources that are available on the API and can contain a permissions
* set for each key. These are stored in the database as r_{resource}.
*/
public const RESOURCE_SERVERS = 'servers';
public const RESOURCE_NODES = 'nodes';
public const RESOURCE_ALLOCATIONS = 'allocations';
public const RESOURCE_USERS = 'users';
public const RESOURCE_EGGS = 'eggs';
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. * Determine if an API key has permission to perform a specific read/write operation.
*/ */
@ -62,20 +34,14 @@ class AdminAcl
*/ */
public static function check(ApiKey $key, string $resource, int $action = self::READ): bool public static function check(ApiKey $key, string $resource, int $action = self::READ): bool
{ {
return self::can(data_get($key, self::COLUMN_IDENTIFIER . $resource, self::NONE), $action); return self::can($key->getPermission($resource), $action);
} }
/** /**
* Return a list of all resource constants defined in this ACL. * Returns a list of all possible permissions.
*
* @throws \ReflectionException
*/ */
public static function getResourceList(): array public static function getResourceList(): array
{ {
$reflect = new \ReflectionClass(__CLASS__); return ApiKey::getPermissionList();
return collect($reflect->getConstants())->filter(function ($value, $key) {
return substr($key, 0, 9) === 'RESOURCE_';
})->values()->toArray();
} }
} }

View File

@ -35,7 +35,7 @@ class KeyCreationService
]); ]);
if ($this->keyType === ApiKey::TYPE_APPLICATION) { if ($this->keyType === ApiKey::TYPE_APPLICATION) {
$data = array_merge($data, $permissions); $data['permissions'] = array_merge($data['permissions'], $permissions);
} }
return ApiKey::query()->forceCreate($data); return ApiKey::query()->forceCreate($data);

View File

@ -4,6 +4,7 @@ namespace App\Services\Nodes;
use App\Models\ApiKey; use App\Models\ApiKey;
use App\Models\Node; use App\Models\Node;
use App\Services\Acl\Api\AdminAcl;
use App\Services\Api\KeyCreationService; use App\Services\Api\KeyCreationService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -28,7 +29,7 @@ class NodeAutoDeployService
/** @var ApiKey|null $key */ /** @var ApiKey|null $key */
$key = ApiKey::query() $key = ApiKey::query()
->where('key_type', ApiKey::TYPE_APPLICATION) ->where('key_type', ApiKey::TYPE_APPLICATION)
->where('r_nodes', true) ->whereJsonContains('permissions->' . Node::RESOURCE_NAME, AdminAcl::READ)
->first(); ->first();
// We couldn't find a key that exists for this user with only permission for // We couldn't find a key that exists for this user with only permission for
@ -37,7 +38,7 @@ class NodeAutoDeployService
$key = $this->keyCreationService->setKeyType(ApiKey::TYPE_APPLICATION)->handle([ $key = $this->keyCreationService->setKeyType(ApiKey::TYPE_APPLICATION)->handle([
'memo' => 'Automatically generated node deployment key.', 'memo' => 'Automatically generated node deployment key.',
'user_id' => $request->user()->id, 'user_id' => $request->user()->id,
], ['r_nodes' => true]); ], ['permissions' => [Node::RESOURCE_NAME => AdminAcl::READ]]);
} }
$token = $key->identifier . $key->token; $token = $key->identifier . $key->token;

View File

@ -7,7 +7,6 @@ use App\Models\Server;
use League\Fractal\Resource\Item; use League\Fractal\Resource\Item;
use App\Models\Allocation; use App\Models\Allocation;
use League\Fractal\Resource\NullResource; use League\Fractal\Resource\NullResource;
use App\Services\Acl\Api\AdminAcl;
class AllocationTransformer extends BaseTransformer class AllocationTransformer extends BaseTransformer
{ {
@ -46,7 +45,7 @@ class AllocationTransformer extends BaseTransformer
*/ */
public function includeNode(Allocation $allocation): Item|NullResource public function includeNode(Allocation $allocation): Item|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_NODES)) { if (!$this->authorize(Node::RESOURCE_NAME)) {
return $this->null(); return $this->null();
} }
@ -64,7 +63,7 @@ class AllocationTransformer extends BaseTransformer
*/ */
public function includeServer(Allocation $allocation): Item|NullResource public function includeServer(Allocation $allocation): Item|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_SERVERS) || !$allocation->server) { if (!$this->authorize(Server::RESOURCE_NAME) || !$allocation->server) {
return $this->null(); return $this->null();
} }

View File

@ -8,7 +8,6 @@ use App\Models\DatabaseHost;
use League\Fractal\Resource\Item; use League\Fractal\Resource\Item;
use League\Fractal\Resource\Collection; use League\Fractal\Resource\Collection;
use League\Fractal\Resource\NullResource; use League\Fractal\Resource\NullResource;
use App\Services\Acl\Api\AdminAcl;
class DatabaseHostTransformer extends BaseTransformer class DatabaseHostTransformer extends BaseTransformer
{ {
@ -49,7 +48,7 @@ class DatabaseHostTransformer extends BaseTransformer
*/ */
public function includeDatabases(DatabaseHost $model): Collection|NullResource public function includeDatabases(DatabaseHost $model): Collection|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_SERVER_DATABASES)) { if (!$this->authorize(Database::RESOURCE_NAME)) {
return $this->null(); return $this->null();
} }
@ -65,7 +64,7 @@ class DatabaseHostTransformer extends BaseTransformer
*/ */
public function includeNode(DatabaseHost $model): Item|NullResource public function includeNode(DatabaseHost $model): Item|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_NODES)) { if (!$this->authorize(Node::RESOURCE_NAME)) {
return $this->null(); return $this->null();
} }

View File

@ -8,7 +8,6 @@ use App\Models\Server;
use App\Models\EggVariable; use App\Models\EggVariable;
use League\Fractal\Resource\Collection; use League\Fractal\Resource\Collection;
use League\Fractal\Resource\NullResource; use League\Fractal\Resource\NullResource;
use App\Services\Acl\Api\AdminAcl;
class EggTransformer extends BaseTransformer class EggTransformer extends BaseTransformer
{ {
@ -83,7 +82,7 @@ class EggTransformer extends BaseTransformer
*/ */
public function includeServers(Egg $model): Collection|NullResource public function includeServers(Egg $model): Collection|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_SERVERS)) { if (!$this->authorize(Server::RESOURCE_NAME)) {
return $this->null(); return $this->null();
} }
@ -99,7 +98,7 @@ class EggTransformer extends BaseTransformer
*/ */
public function includeVariables(Egg $model): Collection|NullResource public function includeVariables(Egg $model): Collection|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_EGGS)) { if (!$this->authorize(Egg::RESOURCE_NAME)) {
return $this->null(); return $this->null();
} }

View File

@ -2,10 +2,12 @@
namespace App\Transformers\Api\Application; namespace App\Transformers\Api\Application;
use App\Models\Egg;
use App\Models\Mount; use App\Models\Mount;
use App\Models\Node;
use App\Models\Server;
use League\Fractal\Resource\Collection; use League\Fractal\Resource\Collection;
use League\Fractal\Resource\NullResource; use League\Fractal\Resource\NullResource;
use App\Services\Acl\Api\AdminAcl;
class MountTransformer extends BaseTransformer class MountTransformer extends BaseTransformer
{ {
@ -34,7 +36,7 @@ class MountTransformer extends BaseTransformer
*/ */
public function includeEggs(Mount $mount): Collection|NullResource public function includeEggs(Mount $mount): Collection|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_EGGS)) { if (!$this->authorize(Egg::RESOURCE_NAME)) {
return $this->null(); return $this->null();
} }
@ -54,7 +56,7 @@ class MountTransformer extends BaseTransformer
*/ */
public function includeNodes(Mount $mount): Collection|NullResource public function includeNodes(Mount $mount): Collection|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_NODES)) { if (!$this->authorize(Node::RESOURCE_NAME)) {
return $this->null(); return $this->null();
} }
@ -74,7 +76,7 @@ class MountTransformer extends BaseTransformer
*/ */
public function includeServers(Mount $mount): Collection|NullResource public function includeServers(Mount $mount): Collection|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_SERVERS)) { if (!$this->authorize(Server::RESOURCE_NAME)) {
return $this->null(); return $this->null();
} }

View File

@ -3,9 +3,10 @@
namespace App\Transformers\Api\Application; namespace App\Transformers\Api\Application;
use App\Models\Node; use App\Models\Node;
use App\Models\Server;
use App\Models\Allocation;
use League\Fractal\Resource\Collection; use League\Fractal\Resource\Collection;
use League\Fractal\Resource\NullResource; use League\Fractal\Resource\NullResource;
use App\Services\Acl\Api\AdminAcl;
class NodeTransformer extends BaseTransformer class NodeTransformer extends BaseTransformer
{ {
@ -52,7 +53,7 @@ class NodeTransformer extends BaseTransformer
*/ */
public function includeAllocations(Node $node): Collection|NullResource public function includeAllocations(Node $node): Collection|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_ALLOCATIONS)) { if (!$this->authorize(Allocation::RESOURCE_NAME)) {
return $this->null(); return $this->null();
} }
@ -72,7 +73,7 @@ class NodeTransformer extends BaseTransformer
*/ */
public function includeServers(Node $node): Collection|NullResource public function includeServers(Node $node): Collection|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_SERVERS)) { if (!$this->authorize(Server::RESOURCE_NAME)) {
return $this->null(); return $this->null();
} }

View File

@ -6,7 +6,6 @@ use App\Models\Database;
use League\Fractal\Resource\Item; use League\Fractal\Resource\Item;
use App\Models\DatabaseHost; use App\Models\DatabaseHost;
use League\Fractal\Resource\NullResource; use League\Fractal\Resource\NullResource;
use App\Services\Acl\Api\AdminAcl;
class ServerDatabaseTransformer extends BaseTransformer class ServerDatabaseTransformer extends BaseTransformer
{ {
@ -57,7 +56,7 @@ class ServerDatabaseTransformer extends BaseTransformer
*/ */
public function includeHost(Database $model): Item|NullResource public function includeHost(Database $model): Item|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_DATABASE_HOSTS)) { if (!$this->authorize(DatabaseHost::RESOURCE_NAME)) {
return $this->null(); return $this->null();
} }

View File

@ -3,10 +3,14 @@
namespace App\Transformers\Api\Application; namespace App\Transformers\Api\Application;
use App\Models\Server; use App\Models\Server;
use App\Models\Node;
use App\Models\User;
use App\Models\Egg;
use App\Models\Database;
use App\Models\Allocation;
use League\Fractal\Resource\Item; use League\Fractal\Resource\Item;
use League\Fractal\Resource\Collection; use League\Fractal\Resource\Collection;
use League\Fractal\Resource\NullResource; use League\Fractal\Resource\NullResource;
use App\Services\Acl\Api\AdminAcl;
use App\Services\Servers\EnvironmentService; use App\Services\Servers\EnvironmentService;
class ServerTransformer extends BaseTransformer class ServerTransformer extends BaseTransformer
@ -97,7 +101,7 @@ class ServerTransformer extends BaseTransformer
*/ */
public function includeAllocations(Server $server): Collection|NullResource public function includeAllocations(Server $server): Collection|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_ALLOCATIONS)) { if (!$this->authorize(Allocation::RESOURCE_NAME)) {
return $this->null(); return $this->null();
} }
@ -113,7 +117,7 @@ class ServerTransformer extends BaseTransformer
*/ */
public function includeSubusers(Server $server): Collection|NullResource public function includeSubusers(Server $server): Collection|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_USERS)) { if (!$this->authorize(User::RESOURCE_NAME)) {
return $this->null(); return $this->null();
} }
@ -129,7 +133,7 @@ class ServerTransformer extends BaseTransformer
*/ */
public function includeUser(Server $server): Item|NullResource public function includeUser(Server $server): Item|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_USERS)) { if (!$this->authorize(User::RESOURCE_NAME)) {
return $this->null(); return $this->null();
} }
@ -145,7 +149,7 @@ class ServerTransformer extends BaseTransformer
*/ */
public function includeEgg(Server $server): Item|NullResource public function includeEgg(Server $server): Item|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_EGGS)) { if (!$this->authorize(Egg::RESOURCE_NAME)) {
return $this->null(); return $this->null();
} }
@ -161,7 +165,7 @@ class ServerTransformer extends BaseTransformer
*/ */
public function includeVariables(Server $server): Collection|NullResource public function includeVariables(Server $server): Collection|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_SERVERS)) { if (!$this->authorize(Server::RESOURCE_NAME)) {
return $this->null(); return $this->null();
} }
@ -177,7 +181,7 @@ class ServerTransformer extends BaseTransformer
*/ */
public function includeNode(Server $server): Item|NullResource public function includeNode(Server $server): Item|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_NODES)) { if (!$this->authorize(Node::RESOURCE_NAME)) {
return $this->null(); return $this->null();
} }
@ -193,7 +197,7 @@ class ServerTransformer extends BaseTransformer
*/ */
public function includeDatabases(Server $server): Collection|NullResource public function includeDatabases(Server $server): Collection|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_SERVER_DATABASES)) { if (!$this->authorize(Database::RESOURCE_NAME)) {
return $this->null(); return $this->null();
} }

View File

@ -4,8 +4,8 @@ namespace App\Transformers\Api\Application;
use League\Fractal\Resource\Item; use League\Fractal\Resource\Item;
use App\Models\EggVariable; use App\Models\EggVariable;
use App\Models\Egg;
use League\Fractal\Resource\NullResource; use League\Fractal\Resource\NullResource;
use App\Services\Acl\Api\AdminAcl;
class ServerVariableTransformer extends BaseTransformer class ServerVariableTransformer extends BaseTransformer
{ {
@ -37,7 +37,7 @@ class ServerVariableTransformer extends BaseTransformer
*/ */
public function includeParent(EggVariable $variable): Item|NullResource public function includeParent(EggVariable $variable): Item|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_EGGS)) { if (!$this->authorize(Egg::RESOURCE_NAME)) {
return $this->null(); return $this->null();
} }

View File

@ -3,9 +3,10 @@
namespace App\Transformers\Api\Application; namespace App\Transformers\Api\Application;
use App\Models\Subuser; use App\Models\Subuser;
use App\Models\User;
use App\Models\Server;
use League\Fractal\Resource\Item; use League\Fractal\Resource\Item;
use League\Fractal\Resource\NullResource; use League\Fractal\Resource\NullResource;
use App\Services\Acl\Api\AdminAcl;
class SubuserTransformer extends BaseTransformer class SubuserTransformer extends BaseTransformer
{ {
@ -44,7 +45,7 @@ class SubuserTransformer extends BaseTransformer
*/ */
public function includeUser(Subuser $subuser): Item|NullResource public function includeUser(Subuser $subuser): Item|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_USERS)) { if (!$this->authorize(User::RESOURCE_NAME)) {
return $this->null(); return $this->null();
} }
@ -60,7 +61,7 @@ class SubuserTransformer extends BaseTransformer
*/ */
public function includeServer(Subuser $subuser): Item|NullResource public function includeServer(Subuser $subuser): Item|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_SERVERS)) { if (!$this->authorize(Server::RESOURCE_NAME)) {
return $this->null(); return $this->null();
} }

View File

@ -4,9 +4,9 @@ namespace App\Transformers\Api\Application;
use App\Models\Role; use App\Models\Role;
use App\Models\User; use App\Models\User;
use App\Models\Server;
use League\Fractal\Resource\Collection; use League\Fractal\Resource\Collection;
use League\Fractal\Resource\NullResource; use League\Fractal\Resource\NullResource;
use App\Services\Acl\Api\AdminAcl;
class UserTransformer extends BaseTransformer class UserTransformer extends BaseTransformer
{ {
@ -55,7 +55,7 @@ class UserTransformer extends BaseTransformer
*/ */
public function includeServers(User $user): Collection|NullResource public function includeServers(User $user): Collection|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_SERVERS)) { if (!$this->authorize(Server::RESOURCE_NAME)) {
return $this->null(); return $this->null();
} }
@ -71,7 +71,7 @@ class UserTransformer extends BaseTransformer
*/ */
public function includeRoles(User $user): Collection|NullResource public function includeRoles(User $user): Collection|NullResource
{ {
if (!$this->authorize(AdminAcl::RESOURCE_ROLES)) { if (!$this->authorize(Role::RESOURCE_NAME)) {
return $this->null(); return $this->null();
} }

View File

@ -28,6 +28,7 @@ class ApiKeyFactory extends Factory
'identifier' => ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION), 'identifier' => ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION),
'token' => $token ?: $token = Str::random(ApiKey::KEY_LENGTH), 'token' => $token ?: $token = Str::random(ApiKey::KEY_LENGTH),
'allowed_ips' => [], 'allowed_ips' => [],
'permissions' => [],
'memo' => 'Test Function Key', 'memo' => 'Test Function Key',
'created_at' => Carbon::now(), 'created_at' => Carbon::now(),
'updated_at' => Carbon::now(), 'updated_at' => Carbon::now(),

View File

@ -0,0 +1,94 @@
<?php
use App\Models\Allocation;
use App\Models\ApiKey;
use App\Models\Database;
use App\Models\DatabaseHost;
use App\Models\Egg;
use App\Models\Mount;
use App\Models\Node;
use App\Models\Server;
use App\Models\User;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('api_keys', function (Blueprint $table) {
$table->json('permissions');
});
foreach (ApiKey::query() as $apiKey) {
$permissions = [
Server::RESOURCE_NAME => intval($apiKey->r_servers ?? 0),
Node::RESOURCE_NAME => intval($apiKey->r_nodes ?? 0),
Allocation::RESOURCE_NAME => intval($apiKey->r_allocations ?? 0),
User::RESOURCE_NAME => intval($apiKey->r_users ?? 0),
Egg::RESOURCE_NAME => intval($apiKey->r_eggs ?? 0),
DatabaseHost::RESOURCE_NAME => intval($apiKey->r_database_hosts ?? 0),
Database::RESOURCE_NAME => intval($apiKey->r_server_databases ?? 0),
Mount::RESOURCE_NAME => intval($apiKey->r_mounts ?? 0),
];
DB::table('api_keys')
->where('id', $apiKey->id)
->update(['permissions' => $permissions]);
}
Schema::table('api_keys', function (Blueprint $table) {
$table->dropColumn([
'r_servers',
'r_nodes',
'r_allocations',
'r_users',
'r_eggs',
'r_database_hosts',
'r_server_databases',
'r_mounts',
]);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('api_keys', function (Blueprint $table) {
$table->unsignedTinyInteger('r_servers')->default(0);
$table->unsignedTinyInteger('r_nodes')->default(0);
$table->unsignedTinyInteger('r_allocations')->default(0);
$table->unsignedTinyInteger('r_users')->default(0);
$table->unsignedTinyInteger('r_eggs')->default(0);
$table->unsignedTinyInteger('r_database_hosts')->default(0);
$table->unsignedTinyInteger('r_server_databases')->default(0);
$table->unsignedTinyInteger('r_mounts')->default(0);
});
foreach (ApiKey::query() as $apiKey) {
DB::table('api_keys')
->where('id', $apiKey->id)
->update([
'r_servers' => $apiKey->permissions[Server::RESOURCE_NAME],
'r_nodes' => $apiKey->permissions[Node::RESOURCE_NAME],
'r_allocations' => $apiKey->permissions[Allocation::RESOURCE_NAME],
'r_users' => $apiKey->permissions[User::RESOURCE_NAME],
'r_eggs' => $apiKey->permissions[Egg::RESOURCE_NAME],
'r_database_hosts' => $apiKey->permissions[DatabaseHost::RESOURCE_NAME],
'r_server_databases' => $apiKey->permissions[Database::RESOURCE_NAME],
'r_mounts' => $apiKey->permissions[Mount::RESOURCE_NAME],
]);
}
Schema::table('api_keys', function (Blueprint $table) {
$table->dropColumn('permissions');
});
}
};

View File

@ -2,10 +2,17 @@
namespace App\Tests\Integration\Api\Application; namespace App\Tests\Integration\Api\Application;
use App\Models\Allocation;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Models\User; use App\Models\User;
use PHPUnit\Framework\Assert; use PHPUnit\Framework\Assert;
use App\Models\ApiKey; use App\Models\ApiKey;
use App\Models\Database;
use App\Models\DatabaseHost;
use App\Models\Egg;
use App\Models\Mount;
use App\Models\Node;
use App\Models\Server;
use App\Models\Role; use App\Models\Role;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Tests\Integration\IntegrationTestCase; use App\Tests\Integration\IntegrationTestCase;
@ -79,18 +86,21 @@ abstract class ApplicationApiIntegrationTestCase extends IntegrationTestCase
*/ */
protected function createApiKey(User $user, array $permissions = []): ApiKey protected function createApiKey(User $user, array $permissions = []): ApiKey
{ {
return ApiKey::factory()->create(array_merge([ return ApiKey::factory()->create([
'user_id' => $user->id, 'user_id' => $user->id,
'key_type' => ApiKey::TYPE_APPLICATION, 'key_type' => ApiKey::TYPE_APPLICATION,
'r_servers' => AdminAcl::READ | AdminAcl::WRITE, 'permissions' => array_merge([
'r_nodes' => AdminAcl::READ | AdminAcl::WRITE, Server::RESOURCE_NAME => AdminAcl::READ | AdminAcl::WRITE,
'r_allocations' => AdminAcl::READ | AdminAcl::WRITE, Node::RESOURCE_NAME => AdminAcl::READ | AdminAcl::WRITE,
'r_users' => AdminAcl::READ | AdminAcl::WRITE, Allocation::RESOURCE_NAME => AdminAcl::READ | AdminAcl::WRITE,
'r_eggs' => AdminAcl::READ | AdminAcl::WRITE, User::RESOURCE_NAME => AdminAcl::READ | AdminAcl::WRITE,
'r_database_hosts' => AdminAcl::READ | AdminAcl::WRITE, Egg::RESOURCE_NAME => AdminAcl::READ | AdminAcl::WRITE,
'r_server_databases' => AdminAcl::READ | AdminAcl::WRITE, DatabaseHost::RESOURCE_NAME => AdminAcl::READ | AdminAcl::WRITE,
'r_mounts' => AdminAcl::READ | AdminAcl::WRITE, Database::RESOURCE_NAME => AdminAcl::READ | AdminAcl::WRITE,
], $permissions)); Mount::RESOURCE_NAME => AdminAcl::READ | AdminAcl::WRITE,
Role::RESOURCE_NAME => AdminAcl::READ | AdminAcl::WRITE,
], $permissions),
]);
} }
/** /**

View File

@ -3,6 +3,7 @@
namespace App\Tests\Integration\Api\Application; namespace App\Tests\Integration\Api\Application;
use App\Models\Egg; use App\Models\Egg;
use App\Services\Acl\Api\AdminAcl;
use App\Transformers\Api\Application\EggTransformer; use App\Transformers\Api\Application\EggTransformer;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
@ -111,7 +112,7 @@ class EggControllerTest extends ApplicationApiIntegrationTestCase
public function testErrorReturnedIfNoPermission(): void public function testErrorReturnedIfNoPermission(): void
{ {
$egg = Egg::query()->findOrFail(1); $egg = Egg::query()->findOrFail(1);
$this->createNewDefaultApiKey($this->getApiUser(), ['r_eggs' => 0]); $this->createNewDefaultApiKey($this->getApiUser(), [Egg::RESOURCE_NAME => AdminAcl::NONE]);
$response = $this->getJson('/api/application/eggs'); $response = $this->getJson('/api/application/eggs');
$this->assertAccessDeniedJson($response); $this->assertAccessDeniedJson($response);

View File

@ -4,6 +4,7 @@ namespace App\Tests\Integration\Api\Application\Users;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use App\Models\User; use App\Models\User;
use App\Services\Acl\Api\AdminAcl;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use App\Tests\Integration\Api\Application\ApplicationApiIntegrationTestCase; use App\Tests\Integration\Api\Application\ApplicationApiIntegrationTestCase;
@ -62,7 +63,7 @@ class ExternalUserControllerTest extends ApplicationApiIntegrationTestCase
public function testErrorReturnedIfNoPermission(): void public function testErrorReturnedIfNoPermission(): void
{ {
$user = User::factory()->create(['external_id' => Str::random()]); $user = User::factory()->create(['external_id' => Str::random()]);
$this->createNewDefaultApiKey($this->getApiUser(), ['r_users' => 0]); $this->createNewDefaultApiKey($this->getApiUser(), [User::RESOURCE_NAME => AdminAcl::NONE]);
$response = $this->getJson('/api/application/users/external/' . $user->external_id); $response = $this->getJson('/api/application/users/external/' . $user->external_id);
$this->assertAccessDeniedJson($response); $this->assertAccessDeniedJson($response);

View File

@ -2,6 +2,7 @@
namespace App\Tests\Integration\Api\Application\Users; namespace App\Tests\Integration\Api\Application\Users;
use App\Models\Server;
use App\Models\User; use App\Models\User;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
@ -152,7 +153,7 @@ class UserControllerTest extends ApplicationApiIntegrationTestCase
*/ */
public function testKeyWithoutPermissionCannotLoadRelationship(): void public function testKeyWithoutPermissionCannotLoadRelationship(): void
{ {
$this->createNewDefaultApiKey($this->getApiUser(), ['r_servers' => 0]); $this->createNewDefaultApiKey($this->getApiUser(), [Server::RESOURCE_NAME => AdminAcl::NONE]);
$user = User::factory()->create(); $user = User::factory()->create();
$this->createServerModel(['user_id' => $user->id]); $this->createServerModel(['user_id' => $user->id]);
@ -197,7 +198,7 @@ class UserControllerTest extends ApplicationApiIntegrationTestCase
public function testErrorReturnedIfNoPermission(): void public function testErrorReturnedIfNoPermission(): void
{ {
$user = User::factory()->create(); $user = User::factory()->create();
$this->createNewDefaultApiKey($this->getApiUser(), ['r_users' => 0]); $this->createNewDefaultApiKey($this->getApiUser(), [User::RESOURCE_NAME => AdminAcl::NONE]);
$response = $this->getJson('/api/application/users/' . $user->id); $response = $this->getJson('/api/application/users/' . $user->id);
$this->assertAccessDeniedJson($response); $this->assertAccessDeniedJson($response);
@ -286,7 +287,7 @@ class UserControllerTest extends ApplicationApiIntegrationTestCase
*/ */
public function testApiKeyWithoutWritePermissions(string $method, string $url): void public function testApiKeyWithoutWritePermissions(string $method, string $url): void
{ {
$this->createNewDefaultApiKey($this->getApiUser(), ['r_users' => AdminAcl::READ]); $this->createNewDefaultApiKey($this->getApiUser(), [User::RESOURCE_NAME => AdminAcl::READ]);
if (str_contains($url, '{id}')) { if (str_contains($url, '{id}')) {
$user = User::factory()->create(); $user = User::factory()->create();

View File

@ -5,6 +5,7 @@ namespace App\Tests\Unit\Services\Acl\Api;
use App\Models\ApiKey; use App\Models\ApiKey;
use App\Tests\TestCase; use App\Tests\TestCase;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Models\Server;
class AdminAclTest extends TestCase class AdminAclTest extends TestCase
{ {
@ -23,9 +24,11 @@ class AdminAclTest extends TestCase
*/ */
public function testCheck(): void public function testCheck(): void
{ {
$model = ApiKey::factory()->make(['r_servers' => AdminAcl::READ | AdminAcl::WRITE]); $model = ApiKey::factory()->make(['permissions' => [
Server::RESOURCE_NAME => AdminAcl::READ | AdminAcl::WRITE,
]]);
$this->assertTrue(AdminAcl::check($model, AdminAcl::RESOURCE_SERVERS, AdminAcl::WRITE)); $this->assertTrue(AdminAcl::check($model, Server::RESOURCE_NAME, AdminAcl::WRITE));
} }
/** /**