fix(api): hide nested server secrets from read tokens
Require read:sensitive for nested server logdrain and sentinel fields in application and database API responses. Limit deployment configuration column migration SQL to PostgreSQL.
This commit is contained in:
parent
2fcc42b0a9
commit
70eda65d19
7 changed files with 743 additions and 2 deletions
|
|
@ -61,11 +61,37 @@ private function removeSensitiveData($application)
|
|||
$application->makeHidden([
|
||||
'private_key_id',
|
||||
]);
|
||||
$this->hideNestedServerSecrets($application);
|
||||
}
|
||||
|
||||
return serializeApiResponse($application);
|
||||
}
|
||||
|
||||
private function hideNestedServerSecrets($model): void
|
||||
{
|
||||
$server = $model->destination?->server ?? null;
|
||||
if (! $server) {
|
||||
return;
|
||||
}
|
||||
|
||||
$server->makeHidden([
|
||||
'logdrain_axiom_api_key',
|
||||
'logdrain_newrelic_license_key',
|
||||
]);
|
||||
|
||||
$settings = $server->settings ?? null;
|
||||
if ($settings) {
|
||||
$settings->makeHidden([
|
||||
'sentinel_token',
|
||||
'sentinel_custom_url',
|
||||
'logdrain_newrelic_license_key',
|
||||
'logdrain_axiom_api_key',
|
||||
'logdrain_custom_config',
|
||||
'logdrain_custom_config_parser',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose sensitive fields on eager-loaded nested Server + ServerSetting
|
||||
* relations for callers with the `read:sensitive` or `root` token ability.
|
||||
|
|
|
|||
|
|
@ -51,11 +51,37 @@ private function removeSensitiveData($database)
|
|||
'mariadb_root_password',
|
||||
]);
|
||||
$this->exposeNestedServerSecrets($database);
|
||||
} else {
|
||||
$this->hideNestedServerSecrets($database);
|
||||
}
|
||||
|
||||
return serializeApiResponse($database);
|
||||
}
|
||||
|
||||
private function hideNestedServerSecrets(Model $model): void
|
||||
{
|
||||
$server = $model->destination?->server;
|
||||
if ($server === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$server->makeHidden([
|
||||
'logdrain_axiom_api_key',
|
||||
'logdrain_newrelic_license_key',
|
||||
]);
|
||||
|
||||
if ($server->settings !== null) {
|
||||
$server->settings->makeHidden([
|
||||
'sentinel_token',
|
||||
'sentinel_custom_url',
|
||||
'logdrain_newrelic_license_key',
|
||||
'logdrain_axiom_api_key',
|
||||
'logdrain_custom_config',
|
||||
'logdrain_custom_config_parser',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose sensitive fields on eager-loaded nested Server + ServerSetting
|
||||
* relations for callers with the `read:sensitive` or `root` token ability.
|
||||
|
|
|
|||
|
|
@ -2,8 +2,12 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class CloudProviderToken extends BaseModel
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'team_id',
|
||||
'provider',
|
||||
|
|
|
|||
30
database/factories/CloudProviderTokenFactory.php
Normal file
30
database/factories/CloudProviderTokenFactory.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\CloudProviderToken;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<CloudProviderToken>
|
||||
*/
|
||||
class CloudProviderTokenFactory extends Factory
|
||||
{
|
||||
protected $model = CloudProviderToken::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'team_id' => Team::factory(),
|
||||
'provider' => 'hetzner',
|
||||
'token' => $this->faker->sha256(),
|
||||
'name' => $this->faker->words(2, true),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -11,12 +11,20 @@
|
|||
*/
|
||||
public function up(): void
|
||||
{
|
||||
if (DB::connection()->getDriverName() !== 'pgsql') {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::statement('ALTER TABLE application_deployment_queues ALTER COLUMN configuration_snapshot TYPE text USING configuration_snapshot::text');
|
||||
DB::statement('ALTER TABLE application_deployment_queues ALTER COLUMN configuration_diff TYPE text USING configuration_diff::text');
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (DB::connection()->getDriverName() !== 'pgsql') {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::statement('ALTER TABLE application_deployment_queues ALTER COLUMN configuration_snapshot TYPE json USING configuration_snapshot::json');
|
||||
DB::statement('ALTER TABLE application_deployment_queues ALTER COLUMN configuration_diff TYPE json USING configuration_diff::json');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -171,6 +171,44 @@
|
|||
|
||||
$response->assertStatus(404);
|
||||
});
|
||||
|
||||
test('read token does not include provider token value by UUID', function () {
|
||||
$token = CloudProviderToken::create([
|
||||
'team_id' => $this->team->id,
|
||||
'name' => 'Hidden Token Detail',
|
||||
'provider' => 'hetzner',
|
||||
'token' => 'hidden-cloud-provider-token-detail',
|
||||
]);
|
||||
|
||||
$readToken = $this->user->createToken('read-token', ['read'])->plainTextToken;
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$readToken,
|
||||
'Content-Type' => 'application/json',
|
||||
])->getJson("/api/v1/cloud-tokens/{$token->uuid}");
|
||||
|
||||
$response->assertSuccessful();
|
||||
expect($response->getContent())->not->toContain('"token":');
|
||||
});
|
||||
|
||||
test('read sensitive token includes provider token value by UUID', function () {
|
||||
$token = CloudProviderToken::create([
|
||||
'team_id' => $this->team->id,
|
||||
'name' => 'Visible Token Detail',
|
||||
'provider' => 'hetzner',
|
||||
'token' => 'visible-cloud-provider-token-detail',
|
||||
]);
|
||||
|
||||
$readSensitiveToken = $this->user->createToken('read-sensitive-token', ['read', 'read:sensitive'])->plainTextToken;
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$readSensitiveToken,
|
||||
'Content-Type' => 'application/json',
|
||||
])->getJson("/api/v1/cloud-tokens/{$token->uuid}");
|
||||
|
||||
$response->assertSuccessful();
|
||||
$response->assertJsonFragment(['token' => 'visible-cloud-provider-token-detail']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/v1/cloud-tokens', function () {
|
||||
|
|
@ -345,8 +383,11 @@
|
|||
'Content-Type' => 'application/json',
|
||||
])->patchJson("/api/v1/cloud-tokens/{$token->uuid}", []);
|
||||
|
||||
$response->assertStatus(422);
|
||||
$response->assertJsonValidationErrors(['name']);
|
||||
$response->assertStatus(400);
|
||||
$response->assertJson([
|
||||
'message' => 'Invalid request.',
|
||||
'error' => 'Invalid JSON.',
|
||||
]);
|
||||
});
|
||||
|
||||
test('cannot update token from another team', function () {
|
||||
|
|
|
|||
|
|
@ -101,6 +101,53 @@ function makeTeamUser(): array
|
|||
$body = $response->getContent();
|
||||
expect($body)->toContain('sentinel_token');
|
||||
});
|
||||
|
||||
test('read token does not leak sentinel or logdrain fields in server detail', function () {
|
||||
$token = makeApiToken($this->user, $this->team, ['read']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson("/api/v1/servers/{$this->server->uuid}");
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$body = $response->getContent();
|
||||
expect($body)->not->toContain('sentinel_token');
|
||||
expect($body)->not->toContain('sentinel_custom_url');
|
||||
expect($body)->not->toContain('logdrain_axiom_api_key');
|
||||
expect($body)->not->toContain('logdrain_newrelic_license_key');
|
||||
expect($body)->not->toContain('logdrain_custom_config');
|
||||
});
|
||||
|
||||
test('read sensitive token sees sentinel and logdrain fields in server detail', function () {
|
||||
$token = makeApiToken($this->user, $this->team, ['read', 'read:sensitive']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson("/api/v1/servers/{$this->server->uuid}");
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$body = $response->getContent();
|
||||
expect($body)->toContain('sentinel_token');
|
||||
expect($body)->toContain('sentinel_custom_url');
|
||||
expect($body)->toContain('logdrain_axiom_api_key');
|
||||
});
|
||||
|
||||
test('server resources response does not leak server secrets', function () {
|
||||
$token = makeApiToken($this->user, $this->team, ['read', 'read:sensitive']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson("/api/v1/servers/{$this->server->uuid}/resources");
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$body = $response->getContent();
|
||||
expect($body)->not->toContain('sentinel_token');
|
||||
expect($body)->not->toContain('logdrain_axiom_api_key');
|
||||
expect($body)->not->toContain('logdrain_newrelic_license_key');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/security/keys sensitive field gating', function () {
|
||||
|
|
@ -140,6 +187,30 @@ function makeTeamUser(): array
|
|||
|
||||
expect($response->getContent())->toContain('"private_key":');
|
||||
});
|
||||
|
||||
test('read token does not leak private key material in key list', function () {
|
||||
$token = makeApiToken($this->user, $this->team, ['read']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson('/api/v1/security/keys');
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
expect($response->getContent())->not->toContain('"private_key":');
|
||||
});
|
||||
|
||||
test('read sensitive token sees private key material in key list', function () {
|
||||
$token = makeApiToken($this->user, $this->team, ['read', 'read:sensitive']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson('/api/v1/security/keys');
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
expect($response->getContent())->toContain('"private_key":');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/deployments sensitive field gating', function () {
|
||||
|
|
@ -194,6 +265,54 @@ function makeTeamUser(): array
|
|||
|
||||
expect($response->getContent())->toContain('"logs":');
|
||||
});
|
||||
|
||||
test('read token does not leak deployment logs in deployment list', function () {
|
||||
$token = makeApiToken($this->user, $this->team, ['read']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson('/api/v1/deployments');
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
expect($response->getContent())->not->toContain('"logs":');
|
||||
});
|
||||
|
||||
test('read sensitive token sees deployment logs in deployment list', function () {
|
||||
$token = makeApiToken($this->user, $this->team, ['read', 'read:sensitive']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson('/api/v1/deployments');
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
expect($response->getContent())->toContain('"logs":');
|
||||
});
|
||||
|
||||
test('read token does not leak deployment logs in application deployment history', function () {
|
||||
$token = makeApiToken($this->user, $this->team, ['read']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson("/api/v1/deployments/applications/{$this->application->uuid}");
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
expect($response->getContent())->not->toContain('"logs":');
|
||||
});
|
||||
|
||||
test('read sensitive token sees deployment logs in application deployment history', function () {
|
||||
$token = makeApiToken($this->user, $this->team, ['read', 'read:sensitive']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson("/api/v1/deployments/applications/{$this->application->uuid}");
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
expect($response->getContent())->toContain('"logs":');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/applications nested-relation scrubbing', function () {
|
||||
|
|
@ -242,6 +361,174 @@ function makeTeamUser(): array
|
|||
expect($body)->toContain('"sentinel_token":');
|
||||
expect($body)->toContain('"sentinel_custom_url":');
|
||||
});
|
||||
|
||||
test('read token does not leak application detail sensitive fields', function () {
|
||||
$this->application->forceFill([
|
||||
'manual_webhook_secret_github' => 'super-secret-github-webhook',
|
||||
'http_basic_auth_password' => 'super-secret-basic-password',
|
||||
])->save();
|
||||
|
||||
$token = makeApiToken($this->user, $this->team, ['read']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson("/api/v1/applications/{$this->application->uuid}");
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$body = $response->getContent();
|
||||
expect($body)->not->toContain('"manual_webhook_secret_github":')
|
||||
->and($body)->not->toContain('"http_basic_auth_password":')
|
||||
->and($body)->not->toContain('"sentinel_token":');
|
||||
});
|
||||
|
||||
test('read sensitive token sees application detail sensitive fields', function () {
|
||||
$this->application->forceFill([
|
||||
'manual_webhook_secret_github' => 'super-secret-github-webhook',
|
||||
'http_basic_auth_password' => 'super-secret-basic-password',
|
||||
])->save();
|
||||
|
||||
$token = makeApiToken($this->user, $this->team, ['read', 'read:sensitive']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson("/api/v1/applications/{$this->application->uuid}");
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$body = $response->getContent();
|
||||
expect($body)->toContain('"manual_webhook_secret_github":')
|
||||
->and($body)->toContain('"http_basic_auth_password":')
|
||||
->and($body)->toContain('"sentinel_token":');
|
||||
});
|
||||
|
||||
test('application env responses hide values for read tokens and reveal them for sensitive tokens', function () {
|
||||
$this->application->environment_variables()->create([
|
||||
'key' => 'APP_SECRET',
|
||||
'value' => 'super-secret-app-env',
|
||||
]);
|
||||
|
||||
$readToken = makeApiToken($this->user, $this->team, ['read']);
|
||||
$sensitiveToken = makeApiToken($this->user, $this->team, ['read', 'read:sensitive']);
|
||||
|
||||
$readResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$readToken,
|
||||
])->getJson("/api/v1/applications/{$this->application->uuid}/envs");
|
||||
|
||||
auth()->forgetGuards();
|
||||
|
||||
$sensitiveResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$sensitiveToken,
|
||||
])->getJson("/api/v1/applications/{$this->application->uuid}/envs");
|
||||
|
||||
$readResponse->assertStatus(200);
|
||||
$sensitiveResponse->assertStatus(200);
|
||||
|
||||
expect($readResponse->getContent())->not->toContain('"value":')
|
||||
->and($readResponse->getContent())->not->toContain('"real_value":')
|
||||
->and($sensitiveResponse->getContent())->toContain('"value":')
|
||||
->and($sensitiveResponse->getContent())->toContain('"real_value":');
|
||||
});
|
||||
|
||||
test('application create env response does not include secret values', function () {
|
||||
$writeToken = makeApiToken($this->user, $this->team, ['write']);
|
||||
$sensitiveToken = makeApiToken($this->user, $this->team, ['write', 'read:sensitive']);
|
||||
|
||||
$writeResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$writeToken,
|
||||
])->postJson("/api/v1/applications/{$this->application->uuid}/envs", [
|
||||
'key' => 'APP_CREATE_SECRET',
|
||||
'value' => 'super-secret-app-create-env',
|
||||
]);
|
||||
|
||||
auth()->forgetGuards();
|
||||
|
||||
$sensitiveResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$sensitiveToken,
|
||||
])->postJson("/api/v1/applications/{$this->application->uuid}/envs", [
|
||||
'key' => 'APP_CREATE_SENSITIVE_SECRET',
|
||||
'value' => 'super-secret-app-create-sensitive-env',
|
||||
]);
|
||||
|
||||
$writeResponse->assertStatus(201);
|
||||
$sensitiveResponse->assertStatus(201);
|
||||
|
||||
expect($writeResponse->getContent())->not->toContain('"value":')
|
||||
->and($writeResponse->getContent())->not->toContain('"real_value":')
|
||||
->and($sensitiveResponse->getContent())->not->toContain('"value":')
|
||||
->and($sensitiveResponse->getContent())->not->toContain('"real_value":');
|
||||
});
|
||||
|
||||
test('application update env response hides values for write tokens and reveals them for sensitive tokens', function () {
|
||||
$this->application->environment_variables()->create([
|
||||
'key' => 'APP_UPDATE_SECRET',
|
||||
'value' => 'old-app-update-secret',
|
||||
]);
|
||||
|
||||
$writeToken = makeApiToken($this->user, $this->team, ['write']);
|
||||
$sensitiveToken = makeApiToken($this->user, $this->team, ['write', 'read:sensitive']);
|
||||
|
||||
$writeResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$writeToken,
|
||||
])->patchJson("/api/v1/applications/{$this->application->uuid}/envs", [
|
||||
'key' => 'APP_UPDATE_SECRET',
|
||||
'value' => 'hidden-app-update-secret',
|
||||
'is_multiline' => false,
|
||||
'is_shown_once' => false,
|
||||
]);
|
||||
|
||||
auth()->forgetGuards();
|
||||
|
||||
$sensitiveResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$sensitiveToken,
|
||||
])->patchJson("/api/v1/applications/{$this->application->uuid}/envs", [
|
||||
'key' => 'APP_UPDATE_SECRET',
|
||||
'value' => 'visible-app-update-secret',
|
||||
'is_multiline' => false,
|
||||
'is_shown_once' => false,
|
||||
]);
|
||||
|
||||
$writeResponse->assertStatus(201);
|
||||
$sensitiveResponse->assertStatus(201);
|
||||
|
||||
expect($writeResponse->getContent())->not->toContain('"value":')
|
||||
->and($writeResponse->getContent())->not->toContain('"real_value":')
|
||||
->and($sensitiveResponse->getContent())->toContain('"value":')
|
||||
->and($sensitiveResponse->getContent())->toContain('"real_value":');
|
||||
});
|
||||
|
||||
test('application bulk env response hides values for write tokens and reveals them for sensitive tokens', function () {
|
||||
$writeToken = makeApiToken($this->user, $this->team, ['write']);
|
||||
$sensitiveToken = makeApiToken($this->user, $this->team, ['write', 'read:sensitive']);
|
||||
|
||||
$writeResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$writeToken,
|
||||
])->patchJson("/api/v1/applications/{$this->application->uuid}/envs/bulk", [
|
||||
'data' => [[
|
||||
'key' => 'APP_BULK_SECRET',
|
||||
'value' => 'hidden-app-bulk-secret',
|
||||
]],
|
||||
]);
|
||||
|
||||
auth()->forgetGuards();
|
||||
|
||||
$sensitiveResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$sensitiveToken,
|
||||
])->patchJson("/api/v1/applications/{$this->application->uuid}/envs/bulk", [
|
||||
'data' => [[
|
||||
'key' => 'APP_BULK_SENSITIVE_SECRET',
|
||||
'value' => 'visible-app-bulk-secret',
|
||||
]],
|
||||
]);
|
||||
|
||||
$writeResponse->assertStatus(201);
|
||||
$sensitiveResponse->assertStatus(201);
|
||||
|
||||
expect($writeResponse->getContent())->not->toContain('"value":')
|
||||
->and($writeResponse->getContent())->not->toContain('"real_value":')
|
||||
->and($sensitiveResponse->getContent())->toContain('"value":')
|
||||
->and($sensitiveResponse->getContent())->toContain('"real_value":');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/databases sensitive field gating', function () {
|
||||
|
|
@ -293,6 +580,161 @@ function makeTeamUser(): array
|
|||
expect($body)->toContain('"sentinel_token":');
|
||||
});
|
||||
|
||||
test('read token does not leak database detail sensitive fields', function () {
|
||||
$token = makeApiToken($this->user, $this->team, ['read']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson("/api/v1/databases/{$this->database->uuid}");
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$body = $response->getContent();
|
||||
expect($body)->not->toContain('"postgres_password":')
|
||||
->and($body)->not->toContain('"internal_db_url":')
|
||||
->and($body)->not->toContain('"external_db_url":')
|
||||
->and($body)->not->toContain('"sentinel_token":');
|
||||
});
|
||||
|
||||
test('read sensitive token sees database detail sensitive fields', function () {
|
||||
$token = makeApiToken($this->user, $this->team, ['read', 'read:sensitive']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson("/api/v1/databases/{$this->database->uuid}");
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$body = $response->getContent();
|
||||
expect($body)->toContain('"postgres_password":')
|
||||
->and($body)->toContain('"internal_db_url":')
|
||||
->and($body)->toContain('"sentinel_token":');
|
||||
});
|
||||
|
||||
test('database env responses hide values for read tokens and reveal them for sensitive tokens', function () {
|
||||
$this->database->environment_variables()->create([
|
||||
'key' => 'DB_SECRET',
|
||||
'value' => 'super-secret-db-env',
|
||||
]);
|
||||
|
||||
$readToken = makeApiToken($this->user, $this->team, ['read']);
|
||||
$sensitiveToken = makeApiToken($this->user, $this->team, ['read', 'read:sensitive']);
|
||||
|
||||
$readResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$readToken,
|
||||
])->getJson("/api/v1/databases/{$this->database->uuid}/envs");
|
||||
|
||||
auth()->forgetGuards();
|
||||
|
||||
$sensitiveResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$sensitiveToken,
|
||||
])->getJson("/api/v1/databases/{$this->database->uuid}/envs");
|
||||
|
||||
$readResponse->assertStatus(200);
|
||||
$sensitiveResponse->assertStatus(200);
|
||||
|
||||
expect($readResponse->getContent())->not->toContain('"value":')
|
||||
->and($readResponse->getContent())->not->toContain('"real_value":')
|
||||
->and($sensitiveResponse->getContent())->toContain('"value":')
|
||||
->and($sensitiveResponse->getContent())->toContain('"real_value":');
|
||||
});
|
||||
|
||||
test('database create env response hides values for write tokens and reveals them for sensitive tokens', function () {
|
||||
$writeToken = makeApiToken($this->user, $this->team, ['write']);
|
||||
$sensitiveToken = makeApiToken($this->user, $this->team, ['write', 'read:sensitive']);
|
||||
|
||||
$writeResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$writeToken,
|
||||
])->postJson("/api/v1/databases/{$this->database->uuid}/envs", [
|
||||
'key' => 'DB_CREATE_SECRET',
|
||||
'value' => 'hidden-db-create-secret',
|
||||
]);
|
||||
|
||||
auth()->forgetGuards();
|
||||
|
||||
$sensitiveResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$sensitiveToken,
|
||||
])->postJson("/api/v1/databases/{$this->database->uuid}/envs", [
|
||||
'key' => 'DB_CREATE_SENSITIVE_SECRET',
|
||||
'value' => 'visible-db-create-secret',
|
||||
]);
|
||||
|
||||
$writeResponse->assertStatus(201);
|
||||
$sensitiveResponse->assertStatus(201);
|
||||
|
||||
expect($writeResponse->getContent())->not->toContain('"value":')
|
||||
->and($writeResponse->getContent())->not->toContain('"real_value":')
|
||||
->and($sensitiveResponse->getContent())->toContain('"value":')
|
||||
->and($sensitiveResponse->getContent())->toContain('"real_value":');
|
||||
});
|
||||
|
||||
test('database update env response hides values for write tokens and reveals them for sensitive tokens', function () {
|
||||
$this->database->environment_variables()->create([
|
||||
'key' => 'DB_UPDATE_SECRET',
|
||||
'value' => 'old-db-update-secret',
|
||||
]);
|
||||
|
||||
$writeToken = makeApiToken($this->user, $this->team, ['write']);
|
||||
$sensitiveToken = makeApiToken($this->user, $this->team, ['write', 'read:sensitive']);
|
||||
|
||||
$writeResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$writeToken,
|
||||
])->patchJson("/api/v1/databases/{$this->database->uuid}/envs", [
|
||||
'key' => 'DB_UPDATE_SECRET',
|
||||
'value' => 'hidden-db-update-secret',
|
||||
]);
|
||||
|
||||
auth()->forgetGuards();
|
||||
|
||||
$sensitiveResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$sensitiveToken,
|
||||
])->patchJson("/api/v1/databases/{$this->database->uuid}/envs", [
|
||||
'key' => 'DB_UPDATE_SECRET',
|
||||
'value' => 'visible-db-update-secret',
|
||||
]);
|
||||
|
||||
$writeResponse->assertStatus(201);
|
||||
$sensitiveResponse->assertStatus(201);
|
||||
|
||||
expect($writeResponse->getContent())->not->toContain('"value":')
|
||||
->and($writeResponse->getContent())->not->toContain('"real_value":')
|
||||
->and($sensitiveResponse->getContent())->toContain('"value":')
|
||||
->and($sensitiveResponse->getContent())->toContain('"real_value":');
|
||||
});
|
||||
|
||||
test('database bulk env response hides values for write tokens and reveals them for sensitive tokens', function () {
|
||||
$writeToken = makeApiToken($this->user, $this->team, ['write']);
|
||||
$sensitiveToken = makeApiToken($this->user, $this->team, ['write', 'read:sensitive']);
|
||||
|
||||
$writeResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$writeToken,
|
||||
])->patchJson("/api/v1/databases/{$this->database->uuid}/envs/bulk", [
|
||||
'data' => [[
|
||||
'key' => 'DB_BULK_SECRET',
|
||||
'value' => 'hidden-db-bulk-secret',
|
||||
]],
|
||||
]);
|
||||
|
||||
auth()->forgetGuards();
|
||||
|
||||
$sensitiveResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$sensitiveToken,
|
||||
])->patchJson("/api/v1/databases/{$this->database->uuid}/envs/bulk", [
|
||||
'data' => [[
|
||||
'key' => 'DB_BULK_SENSITIVE_SECRET',
|
||||
'value' => 'visible-db-bulk-secret',
|
||||
]],
|
||||
]);
|
||||
|
||||
$writeResponse->assertStatus(201);
|
||||
$sensitiveResponse->assertStatus(201);
|
||||
|
||||
expect($writeResponse->getContent())->not->toContain('"value":')
|
||||
->and($writeResponse->getContent())->not->toContain('"real_value":')
|
||||
->and($sensitiveResponse->getContent())->toContain('"value":')
|
||||
->and($sensitiveResponse->getContent())->toContain('"real_value":');
|
||||
});
|
||||
|
||||
test('project database list can eager load nested destination server settings', function () {
|
||||
$databases = $this->project->databases(['destination.server.settings']);
|
||||
$database = $databases->firstWhere('id', $this->database->id);
|
||||
|
|
@ -348,6 +790,170 @@ function makeTeamUser(): array
|
|||
->and($body)->toContain('"sentinel_custom_url":');
|
||||
});
|
||||
|
||||
test('read token does not leak service detail sensitive fields', function () {
|
||||
$this->service->forceFill([
|
||||
'docker_compose_raw' => 'services: secret',
|
||||
'docker_compose' => 'services: rendered',
|
||||
])->save();
|
||||
|
||||
$token = makeApiToken($this->user, $this->team, ['read']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson("/api/v1/services/{$this->service->uuid}");
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$body = $response->getContent();
|
||||
expect($body)->not->toContain('"docker_compose_raw":')
|
||||
->and($body)->not->toContain('"docker_compose":')
|
||||
->and($body)->not->toContain('"sentinel_token":');
|
||||
});
|
||||
|
||||
test('read sensitive token sees service detail sensitive fields', function () {
|
||||
$this->service->forceFill([
|
||||
'docker_compose_raw' => 'services: secret',
|
||||
'docker_compose' => 'services: rendered',
|
||||
])->save();
|
||||
|
||||
$token = makeApiToken($this->user, $this->team, ['read', 'read:sensitive']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson("/api/v1/services/{$this->service->uuid}");
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$body = $response->getContent();
|
||||
expect($body)->toContain('"docker_compose_raw":')
|
||||
->and($body)->toContain('"docker_compose":')
|
||||
->and($body)->toContain('"sentinel_token":');
|
||||
});
|
||||
|
||||
test('service env responses hide values for read tokens and reveal them for sensitive tokens', function () {
|
||||
$this->service->environment_variables()->create([
|
||||
'key' => 'SERVICE_SECRET',
|
||||
'value' => 'super-secret-service-env',
|
||||
]);
|
||||
|
||||
$readToken = makeApiToken($this->user, $this->team, ['read']);
|
||||
$sensitiveToken = makeApiToken($this->user, $this->team, ['read', 'read:sensitive']);
|
||||
|
||||
$readResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$readToken,
|
||||
])->getJson("/api/v1/services/{$this->service->uuid}/envs");
|
||||
|
||||
auth()->forgetGuards();
|
||||
|
||||
$sensitiveResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$sensitiveToken,
|
||||
])->getJson("/api/v1/services/{$this->service->uuid}/envs");
|
||||
|
||||
$readResponse->assertStatus(200);
|
||||
$sensitiveResponse->assertStatus(200);
|
||||
|
||||
expect($readResponse->getContent())->not->toContain('"value":')
|
||||
->and($readResponse->getContent())->not->toContain('"real_value":')
|
||||
->and($sensitiveResponse->getContent())->toContain('"value":')
|
||||
->and($sensitiveResponse->getContent())->toContain('"real_value":');
|
||||
});
|
||||
|
||||
test('service create env response hides values for write tokens and reveals them for sensitive tokens', function () {
|
||||
$writeToken = makeApiToken($this->user, $this->team, ['write']);
|
||||
$sensitiveToken = makeApiToken($this->user, $this->team, ['write', 'read:sensitive']);
|
||||
|
||||
$writeResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$writeToken,
|
||||
])->postJson("/api/v1/services/{$this->service->uuid}/envs", [
|
||||
'key' => 'SERVICE_CREATE_SECRET',
|
||||
'value' => 'hidden-service-create-secret',
|
||||
]);
|
||||
|
||||
auth()->forgetGuards();
|
||||
|
||||
$sensitiveResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$sensitiveToken,
|
||||
])->postJson("/api/v1/services/{$this->service->uuid}/envs", [
|
||||
'key' => 'SERVICE_CREATE_SENSITIVE_SECRET',
|
||||
'value' => 'visible-service-create-secret',
|
||||
]);
|
||||
|
||||
$writeResponse->assertStatus(201);
|
||||
$sensitiveResponse->assertStatus(201);
|
||||
|
||||
expect($writeResponse->getContent())->not->toContain('"value":')
|
||||
->and($writeResponse->getContent())->not->toContain('"real_value":')
|
||||
->and($sensitiveResponse->getContent())->toContain('"value":')
|
||||
->and($sensitiveResponse->getContent())->toContain('"real_value":');
|
||||
});
|
||||
|
||||
test('service update env response hides values for write tokens and reveals them for sensitive tokens', function () {
|
||||
$this->service->environment_variables()->create([
|
||||
'key' => 'SERVICE_UPDATE_SECRET',
|
||||
'value' => 'old-service-update-secret',
|
||||
]);
|
||||
|
||||
$writeToken = makeApiToken($this->user, $this->team, ['write']);
|
||||
$sensitiveToken = makeApiToken($this->user, $this->team, ['write', 'read:sensitive']);
|
||||
|
||||
$writeResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$writeToken,
|
||||
])->patchJson("/api/v1/services/{$this->service->uuid}/envs", [
|
||||
'key' => 'SERVICE_UPDATE_SECRET',
|
||||
'value' => 'hidden-service-update-secret',
|
||||
]);
|
||||
|
||||
auth()->forgetGuards();
|
||||
|
||||
$sensitiveResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$sensitiveToken,
|
||||
])->patchJson("/api/v1/services/{$this->service->uuid}/envs", [
|
||||
'key' => 'SERVICE_UPDATE_SECRET',
|
||||
'value' => 'visible-service-update-secret',
|
||||
]);
|
||||
|
||||
$writeResponse->assertStatus(201);
|
||||
$sensitiveResponse->assertStatus(201);
|
||||
|
||||
expect($writeResponse->getContent())->not->toContain('"value":')
|
||||
->and($writeResponse->getContent())->not->toContain('"real_value":')
|
||||
->and($sensitiveResponse->getContent())->toContain('"value":')
|
||||
->and($sensitiveResponse->getContent())->toContain('"real_value":');
|
||||
});
|
||||
|
||||
test('service bulk env response hides values for write tokens and reveals them for sensitive tokens', function () {
|
||||
$writeToken = makeApiToken($this->user, $this->team, ['write']);
|
||||
$sensitiveToken = makeApiToken($this->user, $this->team, ['write', 'read:sensitive']);
|
||||
|
||||
$writeResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$writeToken,
|
||||
])->patchJson("/api/v1/services/{$this->service->uuid}/envs/bulk", [
|
||||
'data' => [[
|
||||
'key' => 'SERVICE_BULK_SECRET',
|
||||
'value' => 'hidden-service-bulk-secret',
|
||||
]],
|
||||
]);
|
||||
|
||||
auth()->forgetGuards();
|
||||
|
||||
$sensitiveResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$sensitiveToken,
|
||||
])->patchJson("/api/v1/services/{$this->service->uuid}/envs/bulk", [
|
||||
'data' => [[
|
||||
'key' => 'SERVICE_BULK_SENSITIVE_SECRET',
|
||||
'value' => 'visible-service-bulk-secret',
|
||||
]],
|
||||
]);
|
||||
|
||||
$writeResponse->assertStatus(201);
|
||||
$sensitiveResponse->assertStatus(201);
|
||||
|
||||
expect($writeResponse->getContent())->not->toContain('"value":')
|
||||
->and($writeResponse->getContent())->not->toContain('"real_value":')
|
||||
->and($sensitiveResponse->getContent())->toContain('"value":')
|
||||
->and($sensitiveResponse->getContent())->toContain('"real_value":');
|
||||
});
|
||||
|
||||
test('read sensitive service list eager loads nested server settings once', function () {
|
||||
$secondServer = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$secondDestination = $secondServer->standaloneDockers()->firstOrFail();
|
||||
|
|
|
|||
Loading…
Reference in a new issue