diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 29f6526a03..45a2ad54f3 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -210,6 +210,25 @@ class DatabasesController extends Controller return $backup->unsetRelation('s3Storages')->unsetRelation('s3'); } + /** + * Build the create response; the connection URLs contain credentials, so they need sensitive read access. + * + * @return array{uuid: string, internal_db_url?: string, external_db_url?: string} + */ + private function createdDatabasePayload(Request $request, $database): array + { + $payload = ['uuid' => $database->uuid]; + if ($request->attributes->get('can_read_sensitive', false) !== true) { + return $payload; + } + $payload['internal_db_url'] = $database->internal_db_url; + if ($database->is_public && $database->public_port) { + $payload['external_db_url'] = $database->external_db_url; + } + + return $payload; + } + private function removeSensitiveData($database, bool $loadNestedServerSecrets = false) { $database->makeHidden([ @@ -2330,13 +2349,7 @@ class DatabasesController extends Controller $this->attachTagsToResource($database, $tagNames, $teamId); } $database->refresh(); - $payload = [ - 'uuid' => $database->uuid, - 'internal_db_url' => $database->internal_db_url, - ]; - if ($database->is_public && $database->public_port) { - $payload['external_db_url'] = $database->external_db_url; - } + $payload = $this->createdDatabasePayload($request, $database); auditLog('api.database.created', [ 'team_id' => $teamId, @@ -2402,13 +2415,7 @@ class DatabasesController extends Controller } $database->refresh(); - $payload = [ - 'uuid' => $database->uuid, - 'internal_db_url' => $database->internal_db_url, - ]; - if ($database->is_public && $database->public_port) { - $payload['external_db_url'] = $database->external_db_url; - } + $payload = $this->createdDatabasePayload($request, $database); auditLog('api.database.created', [ 'team_id' => $teamId, @@ -2474,13 +2481,7 @@ class DatabasesController extends Controller } $database->refresh(); - $payload = [ - 'uuid' => $database->uuid, - 'internal_db_url' => $database->internal_db_url, - ]; - if ($database->is_public && $database->public_port) { - $payload['external_db_url'] = $database->external_db_url; - } + $payload = $this->createdDatabasePayload($request, $database); auditLog('api.database.created', [ 'team_id' => $teamId, @@ -2543,13 +2544,7 @@ class DatabasesController extends Controller } $database->refresh(); - $payload = [ - 'uuid' => $database->uuid, - 'internal_db_url' => $database->internal_db_url, - ]; - if ($database->is_public && $database->public_port) { - $payload['external_db_url'] = $database->external_db_url; - } + $payload = $this->createdDatabasePayload($request, $database); auditLog('api.database.created', [ 'team_id' => $teamId, @@ -2645,13 +2640,7 @@ class DatabasesController extends Controller } $database->refresh(); - $payload = [ - 'uuid' => $database->uuid, - 'internal_db_url' => $database->internal_db_url, - ]; - if ($database->is_public && $database->public_port) { - $payload['external_db_url'] = $database->external_db_url; - } + $payload = $this->createdDatabasePayload($request, $database); auditLog('api.database.created', [ 'team_id' => $teamId, @@ -2694,13 +2683,7 @@ class DatabasesController extends Controller } $database->refresh(); - $payload = [ - 'uuid' => $database->uuid, - 'internal_db_url' => $database->internal_db_url, - ]; - if ($database->is_public && $database->public_port) { - $payload['external_db_url'] = $database->external_db_url; - } + $payload = $this->createdDatabasePayload($request, $database); auditLog('api.database.created', [ 'team_id' => $teamId, @@ -2765,13 +2748,7 @@ class DatabasesController extends Controller } $database->refresh(); - $payload = [ - 'uuid' => $database->uuid, - 'internal_db_url' => $database->internal_db_url, - ]; - if ($database->is_public && $database->public_port) { - $payload['external_db_url'] = $database->external_db_url; - } + $payload = $this->createdDatabasePayload($request, $database); auditLog('api.database.created', [ 'team_id' => $teamId, diff --git a/tests/Feature/DatabaseCreateSensitiveResponseApiTest.php b/tests/Feature/DatabaseCreateSensitiveResponseApiTest.php new file mode 100644 index 0000000000..4197c57086 --- /dev/null +++ b/tests/Feature/DatabaseCreateSensitiveResponseApiTest.php @@ -0,0 +1,73 @@ + 'file']); + InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0], ['is_api_enabled' => true])); + + $this->team = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + session(['currentTeam' => $this->team]); + + $this->server = Server::factory()->create(['team_id' => $this->team->id]); + $this->project = Project::factory()->create(['team_id' => $this->team->id]); + $this->environment = Environment::factory()->create(['project_id' => $this->project->id]); + + Queue::fake(); +}); + +function databaseCreateSensitiveHeaders(User $user, int $teamId, array $abilities): array +{ + $plainTextToken = Str::random(40); + $token = $user->tokens()->create([ + 'name' => 'database-create-sensitive-test', + 'token' => hash('sha256', $plainTextToken), + 'abilities' => $abilities, + 'team_id' => $teamId, + ]); + + return ['Authorization' => 'Bearer '.$token->getKey().'|'.$plainTextToken]; +} + +function createDatabaseViaApi(object $test, string $engine, array $abilities) +{ + return $test->withHeaders(databaseCreateSensitiveHeaders($test->user, $test->team->id, $abilities)) + ->postJson("/api/v1/databases/{$engine}", [ + 'project_uuid' => $test->project->uuid, + 'environment_uuid' => $test->environment->uuid, + 'server_uuid' => $test->server->uuid, + 'is_public' => true, + 'public_port' => 15432, + ]); +} + +$engines = ['postgresql', 'mariadb', 'mysql', 'redis', 'keydb', 'clickhouse', 'mongodb']; + +test('create response hides database urls from a token without sensitive read access', function (string $engine) { + $response = createDatabaseViaApi($this, $engine, ['write'])->assertCreated(); + + expect($response->json())->toHaveKey('uuid') + ->not->toHaveKey('internal_db_url') + ->not->toHaveKey('external_db_url'); +})->with($engines); + +test('create response includes database urls for a token with sensitive read access', function (string $engine, array $abilities) { + $response = createDatabaseViaApi($this, $engine, $abilities)->assertCreated(); + + expect($response->json())->toHaveKeys(['uuid', 'internal_db_url', 'external_db_url']); +})->with($engines)->with([ + 'read:sensitive' => [['write', 'read:sensitive']], + 'root' => [['root']], +]);