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\Form;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model;
class CreateApiKey extends CreateRecord
{
@ -41,7 +42,7 @@ class CreateApiKey extends CreateRecord
'md' => 2,
])
->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()
->options([
0 => 'None',
@ -87,4 +88,20 @@ class CreateApiKey extends CreateRecord
->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;
use App\Models\ApiKey;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Admin\AdminFormRequest;
class StoreApplicationApiKeyRequest extends AdminFormRequest
@ -16,9 +15,12 @@ class StoreApplicationApiKeyRequest extends AdminFormRequest
{
$modelRules = ApiKey::getRules();
return collect(AdminAcl::getResourceList())->mapWithKeys(function ($resource) use ($modelRules) {
return [AdminAcl::COLUMN_IDENTIFIER . $resource => $modelRules['r_' . $resource]];
})->merge(['memo' => $modelRules['memo']])->toArray();
$rules = [
'memo' => $modelRules['memo'],
'permissions' => $modelRules['permissions'],
];
return $rules;
}
public function attributes(): array
@ -30,8 +32,8 @@ class StoreApplicationApiKeyRequest extends AdminFormRequest
public function getKeyPermissions(): array
{
return collect($this->validated())->filter(function ($value, $key) {
return substr($key, 0, strlen(AdminAcl::COLUMN_IDENTIFIER)) === AdminAcl::COLUMN_IDENTIFIER;
})->toArray();
$data = $this->validated();
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\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Allocation;
class DeleteAllocationRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_ALLOCATIONS;
protected ?string $resource = Allocation::RESOURCE_NAME;
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\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Allocation;
class GetAllocationsRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_ALLOCATIONS;
protected ?string $resource = Allocation::RESOURCE_NAME;
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\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Allocation;
class StoreAllocationRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_ALLOCATIONS;
protected ?string $resource = Allocation::RESOURCE_NAME;
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\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\DatabaseHost;
class DeleteDatabaseHostRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_DATABASE_HOSTS;
protected ?string $resource = DatabaseHost::RESOURCE_NAME;
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\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\DatabaseHost;
class GetDatabaseHostRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_DATABASE_HOSTS;
protected ?string $resource = DatabaseHost::RESOURCE_NAME;
protected int $permission = AdminAcl::READ;
}

View File

@ -8,7 +8,7 @@ use App\Http\Requests\Api\Application\ApplicationApiRequest;
class StoreDatabaseHostRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_DATABASE_HOSTS;
protected ?string $resource = DatabaseHost::RESOURCE_NAME;
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\Services\Acl\Api\AdminAcl;
use App\Models\Egg;
class GetEggRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_EGGS;
protected ?string $resource = Egg::RESOURCE_NAME;
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\Services\Acl\Api\AdminAcl;
use App\Models\Egg;
class GetEggsRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_EGGS;
protected ?string $resource = Egg::RESOURCE_NAME;
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\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Mount;
class DeleteMountRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_MOUNTS;
protected ?string $resource = Mount::RESOURCE_NAME;
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\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Mount;
class GetMountRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_MOUNTS;
protected ?string $resource = Mount::RESOURCE_NAME;
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\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Mount;
class StoreMountRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_MOUNTS;
protected ?string $resource = Mount::RESOURCE_NAME;
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\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Node;
class DeleteNodeRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_NODES;
protected ?string $resource = Node::RESOURCE_NAME;
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\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Node;
class GetNodesRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_NODES;
protected ?string $resource = Node::RESOURCE_NAME;
protected int $permission = AdminAcl::READ;
}

View File

@ -8,7 +8,7 @@ use App\Http\Requests\Api\Application\ApplicationApiRequest;
class StoreNodeRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_NODES;
protected ?string $resource = Node::RESOURCE_NAME;
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\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Role;
class DeleteRoleRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_ROLES;
protected ?string $resource = Role::RESOURCE_NAME;
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\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Role;
class GetRoleRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_ROLES;
protected ?string $resource = Role::RESOURCE_NAME;
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\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Role;
class StoreRoleRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_ROLES;
protected ?string $resource = Role::RESOURCE_NAME;
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\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Database;
class GetServerDatabaseRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_SERVER_DATABASES;
protected ?string $resource = Database::RESOURCE_NAME;
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\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Database;
class GetServerDatabasesRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_SERVER_DATABASES;
protected ?string $resource = Database::RESOURCE_NAME;
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\Databases\DatabaseManagementService;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Database;
class StoreServerDatabaseRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_SERVER_DATABASES;
protected ?string $resource = Database::RESOURCE_NAME;
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\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Server;
class GetExternalServerRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_SERVERS;
protected ?string $resource = Server::RESOURCE_NAME;
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\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Server;
class GetServerRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_SERVERS;
protected ?string $resource = Server::RESOURCE_NAME;
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\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Server;
class ServerWriteRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_SERVERS;
protected ?string $resource = Server::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE;
}

View File

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

View File

