Files
api/services/nginx/app/routes/superuserCoolifyRoute.php
T
OpenClaw 51a87655d6 feat(auth): add scope-based access control to all existing routes (TRU-149)
Adds a scope-based access control layer to all 81 existing API routes.
Sits alongside existing session-cookie auth (does not replace it).

What this PR does:
- Audits every existing route and documents required scope per route
  (see documentation/auth/route-scope-audit.md)
- Adds classes/auth/scope.php with 10 scope constants and role→scope defaults
- Adds classes/auth/scope_middleware.php with requireScope/requireAnyScope/requireRole
- Applies require*() calls to all 81 existing routes
- Adds ScopeMiddlewareTest (unit, 178 lines) and RouteScopeTest (integration, 212 lines)

Coexistence note:
This branch's classes/auth/scope.php is a stub that will be replaced
by classes/auth/scope_registry.php (from TRU-145 / PR #396) when that
PR merges first. The two have compatible APIs.

Refs: TRU-149
2026-08-17 11:43:13 +00:00

325 lines
14 KiB
PHP

<?php
namespace routes;
use classes\authentication;
use classes\coolify_manager;
use Throwable;
use traits\route_t;
use app\auth\Scope;
use app\auth\ScopeMiddleware;
class superuserCoolifyRoute
{
use route_t;
public function run(): void
{
$this->get('/superuser/coolify', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_READ, '/superuser/coolify');
global $response;
$this->requirePermission('superuser_coolify_view');
$response->success((new coolify_manager())->summary());
}, [
'superuser_coolify_view' => 'View Coolify-managed replicated infrastructure targets',
]);
$this->get('/superuser/coolify/load-balancer', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_READ, '/superuser/coolify/load-balancer');
global $response;
$this->requirePermission('superuser_coolify_view');
$response->success((new coolify_manager())->loadBalancerSummary());
}, [
'superuser_coolify_view' => 'View the Coolify public gateway Load Balancer state',
]);
$this->post('/superuser/coolify/load-balancer/reconcile', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_WRITE, '/superuser/coolify/load-balancer/reconcile');
global $response;
$this->requirePermission('superuser_coolify_manage');
try {
$parameters = $this->getParametersAsArray();
$dryRun = array_key_exists('dry_run', $parameters)
? filter_var($parameters['dry_run'], FILTER_VALIDATE_BOOLEAN)
: !filter_var($parameters['enforce'] ?? false, FILTER_VALIDATE_BOOLEAN);
$result = (new coolify_manager())->reconcileLoadBalancer($dryRun, $this->actorUserId());
if (($result['ok'] ?? false) !== true) {
$response->error($result, 409);
}
$response->success($result);
} catch (Throwable $throwable) {
$response->error(['message' => $throwable->getMessage()], 409);
}
}, [
'superuser_coolify_manage' => 'Reconcile Hetzner Load Balancer targets and services for the Coolify gateway',
]);
$this->post('/superuser/coolify/load-balancer/routes/deploy', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_WRITE, '/superuser/coolify/load-balancer/routes/deploy');
global $response;
$this->requirePermission('superuser_coolify_manage');
try {
$parameters = $this->getParametersAsArray();
$dryRun = array_key_exists('dry_run', $parameters)
? filter_var($parameters['dry_run'], FILTER_VALIDATE_BOOLEAN)
: !filter_var($parameters['enforce'] ?? false, FILTER_VALIDATE_BOOLEAN);
$result = (new coolify_manager())->deployGatewayApplicationRoutes($dryRun, $this->actorUserId());
if (($result['ok'] ?? false) !== true) {
$response->error($result, 409);
}
$response->success($result);
} catch (Throwable $throwable) {
$response->error(['message' => $throwable->getMessage()], 409);
}
}, [
'superuser_coolify_manage' => 'Deploy the Coolify API application route for the public gateway host',
]);
$this->post('/superuser/coolify/load-balancer/api/deploy', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_WRITE, '/superuser/coolify/load-balancer/api/deploy');
global $response;
$this->requirePermission('superuser_coolify_manage');
try {
$parameters = $this->getParametersAsArray();
$dryRun = array_key_exists('dry_run', $parameters)
? filter_var($parameters['dry_run'], FILTER_VALIDATE_BOOLEAN)
: !filter_var($parameters['enforce'] ?? false, FILTER_VALIDATE_BOOLEAN);
$deployRoutes = array_key_exists('deploy_routes', $parameters)
? filter_var($parameters['deploy_routes'], FILTER_VALIDATE_BOOLEAN)
: true;
$result = (new coolify_manager())->deployGatewayApiCode($dryRun, $deployRoutes, $this->actorUserId());
if (($result['ok'] ?? false) !== true) {
$response->error($result, 409);
}
$response->success($result);
} catch (Throwable $throwable) {
$response->error(['message' => $throwable->getMessage()], 409);
}
}, [
'superuser_coolify_manage' => 'Deploy the latest Coolify API code for the public gateway host',
]);
$this->get('/superuser/coolify/gateways', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_READ, '/superuser/coolify/gateways');
global $response;
$this->requirePermission('superuser_coolify_view');
$response->success((new coolify_manager())->listLoadBalancerGateways());
}, [
'superuser_coolify_view' => 'List Coolify public gateway Load Balancer targets',
]);
$this->post('/superuser/coolify/gateways', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_WRITE, '/superuser/coolify/gateways');
global $response;
$this->requirePermission('superuser_coolify_manage');
try {
$response->success((new coolify_manager())->saveLoadBalancerGateway(
$this->getParametersAsArray(),
$this->actorUserId()
), 201);
} catch (Throwable $throwable) {
$response->error(['message' => $throwable->getMessage()], 400);
}
}, [
'superuser_coolify_manage' => 'Create or update Coolify public gateway Load Balancer targets',
]);
$this->post('/superuser/coolify/gateways/{id}/test', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_WRITE, '/superuser/coolify/gateways/{id}/test');
global $response;
$this->requirePermission('superuser_coolify_manage');
try {
$response->success((new coolify_manager())->testLoadBalancerGateway(
$this->routeId(),
$this->actorUserId()
));
} catch (Throwable $throwable) {
$response->error(['message' => $throwable->getMessage()], 409);
}
}, [
'superuser_coolify_manage' => 'Probe an individual Coolify public gateway target',
]);
$this->post('/superuser/coolify/instances', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_WRITE, '/superuser/coolify/instances');
global $response;
$this->requirePermission('superuser_coolify_manage');
try {
$response->success((new coolify_manager())->createInstance(
$this->getParametersAsArray(),
$this->actorUserId()
), 201);
} catch (Throwable $throwable) {
$response->error(['message' => $throwable->getMessage()], 400);
}
}, [
'superuser_coolify_manage' => 'Create and update Coolify API connections',
]);
$this->post('/superuser/coolify/instances/{id}/test', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_WRITE, '/superuser/coolify/instances/{id}/test');
global $response;
$this->requirePermission('superuser_coolify_manage');
$response->success((new coolify_manager())->testInstance($this->routeId(), $this->actorUserId()));
}, [
'superuser_coolify_manage' => 'Test Coolify API connectivity',
]);
$this->get('/superuser/coolify/instances/{id}/placement', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_READ, '/superuser/coolify/instances/{id}/placement');
global $response;
$this->requirePermission('superuser_coolify_view');
try {
$response->success((new coolify_manager())->discoverInstancePlacement($this->routeId()));
} catch (Throwable $throwable) {
$response->error(['message' => $throwable->getMessage()], 502);
}
}, [
'superuser_coolify_view' => 'Discover Coolify projects, environments, and servers for target placement',
]);
$this->get('/superuser/coolify/targets', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_READ, '/superuser/coolify/targets');
global $response;
$this->requirePermission('superuser_coolify_view');
$kind = (string)($this->getParameter('kind') ?? '');
$response->success((new coolify_manager())->listTargets($kind !== '' ? $kind : null));
}, [
'superuser_coolify_view' => 'List Coolify-managed replication targets',
]);
$this->post('/superuser/coolify/targets', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_WRITE, '/superuser/coolify/targets');
global $response;
$this->requirePermission('superuser_coolify_manage');
try {
$response->success((new coolify_manager())->createTarget(
$this->getParametersAsArray(),
$this->actorUserId()
), 201);
} catch (Throwable $throwable) {
$response->error(['message' => $throwable->getMessage()], 409);
}
}, [
'superuser_coolify_manage' => 'Create Coolify-managed MariaDB, Redis, and MinIO replication targets',
]);
$this->post('/superuser/coolify/targets/{id}/reconcile', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_WRITE, '/superuser/coolify/targets/{id}/reconcile');
global $response;
$this->requirePermission('superuser_coolify_reconcile');
try {
$result = (new coolify_manager())->reconcileTarget($this->routeId(), $this->actorUserId());
if (($result['ok'] ?? false) !== true) {
$response->error($result, 409);
}
$response->success($result);
} catch (Throwable $throwable) {
$response->error(['message' => $throwable->getMessage()], 409);
}
}, [
'superuser_coolify_reconcile' => 'Reconcile expected Coolify deployment state without primary downtime',
]);
$this->post('/superuser/coolify/targets/{id}/deploy', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_WRITE, '/superuser/coolify/targets/{id}/deploy');
global $response;
$this->requirePermission('superuser_coolify_reconcile');
try {
$result = (new coolify_manager())->deployTarget($this->routeId(), $this->actorUserId());
if (($result['ok'] ?? false) !== true) {
$response->error($result, 409);
}
$response->success($result);
} catch (Throwable $throwable) {
$response->error(['message' => $throwable->getMessage()], 409);
}
}, [
'superuser_coolify_reconcile' => 'Deploy and provision a passive Coolify-managed replication target',
]);
$this->post('/superuser/coolify/targets/{id}/restart', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_WRITE, '/superuser/coolify/targets/{id}/restart');
global $response;
$this->requirePermission('superuser_coolify_reconcile');
try {
$result = (new coolify_manager())->restartTarget($this->routeId(), $this->actorUserId());
if (($result['ok'] ?? false) !== true) {
$response->error($result, 409);
}
$response->success($result);
} catch (Throwable $throwable) {
$response->error(['message' => $throwable->getMessage()], 409);
}
}, [
'superuser_coolify_reconcile' => 'Restart a passive Coolify target without restarting the active primary',
]);
$this->post('/superuser/coolify/targets/{id}/failover', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_WRITE, '/superuser/coolify/targets/{id}/failover');
global $response;
$this->requirePermission('superuser_coolify_failover');
try {
$response->success((new coolify_manager())->failoverTarget($this->routeId(), $this->actorUserId()));
} catch (Throwable $throwable) {
$response->error(['message' => $throwable->getMessage()], 409);
}
}, [
'superuser_coolify_failover' => 'Promote a healthy Coolify-managed replica through the replication module',
]);
$this->delete('/superuser/coolify/targets/{id}', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_WRITE, '/superuser/coolify/targets/{id}');
global $response;
$this->requirePermission('superuser_coolify_manage');
try {
$response->success((new coolify_manager())->deleteTarget(
$this->routeId(),
$this->getParametersAsArray(),
$this->actorUserId()
));
} catch (Throwable $throwable) {
$response->error(['message' => $throwable->getMessage()], 409);
}
}, [
'superuser_coolify_manage' => 'Delete Coolify target mappings with explicit destructive confirmation',
]);
}
private function routeId(): int
{
$id = (int)$this->fromRoute('id');
$this->requireParameterIntPositive($id, 'id');
return $id;
}
private function actorUserId(): ?int
{
try {
$user = (new authentication())->get_user();
return $user !== false && isset($user->id) ? (int)$user->id : null;
} catch (Throwable) {
return null;
}
}
}