fix(api): hide sensitive fields by default

Add model-level hidden fields for secrets, tokens, keys, notification
credentials, deployment logs, and environment values. Allow explicit
read:sensitive API access to reveal gated private keys and deployment logs,
and cover the behavior with feature and unit tests.
This commit is contained in:
Andras Bacsai committed 2026-05-11 21:18:07 +02:00
1 parent f40bb80f9d
commit 81a3bb0f07
20 files changed
+335 -2

No files matched your search

@@ -1,8 +1,10 @@
<?php
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
use App\Models\Service;
@@ -101,6 +103,99 @@ describe('GET /api/v1/servers sensitive field gating', function () {
});
});
describe('GET /api/v1/security/keys sensitive field gating', function () {
beforeEach(function () {
PrivateKey::withoutEvents(function () {
PrivateKey::forceCreate([
'uuid' => 'private-key-sensitive-test',
'name' => 'Sensitive key',
'description' => 'test',
'private_key' => 'super-secret-private-key',
'is_git_related' => false,
'team_id' => $this->team->id,
]);
});
});
test('read token does not leak private key material', function () {
$token = makeApiToken($this->user, $this->team, ['read']);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token,
])->getJson('/api/v1/security/keys/private-key-sensitive-test');
$response->assertStatus(200);
expect($response->getContent())->not->toContain('"private_key":');
});
test('read sensitive token sees private key material', function () {
$token = makeApiToken($this->user, $this->team, ['read', 'read:sensitive']);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token,
])->getJson('/api/v1/security/keys/private-key-sensitive-test');
$response->assertStatus(200);
expect($response->getContent())->toContain('"private_key":');
});
});
describe('GET /api/v1/deployments sensitive field gating', function () {
beforeEach(function () {
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
$destination = $this->server->standaloneDockers()->firstOrFail();
$this->application = Application::create([
'name' => 'deployment-sensitive-test-app',
'git_repository' => 'https://github.com/test/test',
'git_branch' => 'main',
'build_pack' => 'nixpacks',
'ports_exposes' => '3000',
'environment_id' => $this->environment->id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
ApplicationDeploymentQueue::create([
'application_id' => $this->application->id,
'deployment_uuid' => 'deployment-sensitive-test',
'pull_request_id' => 0,
'status' => 'in_progress',
'logs' => '[{"output":"super-secret-deployment-log"}]',
'server_id' => $this->server->id,
'application_name' => $this->application->name,
'server_name' => $this->server->name,
]);
});
test('read token does not leak deployment logs', function () {
$token = makeApiToken($this->user, $this->team, ['read']);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token,
])->getJson('/api/v1/deployments/deployment-sensitive-test');
$response->assertStatus(200);
expect($response->getContent())->not->toContain('"logs":');
});
test('read sensitive token sees deployment logs', function () {
$token = makeApiToken($this->user, $this->team, ['read', 'read:sensitive']);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token,
])->getJson('/api/v1/deployments/deployment-sensitive-test');
$response->assertStatus(200);
expect($response->getContent())->toContain('"logs":');
});
});
describe('GET /api/v1/applications nested-relation scrubbing', function () {
beforeEach(function () {
$this->project = Project::factory()->create(['team_id' => $this->team->id]);