@ -8,7 +8,7 @@ use App\Http\Requests\Api\Application\ApplicationApiRequest;
class UpdateServerStartupRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_SERVERS;
protected ?string $resource = Server::RESOURCE_NAME;
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\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\User;
class DeleteUserRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_USERS;
protected ?string $resource = User::RESOURCE_NAME;
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\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\User;
class GetExternalUserRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_USERS;
protected ?string $resource = User::RESOURCE_NAME;
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\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\User;
class GetUsersRequest extends ApplicationApiRequest
{
protected ?string $resource = Acl::RESOURCE_USERS;
protected ?string $resource = User::RESOURCE_NAME;
protected int $permission = Acl::READ;
}

View File

@ -8,7 +8,7 @@ use App\Http\Requests\Api\Application\ApplicationApiRequest;
class StoreUserRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_USERS;
protected ?string $resource = User::RESOURCE_NAME;
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
* API representation using fractal.
* API representation using fractal. Also used as name for api key permissions.
*/
public const RESOURCE_NAME = 'allocation';

View File

@ -2,9 +2,9 @@
namespace App\Models;
use App\Services\Acl\Api\AdminAcl;
use Illuminate\Support\Str;
use Webmozart\Assert\Assert;
use App\Services\Acl\Api\AdminAcl;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
@ -15,20 +15,13 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property int $key_type
* @property string $identifier
* @property string $token
* @property array $permissions
* @property array $allowed_ips
* @property string|null $memo
* @property \Illuminate\Support\Carbon|null $last_used_at
* @property \Illuminate\Support\Carbon|null $expires_at
* @property \Illuminate\Support\Carbon|null $created_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 $user
*
@ -84,8 +77,6 @@ class ApiKey extends Model
*/
public const KEY_LENGTH = 32;
public const RESOURCES = ['servers', 'nodes', 'allocations', 'users', 'eggs', 'database_hosts', 'server_databases', 'mounts'];
/**
* The table associated with the model.
*/
@ -99,18 +90,11 @@ class ApiKey extends Model
'key_type',
'identifier',
'token',
'permissions',
'allowed_ips',
'memo',
'last_used_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 = [
'allowed_ips' => '[]',
'permissions' => '[]',
];
/**
@ -134,24 +119,19 @@ class ApiKey extends Model
'key_type' => 'present|integer|min:0|max:2',
'identifier' => 'required|string|size:16|unique:api_keys,identifier',
'token' => 'required|string',
'permissions' => 'array',
'permissions.*' => 'integer|min:0|max:3',
'memo' => 'required|nullable|string|max:500',
'allowed_ips' => 'array',
'allowed_ips.*' => 'string',
'last_used_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
{
return [
'permissions' => 'array',
'allowed_ips' => 'array',
'user_id' => 'int',
'last_used_at' => 'datetime',
@ -159,14 +139,6 @@ class ApiKey extends Model
'token' => 'encrypted',
self::CREATED_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();
}
/**
* 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.
*/

View File

@ -23,7 +23,7 @@ class Database extends Model
{
/**
* 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';

View File

@ -21,7 +21,7 @@ class DatabaseHost extends Model
{
/**
* 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';

View File

@ -51,7 +51,7 @@ class Egg extends Model
{
/**
* 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';

View File

@ -49,7 +49,7 @@ class Node extends Model
/**
* 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';

View File

@ -124,7 +124,7 @@ class Server extends Model
/**
* 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';

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
* API representation using fractal.
* API representation using fractal. Also used as name for api key permissions.
*/
public const RESOURCE_NAME = 'user';

View File

@ -6,12 +6,6 @@ use App\Models\ApiKey;
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
* implements a read/write/none permissions scheme for all endpoints.
@ -22,28 +16,6 @@ class AdminAcl
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.
*/
@ -62,20 +34,14 @@ class AdminAcl
*/
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.
*
* @throws \ReflectionException
* Returns a list of all possible permissions.
*/
public static function getResourceList(): array
{
$reflect = new \ReflectionClass(__CLASS__);
return collect($reflect->getConstants())->filter(function ($value, $key) {
return substr($key, 0, 9) === 'RESOURCE_';
})->values()->toArray();
return ApiKey::getPermissionList();
}
}

View File

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

View File

