mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(api): require deploy permission for proxy and custom log drain changes (#12198)
This commit is contained in:
@@ -108,7 +108,7 @@ class ServerLogDrainsController extends Controller
|
||||
|
||||
#[OA\Patch(
|
||||
summary: 'Update log drain settings',
|
||||
description: 'Update New Relic, Axiom, or custom log drain settings for a server owned by the authenticated team.',
|
||||
description: 'Update New Relic, Axiom, or custom log drain settings for a server owned by the authenticated team. Changing `logdrain_custom_config` or `logdrain_custom_config_parser` needs a token with the `deploy` permission.',
|
||||
path: '/servers/{uuid}/log-drains',
|
||||
operationId: 'update-server-log-drains',
|
||||
security: [['bearerAuth' => []]],
|
||||
@@ -186,6 +186,14 @@ class ServerLogDrainsController extends Controller
|
||||
}
|
||||
|
||||
$settings = $server->settings;
|
||||
|
||||
// The custom config is applied on the server, like a deployment.
|
||||
$customConfigChanged = collect(['logdrain_custom_config', 'logdrain_custom_config_parser'])
|
||||
->contains(fn (string $field) => $request->has($field) && (string) $request->input($field) !== (string) $settings->{$field});
|
||||
if ($customConfigChanged && ! $request->user()->tokenCan('deploy') && ! $request->user()->tokenCan('root')) {
|
||||
return response()->json(['message' => 'Missing required permissions: deploy'], 403);
|
||||
}
|
||||
|
||||
foreach (self::ALLOWED_FIELDS as $field) {
|
||||
if ($request->has($field)) {
|
||||
$settings->{$field} = $request->input($field);
|
||||
|
||||
@@ -270,7 +270,7 @@ class ServerProxyController extends Controller
|
||||
|
||||
#[OA\Put(
|
||||
summary: 'Save server proxy configuration',
|
||||
description: 'Save the raw proxy Docker Compose configuration for a team-owned server. Multi-line configuration must be base64 encoded (same pattern as other compose payloads).',
|
||||
description: 'Save the raw proxy Docker Compose configuration for a team-owned server. Needs a token with the `write` and `deploy` permissions. Multi-line configuration must be base64 encoded (same pattern as other compose payloads).',
|
||||
path: '/servers/{uuid}/proxy/configuration',
|
||||
operationId: 'save-server-proxy-configuration',
|
||||
security: [['bearerAuth' => []]],
|
||||
@@ -322,6 +322,11 @@ class ServerProxyController extends Controller
|
||||
return $teamId;
|
||||
}
|
||||
|
||||
// The proxy compose file is applied on the server, like a deployment.
|
||||
if (! $request->user()->tokenCan('deploy') && ! $request->user()->tokenCan('root')) {
|
||||
return response()->json(['message' => 'Missing required permissions: deploy'], 403);
|
||||
}
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
@@ -391,7 +396,7 @@ class ServerProxyController extends Controller
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Restart server proxy',
|
||||
description: 'Queue a proxy restart for a team-owned server.',
|
||||
description: 'Queue a proxy restart for a team-owned server. Needs a token with the `deploy` permission.',
|
||||
path: '/servers/{uuid}/proxy/restart',
|
||||
operationId: 'restart-server-proxy',
|
||||
security: [['bearerAuth' => []]],
|
||||
|
||||
+3
-3
@@ -16575,7 +16575,7 @@
|
||||
"Servers"
|
||||
],
|
||||
"summary": "Update log drain settings",
|
||||
"description": "Update New Relic, Axiom, or custom log drain settings for a server owned by the authenticated team.",
|
||||
"description": "Update New Relic, Axiom, or custom log drain settings for a server owned by the authenticated team. Changing `logdrain_custom_config` or `logdrain_custom_config_parser` needs a token with the `deploy` permission.",
|
||||
"operationId": "update-server-log-drains",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -16878,7 +16878,7 @@
|
||||
"Servers"
|
||||
],
|
||||
"summary": "Save server proxy configuration",
|
||||
"description": "Save the raw proxy Docker Compose configuration for a team-owned server. Multi-line configuration must be base64 encoded (same pattern as other compose payloads).",
|
||||
"description": "Save the raw proxy Docker Compose configuration for a team-owned server. Needs a token with the `write` and `deploy` permissions. Multi-line configuration must be base64 encoded (same pattern as other compose payloads).",
|
||||
"operationId": "save-server-proxy-configuration",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -16983,7 +16983,7 @@
|
||||
"Servers"
|
||||
],
|
||||
"summary": "Restart server proxy",
|
||||
"description": "Queue a proxy restart for a team-owned server.",
|
||||
"description": "Queue a proxy restart for a team-owned server. Needs a token with the `deploy` permission.",
|
||||
"operationId": "restart-server-proxy",
|
||||
"parameters": [
|
||||
{
|
||||
|
||||
+3
-3
@@ -10522,7 +10522,7 @@ paths:
|
||||
tags:
|
||||
- Servers
|
||||
summary: 'Update log drain settings'
|
||||
description: 'Update New Relic, Axiom, or custom log drain settings for a server owned by the authenticated team.'
|
||||
description: 'Update New Relic, Axiom, or custom log drain settings for a server owned by the authenticated team. Changing `logdrain_custom_config` or `logdrain_custom_config_parser` needs a token with the `deploy` permission.'
|
||||
operationId: update-server-log-drains
|
||||
parameters:
|
||||
-
|
||||
@@ -10676,7 +10676,7 @@ paths:
|
||||
tags:
|
||||
- Servers
|
||||
summary: 'Save server proxy configuration'
|
||||
description: 'Save the raw proxy Docker Compose configuration for a team-owned server. Multi-line configuration must be base64 encoded (same pattern as other compose payloads).'
|
||||
description: 'Save the raw proxy Docker Compose configuration for a team-owned server. Needs a token with the `write` and `deploy` permissions. Multi-line configuration must be base64 encoded (same pattern as other compose payloads).'
|
||||
operationId: save-server-proxy-configuration
|
||||
parameters:
|
||||
-
|
||||
@@ -10729,7 +10729,7 @@ paths:
|
||||
tags:
|
||||
- Servers
|
||||
summary: 'Restart server proxy'
|
||||
description: 'Queue a proxy restart for a team-owned server.'
|
||||
description: 'Queue a proxy restart for a team-owned server. Needs a token with the `deploy` permission.'
|
||||
operationId: restart-server-proxy
|
||||
parameters:
|
||||
-
|
||||
|
||||
+1
-1
@@ -202,7 +202,7 @@ Route::group([
|
||||
Route::get('/servers/{uuid}/proxy', [ServerProxyController::class, 'show'])->middleware(['api.ability:read']);
|
||||
Route::patch('/servers/{uuid}/proxy', [ServerProxyController::class, 'update'])->middleware(['api.ability:write']);
|
||||
Route::put('/servers/{uuid}/proxy/configuration', [ServerProxyController::class, 'saveConfiguration'])->middleware(['api.ability:write']);
|
||||
Route::post('/servers/{uuid}/proxy/restart', [ServerProxyController::class, 'restart'])->middleware(['api.ability:write']);
|
||||
Route::post('/servers/{uuid}/proxy/restart', [ServerProxyController::class, 'restart'])->middleware(['api.ability:deploy']);
|
||||
|
||||
Route::post('/servers', [ServersController::class, 'create_server'])->middleware(['api.ability:write']);
|
||||
Route::post('/servers/import', [ServerTransferController::class, 'import'])->middleware(['api.ability:write']);
|
||||
|
||||
@@ -268,6 +268,66 @@ test('PUT /api/v1/servers/{uuid}/proxy/configuration forbids normal members', fu
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('PUT /api/v1/servers/{uuid}/proxy/configuration requires the deploy permission', function () {
|
||||
$writeOnlyToken = serverProxyApiToken($this->user, $this->team, ['write']);
|
||||
$configuration = "services:\n traefik:\n image: traefik:v3.5\n";
|
||||
|
||||
SaveProxyConfiguration::shouldNotRun();
|
||||
|
||||
$this->withHeaders(serverProxyApiHeaders($writeOnlyToken))
|
||||
->putJson("/api/v1/servers/{$this->server->uuid}/proxy/configuration", [
|
||||
'configuration' => base64_encode($configuration),
|
||||
])
|
||||
->assertForbidden()
|
||||
->assertJsonPath('message', 'Missing required permissions: deploy');
|
||||
});
|
||||
|
||||
test('PUT /api/v1/servers/{uuid}/proxy/configuration saves with write and deploy permissions', function () {
|
||||
$deployToken = serverProxyApiToken($this->user, $this->team, ['write', 'deploy']);
|
||||
|
||||
SaveProxyConfiguration::shouldRun()->once();
|
||||
|
||||
$this->withHeaders(serverProxyApiHeaders($deployToken))
|
||||
->putJson("/api/v1/servers/{$this->server->uuid}/proxy/configuration", [
|
||||
'configuration' => base64_encode("services:\n traefik:\n image: traefik:v3.5\n"),
|
||||
])
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('PUT /api/v1/servers/{uuid}/proxy/configuration requires the write permission', function () {
|
||||
$deployOnlyToken = serverProxyApiToken($this->user, $this->team, ['deploy']);
|
||||
|
||||
SaveProxyConfiguration::shouldNotRun();
|
||||
|
||||
$this->withHeaders(serverProxyApiHeaders($deployOnlyToken))
|
||||
->putJson("/api/v1/servers/{$this->server->uuid}/proxy/configuration", [
|
||||
'configuration' => base64_encode("services:\n traefik:\n image: traefik:v3.5\n"),
|
||||
])
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('POST /api/v1/servers/{uuid}/proxy/restart requires the deploy permission', function () {
|
||||
Queue::fake();
|
||||
$writeOnlyToken = serverProxyApiToken($this->user, $this->team, ['write']);
|
||||
|
||||
$this->withHeaders(serverProxyApiHeaders($writeOnlyToken))
|
||||
->postJson("/api/v1/servers/{$this->server->uuid}/proxy/restart")
|
||||
->assertForbidden();
|
||||
|
||||
Queue::assertNotPushed(RestartProxyJob::class);
|
||||
});
|
||||
|
||||
test('POST /api/v1/servers/{uuid}/proxy/restart works with the deploy permission', function () {
|
||||
Queue::fake();
|
||||
$deployToken = serverProxyApiToken($this->user, $this->team, ['deploy']);
|
||||
|
||||
$this->withHeaders(serverProxyApiHeaders($deployToken))
|
||||
->postJson("/api/v1/servers/{$this->server->uuid}/proxy/restart")
|
||||
->assertOk();
|
||||
|
||||
Queue::assertPushed(RestartProxyJob::class);
|
||||
});
|
||||
|
||||
test('POST /api/v1/servers/{uuid}/proxy/restart queues RestartProxyJob', function () {
|
||||
Queue::fake();
|
||||
|
||||
|
||||
@@ -188,6 +188,58 @@ describe('Log drains API', function () {
|
||||
->and($settings->logdrain_axiom_api_key)->toBe('axiom-key-123');
|
||||
});
|
||||
|
||||
test('PATCH requires the deploy permission to change the custom log drain config', function (string $field) {
|
||||
Queue::fake();
|
||||
$writeOnlyToken = $this->user->createToken('server-subsystems-write', ['write'])->plainTextToken;
|
||||
|
||||
$this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$writeOnlyToken,
|
||||
'Accept' => 'application/json',
|
||||
])
|
||||
->patchJson("/api/v1/servers/{$this->server->uuid}/log-drains", [
|
||||
$field => "[OUTPUT]\n Name stdout",
|
||||
])
|
||||
->assertForbidden()
|
||||
->assertJsonPath('message', 'Missing required permissions: deploy');
|
||||
|
||||
$settings = $this->server->settings->fresh();
|
||||
expect($settings->{$field})->toBeNull();
|
||||
})->with(['logdrain_custom_config', 'logdrain_custom_config_parser']);
|
||||
|
||||
test('PATCH lets a write token enable the custom log drain with its saved config', function () {
|
||||
Queue::fake();
|
||||
$this->server->settings->update(['logdrain_custom_config' => "[OUTPUT]\n Name stdout"]);
|
||||
$writeOnlyToken = $this->user->createToken('server-subsystems-write', ['write'])->plainTextToken;
|
||||
|
||||
$this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$writeOnlyToken,
|
||||
'Accept' => 'application/json',
|
||||
])
|
||||
->patchJson("/api/v1/servers/{$this->server->uuid}/log-drains", [
|
||||
'is_logdrain_custom_enabled' => true,
|
||||
'logdrain_custom_config' => "[OUTPUT]\n Name stdout",
|
||||
])
|
||||
->assertOk();
|
||||
|
||||
expect($this->server->settings->fresh()->is_logdrain_custom_enabled)->toBeTruthy();
|
||||
});
|
||||
|
||||
test('PATCH changes the custom log drain config with the deploy permission', function () {
|
||||
Queue::fake();
|
||||
$deployToken = $this->user->createToken('server-subsystems-deploy', ['write', 'deploy'])->plainTextToken;
|
||||
|
||||
$this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$deployToken,
|
||||
'Accept' => 'application/json',
|
||||
])
|
||||
->patchJson("/api/v1/servers/{$this->server->uuid}/log-drains", [
|
||||
'logdrain_custom_config' => "[OUTPUT]\n Name stdout",
|
||||
])
|
||||
->assertOk();
|
||||
|
||||
expect($this->server->settings->fresh()->logdrain_custom_config)->toBe("[OUTPUT]\n Name stdout");
|
||||
});
|
||||
|
||||
test('other-team log drains endpoints return 404', function () {
|
||||
$this->withHeaders(serverSubsystemsHeaders())
|
||||
->getJson("/api/v1/servers/{$this->otherServer->uuid}/log-drains")
|
||||
|
||||
Reference in New Issue
Block a user