diff --git a/app/Models/PersonalAccessToken.php b/app/Models/PersonalAccessToken.php index cd4720424..5cc7c3b30 100644 --- a/app/Models/PersonalAccessToken.php +++ b/app/Models/PersonalAccessToken.php @@ -4,9 +4,12 @@ namespace Pterodactyl\Models; use Illuminate\Support\Str; use Laravel\Sanctum\Contracts\HasAbilities; +use Illuminate\Database\Eloquent\Factories\HasFactory; class PersonalAccessToken extends Model implements HasAbilities { + use HasFactory; + public const RESOURCE_NAME = 'personal_access_token'; /** diff --git a/app/Services/Acl/Api/AdminAcl.php b/app/Services/Acl/Api/AdminAcl.php index 99c2d344f..547e43890 100644 --- a/app/Services/Acl/Api/AdminAcl.php +++ b/app/Services/Acl/Api/AdminAcl.php @@ -2,17 +2,8 @@ namespace Pterodactyl\Services\Acl\Api; -use ReflectionClass; -use Pterodactyl\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. @@ -36,48 +27,4 @@ class AdminAcl public const RESOURCE_SERVER_DATABASES = 'server_databases'; public const RESOURCE_ROLES = 'roles'; public const RESOURCE_MOUNTS = 'mounts'; - - /** - * Determine if an API key has permission to perform a specific read/write operation. - * - * @param int $permission - * @param int $action - * @return bool - */ - public static function can(int $permission, int $action = self::READ) - { - if ($permission & $action) { - return true; - } - - return false; - } - - /** - * Determine if an API Key model has permission to access a given resource - * at a specific action level. - * - * @param \Pterodactyl\Models\ApiKey $key - * @param string $resource - * @param int $action - * @return bool - */ - public static function check(ApiKey $key, string $resource, int $action = self::READ) - { - return self::can(data_get($key, self::COLUMN_IDENTIFIER . $resource, self::NONE), $action); - } - - /** - * Return a list of all resource constants defined in this ACL. - * - * @return array - */ - 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(); - } } diff --git a/database/Factories/PersonalAccessTokenFactory.php b/database/Factories/PersonalAccessTokenFactory.php new file mode 100644 index 000000000..0d64d4cc8 --- /dev/null +++ b/database/Factories/PersonalAccessTokenFactory.php @@ -0,0 +1,28 @@ + PersonalAccessToken::generateTokenIdentifier(), + 'token' => hash('sha256', Str::random(PersonalAccessToken::TOKEN_LENGTH)), + 'description' => 'Generated test token', + 'abilities' => ['*'], + ]; + } +} diff --git a/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php b/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php index 0be671760..e7d2c1019 100644 --- a/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php +++ b/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php @@ -34,7 +34,7 @@ abstract class ApplicationApiIntegrationTestCase extends IntegrationTestCase { parent::setUp(); - $this->user = $this->createApiUser(); + $this->user = User::factory()->create(['root_admin' => true]); $this->key = $this->createApiKey($this->user); $this->withHeader('Accept', 'application/vnd.pterodactyl.v1+json'); @@ -85,18 +85,6 @@ abstract class ApplicationApiIntegrationTestCase extends IntegrationTestCase $this->withHeader('Authorization', 'Bearer ' . $key->identifier . decrypt($key->token)); } - /** - * Create an administrative user. - * - * @return \Pterodactyl\Models\User - */ - protected function createApiUser(): User - { - return User::factory()->create([ - 'root_admin' => true, - ]); - } - /** * Create a new application API key for a given user model. * diff --git a/tests/Integration/Api/Client/ApiKeyControllerTest.php b/tests/Integration/Api/Client/ApiKeyControllerTest.php index 77bb8e1a6..5d736ecaf 100644 --- a/tests/Integration/Api/Client/ApiKeyControllerTest.php +++ b/tests/Integration/Api/Client/ApiKeyControllerTest.php @@ -4,7 +4,8 @@ namespace Pterodactyl\Tests\Integration\Api\Client; use Pterodactyl\Models\User; use Illuminate\Http\Response; -use Pterodactyl\Models\ApiKey; +use Pterodactyl\Models\PersonalAccessToken; +use Pterodactyl\Transformers\Api\Client\PersonalAccessTokenTransformer; class ApiKeyControllerTest extends ClientApiIntegrationTestCase { @@ -13,7 +14,7 @@ class ApiKeyControllerTest extends ClientApiIntegrationTestCase */ protected function tearDown(): void { - ApiKey::query()->forceDelete(); + PersonalAccessToken::query()->forceDelete(); parent::tearDown(); } @@ -25,11 +26,8 @@ class ApiKeyControllerTest extends ClientApiIntegrationTestCase { /** @var \Pterodactyl\Models\User $user */ $user = User::factory()->create(); - /** @var \Pterodactyl\Models\ApiKey $key */ - $key = ApiKey::factory()->create([ - 'user_id' => $user->id, - 'key_type' => ApiKey::TYPE_ACCOUNT, - ]); + $token = $user->createToken('test'); + $token = $token->accessToken; $response = $this->actingAs($user)->get('/api/client/account/api-keys'); @@ -38,13 +36,14 @@ class ApiKeyControllerTest extends ClientApiIntegrationTestCase 'object' => 'list', 'data' => [ [ - 'object' => 'api_key', + 'object' => 'personal_access_token', 'attributes' => [ - 'identifier' => $key->identifier, - 'description' => $key->memo, - 'allowed_ips' => $key->allowed_ips, + 'token_id' => $token->token_id, + 'description' => $token->description, + 'abilities' => ['*'], 'last_used_at' => null, - 'created_at' => $key->created_at->toIso8601String(), + 'updated_at' => $this->formatTimestamp($token->updated_at), + 'created_at' => $this->formatTimestamp($token->created_at), ], ], ], @@ -64,34 +63,24 @@ class ApiKeyControllerTest extends ClientApiIntegrationTestCase // Small sub-test to ensure we're always comparing the number of keys to the // specific logged in account, and not just the total number of keys stored in // the database. - ApiKey::factory()->times(10)->create([ + PersonalAccessToken::factory()->times(10)->create([ 'user_id' => User::factory()->create()->id, - 'key_type' => ApiKey::TYPE_ACCOUNT, ]); $response = $this->actingAs($user)->postJson('/api/client/account/api-keys', [ 'description' => 'Test Description', - 'allowed_ips' => ['127.0.0.1'], ]); $response->assertOk(); - /** @var \Pterodactyl\Models\ApiKey $key */ - $key = ApiKey::query()->where('identifier', $response->json('attributes.identifier'))->firstOrFail(); + $key = PersonalAccessToken::query()->where('token_id', $response->json('attributes.token_id'))->firstOrFail(); $response->assertJson([ - 'object' => 'api_key', - 'attributes' => [ - 'identifier' => $key->identifier, - 'description' => 'Test Description', - 'allowed_ips' => ['127.0.0.1'], - 'last_used_at' => null, - 'created_at' => $key->created_at->toIso8601String(), - ], - 'meta' => [ - 'secret_token' => decrypt($key->token), - ], + 'object' => 'personal_access_token', + 'attributes' => (new PersonalAccessTokenTransformer())->transform($key), ]); + + $this->assertEquals($key->token, hash('sha256', substr($response->json('meta.secret_token'), 16))); } /** @@ -104,14 +93,10 @@ class ApiKeyControllerTest extends ClientApiIntegrationTestCase { /** @var \Pterodactyl\Models\User $user */ $user = User::factory()->create(); - ApiKey::factory()->times(5)->create([ - 'user_id' => $user->id, - 'key_type' => ApiKey::TYPE_ACCOUNT, - ]); + PersonalAccessToken::factory()->times(10)->create(['user_id' => $user->id]); $response = $this->actingAs($user)->postJson('/api/client/account/api-keys', [ 'description' => 'Test Description', - 'allowed_ips' => ['127.0.0.1'], ]); $response->assertStatus(Response::HTTP_BAD_REQUEST); @@ -131,7 +116,6 @@ class ApiKeyControllerTest extends ClientApiIntegrationTestCase $response = $this->actingAs($user)->postJson('/api/client/account/api-keys', [ 'description' => '', - 'allowed_ips' => ['127.0.0.1'], ]); $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); @@ -140,7 +124,6 @@ class ApiKeyControllerTest extends ClientApiIntegrationTestCase $response = $this->actingAs($user)->postJson('/api/client/account/api-keys', [ 'description' => str_repeat('a', 501), - 'allowed_ips' => ['127.0.0.1'], ]); $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); @@ -155,16 +138,12 @@ class ApiKeyControllerTest extends ClientApiIntegrationTestCase { /** @var \Pterodactyl\Models\User $user */ $user = User::factory()->create(); - /** @var \Pterodactyl\Models\ApiKey $key */ - $key = ApiKey::factory()->create([ - 'user_id' => $user->id, - 'key_type' => ApiKey::TYPE_ACCOUNT, - ]); + $token = $user->createToken('test'); - $response = $this->actingAs($user)->delete('/api/client/account/api-keys/' . $key->identifier); - $response->assertStatus(Response::HTTP_NO_CONTENT); + $response = $this->actingAs($user)->delete('/api/client/account/api-keys/' . $token->accessToken->token_id); + $response->assertNoContent(); - $this->assertDatabaseMissing('api_keys', ['id' => $key->id]); + $this->assertDatabaseMissing('personal_access_tokens', ['id' => $token->accessToken->id]); } /** @@ -174,16 +153,12 @@ class ApiKeyControllerTest extends ClientApiIntegrationTestCase { /** @var \Pterodactyl\Models\User $user */ $user = User::factory()->create(); - /** @var \Pterodactyl\Models\ApiKey $key */ - $key = ApiKey::factory()->create([ - 'user_id' => $user->id, - 'key_type' => ApiKey::TYPE_ACCOUNT, - ]); + $token = $user->createToken('test'); - $response = $this->actingAs($user)->delete('/api/client/account/api-keys/1234'); - $response->assertNotFound(); + $response = $this->actingAs($user)->delete('/api/client/account/api-keys/ptdl_1234'); + $response->assertNoContent(); - $this->assertDatabaseHas('api_keys', ['id' => $key->id]); + $this->assertDatabaseHas('personal_access_tokens', ['id' => $token->accessToken->id]); } /** @@ -196,35 +171,11 @@ class ApiKeyControllerTest extends ClientApiIntegrationTestCase $user = User::factory()->create(); /** @var \Pterodactyl\Models\User $user2 */ $user2 = User::factory()->create(); - /** @var \Pterodactyl\Models\ApiKey $key */ - $key = ApiKey::factory()->create([ - 'user_id' => $user2->id, - 'key_type' => ApiKey::TYPE_ACCOUNT, - ]); + $token = $user2->createToken('test'); - $response = $this->actingAs($user)->delete('/api/client/account/api-keys/' . $key->identifier); - $response->assertNotFound(); + $response = $this->actingAs($user)->delete('/api/client/account/api-keys/' . $token->accessToken->token_id); + $response->assertNoContent(); - $this->assertDatabaseHas('api_keys', ['id' => $key->id]); - } - - /** - * Tests that an application API key also belonging to the logged in user cannot be - * deleted through this endpoint if it exists. - */ - public function testApplicationApiKeyCannotBeDeleted() - { - /** @var \Pterodactyl\Models\User $user */ - $user = User::factory()->create(); - /** @var \Pterodactyl\Models\ApiKey $key */ - $key = ApiKey::factory()->create([ - 'user_id' => $user->id, - 'key_type' => ApiKey::TYPE_APPLICATION, - ]); - - $response = $this->actingAs($user)->delete('/api/client/account/api-keys/' . $key->identifier); - $response->assertNotFound(); - - $this->assertDatabaseHas('api_keys', ['id' => $key->id]); + $this->assertDatabaseHas('personal_access_tokens', ['id' => $token->accessToken->id]); } } diff --git a/tests/Integration/Api/Client/ClientApiIntegrationTestCase.php b/tests/Integration/Api/Client/ClientApiIntegrationTestCase.php index c6c5dcade..0ec5fa48f 100644 --- a/tests/Integration/Api/Client/ClientApiIntegrationTestCase.php +++ b/tests/Integration/Api/Client/ClientApiIntegrationTestCase.php @@ -6,7 +6,6 @@ use ReflectionClass; use Pterodactyl\Models\Node; use Pterodactyl\Models\Task; use Pterodactyl\Models\User; -use Webmozart\Assert\Assert; use InvalidArgumentException; use Pterodactyl\Models\Backup; use Pterodactyl\Models\Server; @@ -60,7 +59,6 @@ abstract class ClientApiIntegrationTestCase extends IntegrationTestCase */ protected function link($model, $append = null): string { - $link = ''; switch (get_class($model)) { case Server::class: $link = "/api/client/servers/{$model->uuid}"; @@ -99,7 +97,6 @@ abstract class ClientApiIntegrationTestCase extends IntegrationTestCase return [$user, $this->createServerModel(['user_id' => $user->id])]; } - /** @var \Pterodactyl\Models\Server $server */ $server = $this->createServerModel(); Subuser::query()->create([