@ -4,6 +4,7 @@ namespace App\Services\Nodes;
use App\Models\ApiKey;
use App\Models\Node;
use App\Services\Acl\Api\AdminAcl;
use App\Services\Api\KeyCreationService;
use Illuminate\Http\Request;
@ -28,7 +29,7 @@ class NodeAutoDeployService
/** @var ApiKey|null $key */
$key = ApiKey::query()
->where('key_type', ApiKey::TYPE_APPLICATION)
->where('r_nodes', true)
->whereJsonContains('permissions->' . Node::RESOURCE_NAME, AdminAcl::READ)
->first();
// 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([
'memo' => 'Automatically generated node deployment key.',
'user_id' => $request->user()->id,
], ['r_nodes' => true]);
], ['permissions' => [Node::RESOURCE_NAME => AdminAcl::READ]]);
}
$token = $key->identifier . $key->token;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,6 +28,7 @@ class ApiKeyFactory extends Factory
'identifier' => ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION),
'token' => $token ?: $token = Str::random(ApiKey::KEY_LENGTH),
'allowed_ips' => [],
'permissions' => [],
'memo' => 'Test Function Key',
'created_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;
use App\Models\Allocation;
use Illuminate\Http\Request;
use App\Models\User;
use PHPUnit\Framework\Assert;
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\Services\Acl\Api\AdminAcl;
use App\Tests\Integration\IntegrationTestCase;
@ -79,18 +86,21 @@ abstract class ApplicationApiIntegrationTestCase extends IntegrationTestCase
*/
protected function createApiKey(User $user, array $permissions = []): ApiKey
{
return ApiKey::factory()->create(array_merge([
return ApiKey::factory()->create([
'user_id' => $user->id,
'key_type' => ApiKey::TYPE_APPLICATION,
'r_servers' => AdminAcl::READ | AdminAcl::WRITE,
'r_nodes' => AdminAcl::READ | AdminAcl::WRITE,
'r_allocations' => AdminAcl::READ | AdminAcl::WRITE,
'r_users' => AdminAcl::READ | AdminAcl::WRITE,
'r_eggs' => AdminAcl::READ | AdminAcl::WRITE,
'r_database_hosts' => AdminAcl::READ | AdminAcl::WRITE,
'r_server_databases' => AdminAcl::READ | AdminAcl::WRITE,
'r_mounts' => AdminAcl::READ | AdminAcl::WRITE,
], $permissions));
'permissions' => array_merge([
Server::RESOURCE_NAME => AdminAcl::READ | AdminAcl::WRITE,
Node::RESOURCE_NAME => AdminAcl::READ | AdminAcl::WRITE,
Allocation::RESOURCE_NAME => AdminAcl::READ | AdminAcl::WRITE,
User::RESOURCE_NAME => AdminAcl::READ | AdminAcl::WRITE,
Egg::RESOURCE_NAME => AdminAcl::READ | AdminAcl::WRITE,
DatabaseHost::RESOURCE_NAME => AdminAcl::READ | AdminAcl::WRITE,
Database::RESOURCE_NAME => AdminAcl::READ | AdminAcl::WRITE,
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;
use App\Models\Egg;
use App\Services\Acl\Api\AdminAcl;
use App\Transformers\Api\Application\EggTransformer;
use Illuminate\Http\Response;
use Illuminate\Support\Arr;
@ -111,7 +112,7 @@ class EggControllerTest extends ApplicationApiIntegrationTestCase
public function testErrorReturnedIfNoPermission(): void
{
$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');
$this->assertAccessDeniedJson($response);

View File

@ -4,6 +4,7 @@ namespace App\Tests\Integration\Api\Application\Users;
use Illuminate\Support\Str;
use App\Models\User;
use App\Services\Acl\Api\AdminAcl;
use Illuminate\Http\Response;
use App\Tests\Integration\Api\Application\ApplicationApiIntegrationTestCase;
@ -62,7 +63,7 @@ class ExternalUserControllerTest extends ApplicationApiIntegrationTestCase
public function testErrorReturnedIfNoPermission(): void
{
$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);
$this->assertAccessDeniedJson($response);

View File

@ -2,6 +2,7 @@
namespace App\Tests\Integration\Api\Application\Users;
use App\Models\Server;
use App\Models\User;
use Illuminate\Http\Response;
use App\Services\Acl\Api\AdminAcl;
@ -152,7 +153,7 @@ class UserControllerTest extends ApplicationApiIntegrationTestCase
*/
public function testKeyWithoutPermissionCannotLoadRelationship(): void
{
$this->createNewDefaultApiKey($this->getApiUser(), ['r_servers' => 0]);
$this->createNewDefaultApiKey($this->getApiUser(), [Server::RESOURCE_NAME => AdminAcl::NONE]);
$user = User::factory()->create();
$this->createServerModel(['user_id' => $user->id]);
@ -197,7 +198,7 @@ class UserControllerTest extends ApplicationApiIntegrationTestCase
public function testErrorReturnedIfNoPermission(): void
{
$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);
$this->assertAccessDeniedJson($response);
@ -286,7 +287,7 @@ class UserControllerTest extends ApplicationApiIntegrationTestCase
*/
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}')) {
$user = User::factory()->create();

View File

@ -5,6 +5,7 @@ namespace App\Tests\Unit\Services\Acl\Api;
use App\Models\ApiKey;
use App\Tests\TestCase;
use App\Services\Acl\Api\AdminAcl;
use App\Models\Server;
class AdminAclTest extends TestCase
{
@ -23,9 +24,11 @@ class AdminAclTest extends TestCase
*/
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));
}
/**