mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(mcp): show commands and configuration to read:sensitive tokens
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
afe8fc05ea
commit
e7a46bd790
@@ -80,6 +80,20 @@ trait ResolvesTeam
|
||||
return $this->mcpError($request, "{$action} requires confirm=true.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Same rule as the REST API (ApiSensitiveData): a root or read:sensitive token of a team admin or owner.
|
||||
*/
|
||||
protected function canReadSensitiveData(Request $request): bool
|
||||
{
|
||||
$user = $request->user();
|
||||
$token = $user?->currentAccessToken();
|
||||
if (! $token || ! ($token->can('root') || $token->can('read:sensitive'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $token->team_id !== null && $user->isAdminOfTeam((int) $token->team_id);
|
||||
}
|
||||
|
||||
protected function resolveTeamId(Request $request): ?int
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
@@ -16,7 +16,7 @@ class GetApplication extends Tool
|
||||
{
|
||||
protected string $name = 'get_application';
|
||||
|
||||
protected string $description = 'Get full details for a single application by UUID.';
|
||||
protected string $description = 'Get full details for a single application by UUID. Commands and custom configuration (build, start, deployment, and healthcheck commands, docker run options, nginx configuration) need read:sensitive.';
|
||||
|
||||
use BuildsResponse;
|
||||
use ResolvesTeam;
|
||||
@@ -47,8 +47,11 @@ class GetApplication extends Tool
|
||||
$application->setRelations([]);
|
||||
$application->makeHidden(['destination', 'source', 'additional_servers', 'environment', 'tags', 'environmentVariables']);
|
||||
|
||||
$visibleSensitiveKeys = $this->canReadSensitiveData($request) ? Application::COMMAND_AND_CONFIGURATION_ATTRIBUTES : [];
|
||||
$application->makeVisible($visibleSensitiveKeys);
|
||||
|
||||
return $this->mcpSuccess($request, $this->respond(
|
||||
$this->scrubSensitive($application->toArray()),
|
||||
$this->scrubSensitive($application->toArray(), $visibleSensitiveKeys),
|
||||
$this->actionsForApplication($uuid, $application->status),
|
||||
), ['resource_uuid' => $uuid]);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ class GetDatabase extends Tool
|
||||
{
|
||||
protected string $name = 'get_database';
|
||||
|
||||
protected string $description = 'Get full details for a standalone database by UUID. Detects type across postgresql, mysql, mariadb, mongodb, redis, keydb, dragonfly, clickhouse, sqlite.';
|
||||
protected string $description = 'Get full details for a standalone database by UUID. Detects type across postgresql, mysql, mariadb, mongodb, redis, keydb, dragonfly, clickhouse, sqlite. Custom configuration (database config file, docker run options) needs read:sensitive.';
|
||||
|
||||
use BuildsResponse;
|
||||
use ResolvesTeam;
|
||||
@@ -45,8 +45,11 @@ class GetDatabase extends Tool
|
||||
$database->setRelations([]);
|
||||
$database->makeHidden(['destination', 'source', 'environment', 'environment_variables', 'environment_variables_preview']);
|
||||
|
||||
$visibleSensitiveKeys = $this->canReadSensitiveData($request) ? $database::CONFIGURATION_ATTRIBUTES : [];
|
||||
$database->makeVisible($visibleSensitiveKeys);
|
||||
|
||||
return $this->mcpSuccess($request, $this->respond(
|
||||
$this->scrubSensitive($database->toArray()),
|
||||
$this->scrubSensitive($database->toArray(), $visibleSensitiveKeys),
|
||||
$this->actionsForDatabase($uuid, $database->status ?? null),
|
||||
), ['resource_uuid' => $uuid]);
|
||||
}
|
||||
|
||||
@@ -644,6 +644,66 @@ test('get_application omits free-form configuration that can contain secrets', f
|
||||
->and($raw)->not->toContain('custom_nginx_configuration');
|
||||
});
|
||||
|
||||
function mcpReadCallWithAbilities(array $abilities, string $name, array $arguments = [])
|
||||
{
|
||||
auth()->forgetGuards();
|
||||
|
||||
return test()->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json, text/event-stream',
|
||||
'Authorization' => 'Bearer '.test()->user->createToken('mcp-abilities', $abilities)->plainTextToken,
|
||||
])->postJson('/mcp', [
|
||||
'jsonrpc' => '2.0',
|
||||
'id' => 1,
|
||||
'method' => 'tools/call',
|
||||
'params' => ['name' => $name, 'arguments' => (object) $arguments],
|
||||
]);
|
||||
}
|
||||
|
||||
test('get_application returns commands and configuration to a read:sensitive or root token of an owner', function (array $abilities) {
|
||||
$this->application->update([
|
||||
'build_command' => 'npm run build',
|
||||
'post_deployment_command' => 'php artisan migrate',
|
||||
'custom_docker_run_options' => '--cap-add SYS_ADMIN',
|
||||
]);
|
||||
|
||||
$response = mcpReadCallWithAbilities($abilities, 'get_application', ['uuid' => $this->application->uuid]);
|
||||
$response->assertOk();
|
||||
|
||||
expect(mcpReadJson($response)['data'])
|
||||
->toMatchArray([
|
||||
'build_command' => 'npm run build',
|
||||
'post_deployment_command' => 'php artisan migrate',
|
||||
'custom_docker_run_options' => '--cap-add SYS_ADMIN',
|
||||
])
|
||||
->not->toHaveKey('git_full_url');
|
||||
})->with([
|
||||
'read:sensitive' => [['read', 'read:sensitive']],
|
||||
'root' => [['root']],
|
||||
]);
|
||||
|
||||
test('get_database returns custom configuration to a read:sensitive token of an owner', function () {
|
||||
$database = StandalonePostgresql::create([
|
||||
'name' => 'Configured database',
|
||||
'postgres_password' => 'database-password',
|
||||
'postgres_conf' => 'max_connections = 200',
|
||||
'custom_docker_run_options' => '--shm-size 1g',
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
]);
|
||||
|
||||
$response = mcpSensitiveReadCall('get_database', ['uuid' => $database->uuid]);
|
||||
$response->assertOk();
|
||||
|
||||
expect(mcpReadJson($response)['data'])
|
||||
->toMatchArray([
|
||||
'postgres_conf' => 'max_connections = 200',
|
||||
'custom_docker_run_options' => '--shm-size 1g',
|
||||
])
|
||||
->not->toHaveKey('postgres_password');
|
||||
});
|
||||
|
||||
test('get_database omits configuration blobs that can contain secrets', function () {
|
||||
$database = StandalonePostgresql::create([
|
||||
'name' => 'Secret-bearing config',
|
||||
|
||||
Reference in New Issue
Block a user