fix(api): expose nested server secrets for privileged tokens
Add `exposeNestedServerSecrets()` to Applications, Databases, and Services controllers so that `read:sensitive`/`root` tokens see sentinel and logdrain fields on eager-loaded Server + ServerSetting relations. ServicesController handles both single models and Eloquent Collections (listing endpoint passes a Collection per project). Tests tightened to use JSON-key assertions (`"field":`) to avoid false positives from field names appearing in values.
This commit is contained in:
parent
e828058755
commit
8dc79f4ed6
4 changed files with 116 additions and 6 deletions
|
|
@ -57,6 +57,7 @@ private function removeSensitiveData($application)
|
||||||
'value',
|
'value',
|
||||||
'real_value',
|
'real_value',
|
||||||
]);
|
]);
|
||||||
|
$this->exposeNestedServerSecrets($application);
|
||||||
} else {
|
} else {
|
||||||
$application->makeHidden([
|
$application->makeHidden([
|
||||||
'private_key_id',
|
'private_key_id',
|
||||||
|
|
@ -66,6 +67,34 @@ private function removeSensitiveData($application)
|
||||||
return serializeApiResponse($application);
|
return serializeApiResponse($application);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expose sensitive fields on eager-loaded nested Server + ServerSetting
|
||||||
|
* relations for callers with the `read:sensitive` or `root` token ability.
|
||||||
|
* Models hide these by default via $hidden; this re-exposes them per-request.
|
||||||
|
*/
|
||||||
|
private function exposeNestedServerSecrets($model): void
|
||||||
|
{
|
||||||
|
$server = $model->destination?->server ?? null;
|
||||||
|
if (! $server) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$server->makeVisible([
|
||||||
|
'logdrain_axiom_api_key',
|
||||||
|
'logdrain_newrelic_license_key',
|
||||||
|
]);
|
||||||
|
$settings = $server->settings ?? null;
|
||||||
|
if ($settings) {
|
||||||
|
$settings->makeVisible([
|
||||||
|
'sentinel_token',
|
||||||
|
'sentinel_custom_url',
|
||||||
|
'logdrain_newrelic_license_key',
|
||||||
|
'logdrain_axiom_api_key',
|
||||||
|
'logdrain_custom_config',
|
||||||
|
'logdrain_custom_config_parser',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[OA\Get(
|
#[OA\Get(
|
||||||
summary: 'List',
|
summary: 'List',
|
||||||
description: 'List all applications.',
|
description: 'List all applications.',
|
||||||
|
|
|
||||||
|
|
@ -49,11 +49,39 @@ private function removeSensitiveData($database)
|
||||||
'mariadb_password',
|
'mariadb_password',
|
||||||
'mariadb_root_password',
|
'mariadb_root_password',
|
||||||
]);
|
]);
|
||||||
|
$this->exposeNestedServerSecrets($database);
|
||||||
}
|
}
|
||||||
|
|
||||||
return serializeApiResponse($database);
|
return serializeApiResponse($database);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expose sensitive fields on eager-loaded nested Server + ServerSetting
|
||||||
|
* relations for callers with the `read:sensitive` or `root` token ability.
|
||||||
|
*/
|
||||||
|
private function exposeNestedServerSecrets($model): void
|
||||||
|
{
|
||||||
|
$server = $model->destination?->server ?? null;
|
||||||
|
if (! $server) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$server->makeVisible([
|
||||||
|
'logdrain_axiom_api_key',
|
||||||
|
'logdrain_newrelic_license_key',
|
||||||
|
]);
|
||||||
|
$settings = $server->settings ?? null;
|
||||||
|
if ($settings) {
|
||||||
|
$settings->makeVisible([
|
||||||
|
'sentinel_token',
|
||||||
|
'sentinel_custom_url',
|
||||||
|
'logdrain_newrelic_license_key',
|
||||||
|
'logdrain_axiom_api_key',
|
||||||
|
'logdrain_custom_config',
|
||||||
|
'logdrain_custom_config_parser',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[OA\Get(
|
#[OA\Get(
|
||||||
summary: 'List',
|
summary: 'List',
|
||||||
description: 'List all databases.',
|
description: 'List all databases.',
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
use App\Support\ValidationPatterns;
|
use App\Support\ValidationPatterns;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
use OpenApi\Attributes as OA;
|
use OpenApi\Attributes as OA;
|
||||||
use Symfony\Component\Yaml\Yaml;
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
|
@ -37,11 +38,48 @@ private function removeSensitiveData($service)
|
||||||
'value',
|
'value',
|
||||||
'real_value',
|
'real_value',
|
||||||
]);
|
]);
|
||||||
|
$this->exposeNestedServerSecrets($service);
|
||||||
}
|
}
|
||||||
|
|
||||||
return serializeApiResponse($service);
|
return serializeApiResponse($service);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expose sensitive fields on eager-loaded nested Server + ServerSetting
|
||||||
|
* relations for callers with the `read:sensitive` or `root` token ability.
|
||||||
|
* Handles both single models and Eloquent Collections (the listing endpoint
|
||||||
|
* passes a Collection of Services per project to removeSensitiveData()).
|
||||||
|
*/
|
||||||
|
private function exposeNestedServerSecrets($model): void
|
||||||
|
{
|
||||||
|
if ($model instanceof Collection || $model instanceof \Illuminate\Database\Eloquent\Collection) {
|
||||||
|
foreach ($model as $item) {
|
||||||
|
$this->exposeNestedServerSecrets($item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$server = $model->destination?->server ?? $model->server ?? null;
|
||||||
|
if (! $server) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$server->makeVisible([
|
||||||
|
'logdrain_axiom_api_key',
|
||||||
|
'logdrain_newrelic_license_key',
|
||||||
|
]);
|
||||||
|
$settings = $server->settings ?? null;
|
||||||
|
if ($settings) {
|
||||||
|
$settings->makeVisible([
|
||||||
|
'sentinel_token',
|
||||||
|
'sentinel_custom_url',
|
||||||
|
'logdrain_newrelic_license_key',
|
||||||
|
'logdrain_axiom_api_key',
|
||||||
|
'logdrain_custom_config',
|
||||||
|
'logdrain_custom_config_parser',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function applyServiceUrls(Service $service, array $urlsArray, string $teamId, bool $forceDomainOverride = false): ?array
|
private function applyServiceUrls(Service $service, array $urlsArray, string $teamId, bool $forceDomainOverride = false): ?array
|
||||||
{
|
{
|
||||||
$errors = [];
|
$errors = [];
|
||||||
|
|
|
||||||
|
|
@ -128,9 +128,23 @@ function makeTeamUser(): array
|
||||||
$response->assertStatus(200);
|
$response->assertStatus(200);
|
||||||
|
|
||||||
$body = $response->getContent();
|
$body = $response->getContent();
|
||||||
expect($body)->not->toContain('sentinel_token');
|
expect($body)->not->toContain('"sentinel_token":');
|
||||||
expect($body)->not->toContain('sentinel_custom_url');
|
expect($body)->not->toContain('"sentinel_custom_url":');
|
||||||
expect($body)->not->toContain('logdrain_axiom_api_key');
|
expect($body)->not->toContain('"logdrain_axiom_api_key":');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('read-sensitive token sees nested sentinel_token via destination.server.settings', function () {
|
||||||
|
$token = makeApiToken($this->user, $this->team, ['read', 'read:sensitive']);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer '.$token,
|
||||||
|
])->getJson('/api/v1/applications');
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
|
||||||
|
$body = $response->getContent();
|
||||||
|
expect($body)->toContain('"sentinel_token":');
|
||||||
|
expect($body)->toContain('"sentinel_custom_url":');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -168,7 +182,7 @@ function makeTeamUser(): array
|
||||||
expect($body)->not->toContain('external_db_url');
|
expect($body)->not->toContain('external_db_url');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('read sensitive token sees postgres_password', function () {
|
test('read sensitive token sees postgres_password and nested sentinel_token', function () {
|
||||||
$token = makeApiToken($this->user, $this->team, ['read', 'read:sensitive']);
|
$token = makeApiToken($this->user, $this->team, ['read', 'read:sensitive']);
|
||||||
|
|
||||||
$response = $this->withHeaders([
|
$response = $this->withHeaders([
|
||||||
|
|
@ -178,7 +192,8 @@ function makeTeamUser(): array
|
||||||
$response->assertStatus(200);
|
$response->assertStatus(200);
|
||||||
|
|
||||||
$body = $response->getContent();
|
$body = $response->getContent();
|
||||||
expect($body)->toContain('postgres_password');
|
expect($body)->toContain('"postgres_password":');
|
||||||
expect($body)->toContain('internal_db_url');
|
expect($body)->toContain('"internal_db_url":');
|
||||||
|
expect($body)->toContain('"sentinel_token":');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue