Files
api/services/nginx/app/routes/departmentGatesRelaysRoute.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

354 lines
16 KiB
PHP

<?php
namespace routes;
use classes\department_gate_config;
use classes\department_relay_config;
use classes\authentication;
use classes\response;
use objects\department_gates_o;
use objects\department_relays_o;
use objects\departments_o;
use objects\logs_o;
use traits\route_t;
use app\auth\Scope;
use app\auth\ScopeMiddleware;
class departmentGatesRelaysRoute
{
use route_t;
public function run(): void
{
$this->get('/department/gates', function () {
ScopeMiddleware::requireScope(Scope::CUSTOMER_READ, '/department/gates');
global /** @var response $response */ $response;
$this->requirePermission('list_department_gates');
$user = (new authentication())->get_user();
if (!$user) {
$response->error('Invalid session', 400);
}
$gates_o = new department_gates_o();
if (self::isParametersSet(['id'])) {
self::requireType(self::getParameter('id'), self::type_int());
self::requireMinValue((int)self::getParameter('id'), 1);
$gate = (new department_gates_o())->select((int)self::getParameter('id'));
if (!$gate->exists()) {
$response->error('Department gate not found', 404);
}
self::requireDepartmentAccess((int)$gate->department->value());
$response->success($gate->asArray());
}
$result = $gates_o->listObjectsWithPaginationIfSet(
function ($tmp_object_array) {
return (new department_gates_o())->select((int)$tmp_object_array['id'])->asArray();
},
$gates_o->forceRestrictFilters([
'department' => $user->getGroup()->getDepartments(),
])
);
(new logs_o())->add('department_gates', 'global', 1, $user->id, 'LIST_DEPARTMENT_GATES', 'User listed department gates');
$response->success($result);
}, [
'list_department_gates' => 'List department gates in departments the user has access to',
]);
$this->post('/department/gates', function () {
ScopeMiddleware::requireScope(Scope::CUSTOMER_WRITE, '/department/gates');
global /** @var response $response */ $response;
$this->requirePermission('add_department_gate');
$user = (new authentication())->get_user();
if (!$user) {
$response->error('Invalid session', 400);
}
self::requireParameters(['department', 'is_entrance', 'is_exit', 'name', 'config']);
self::requireType(self::getParameter('department'), self::type_int());
self::requireMinValue((int)self::getParameter('department'), 1);
self::requireType(self::getParameter('is_entrance'), self::type_bool());
self::requireType(self::getParameter('is_exit'), self::type_bool());
self::requireType(self::getParameter('name'), self::type_string());
self::requireTypeIn(self::getParameter('config'), ['array', 'object']);
self::requireDepartmentAccess((int)self::getParameter('department'));
$department = (new departments_o())->select((int)self::getParameter('department'));
if (!$department->exists()) {
$response->error('Department not found', 404);
}
$config = self::getParameter('config');
$config = is_array($config) ? $config : (array)$config;
$gateConfig = new department_gate_config($config);
$gateConfig->validate();
$gate = (new department_gates_o())->add(
$department,
(bool)self::getParameter('is_entrance'),
(bool)self::getParameter('is_exit'),
(string)self::getParameter('name'),
$gateConfig,
);
(new logs_o())->add('department_gates', 'global', 1, $user->id, 'ADD_DEPARTMENT_GATE', 'User added department gate');
$response->success($gate->asArray(), 201);
}, [
'add_department_gate' => 'Add department gate in departments the user has access to',
]);
$this->put('/department/gates', function () {
ScopeMiddleware::requireScope(Scope::CUSTOMER_WRITE, '/department/gates');
global /** @var response $response */ $response;
$this->requirePermission('update_department_gate');
$user = (new authentication())->get_user();
if (!$user) {
$response->error('Invalid session', 400);
}
self::requireParameters(['id']);
self::requireType(self::getParameter('id'), self::type_int());
self::requireMinValue((int)self::getParameter('id'), 1);
$gate = (new department_gates_o())->select((int)self::getParameter('id'));
if (!$gate->exists()) {
$response->error('Department gate not found', 404);
}
self::requireDepartmentAccess((int)$gate->department->value());
$data = [];
if (self::isParametersSet(['is_entrance'])) {
self::requireType(self::getParameter('is_entrance'), self::type_bool());
$data['is_entrance'] = (bool)self::getParameter('is_entrance');
}
if (self::isParametersSet(['is_exit'])) {
self::requireType(self::getParameter('is_exit'), self::type_bool());
$data['is_exit'] = (bool)self::getParameter('is_exit');
}
if (self::isParametersSet(['name'])) {
self::requireType(self::getParameter('name'), self::type_string());
$data['name'] = (string)self::getParameter('name');
}
if (self::isParametersSet(['config'])) {
self::requireTypeIn(self::getParameter('config'), ['array', 'object']);
$config = self::getParameter('config');
$configArray = is_array($config) ? $config : (array)$config;
$gateConfig = new department_gate_config($configArray);
$gateConfig->validate();
$data['config'] = $gateConfig->toArray();
}
$nextIsEntrance = $data['is_entrance'] ?? (bool)$gate->is_entrance->value();
$nextIsExit = $data['is_exit'] ?? (bool)$gate->is_exit->value();
if (!$nextIsEntrance && !$nextIsExit) {
$response->error('Gate must be either entrance, exit or both', 400);
}
if (empty($data)) {
$response->error('No data to update', 400);
}
$gate->update($data);
(new logs_o())->add('department_gates', 'global', 1, $user->id, 'UPDATE_DEPARTMENT_GATE', 'User updated department gate');
$response->success($gate->asArray());
}, [
'update_department_gate' => 'Update department gate in departments the user has access to',
]);
$this->delete('/department/gates', function () {
ScopeMiddleware::requireScope(Scope::CUSTOMER_WRITE, '/department/gates');
global /** @var response $response */ $response;
$this->requirePermission('delete_department_gate');
$user = (new authentication())->get_user();
if (!$user) {
$response->error('Invalid session', 400);
}
self::requireParameters(['id']);
self::requireType(self::getParameter('id'), self::type_int());
self::requireMinValue((int)self::getParameter('id'), 1);
$gate = (new department_gates_o())->select((int)self::getParameter('id'));
if (!$gate->exists()) {
$response->error('Department gate not found', 404);
}
self::requireDepartmentAccess((int)$gate->department->value());
$gate->delete();
(new logs_o())->add('department_gates', 'global', 1, $user->id, 'DELETE_DEPARTMENT_GATE', 'User deleted department gate');
$response->success('Department gate deleted');
}, [
'delete_department_gate' => 'Delete department gate in departments the user has access to',
]);
$this->get('/department/relays', function () {
ScopeMiddleware::requireScope(Scope::CUSTOMER_READ, '/department/relays');
global /** @var response $response */ $response;
$this->requirePermission('list_department_relays');
$user = (new authentication())->get_user();
if (!$user) {
$response->error('Invalid session', 400);
}
$relays_o = new department_relays_o();
if (self::isParametersSet(['id'])) {
self::requireType(self::getParameter('id'), self::type_int());
self::requireMinValue((int)self::getParameter('id'), 1);
$relay = (new department_relays_o())->select((int)self::getParameter('id'));
if (!$relay->exists()) {
$response->error('Department relay not found', 404);
}
self::requireDepartmentAccess((int)$relay->department->value());
$response->success($relay->asArray());
}
$result = $relays_o->listObjectsWithPaginationIfSet(
function ($tmp_object_array) {
return (new department_relays_o())->select((int)$tmp_object_array['id'])->asArray();
},
$relays_o->forceRestrictFilters([
'department' => $user->getGroup()->getDepartments(),
])
);
(new logs_o())->add('department_relays', 'global', 1, $user->id, 'LIST_DEPARTMENT_RELAYS', 'User listed department relays');
$response->success($result);
}, [
'list_department_relays' => 'List department relays in departments the user has access to',
]);
$this->post('/department/relays', function () {
ScopeMiddleware::requireScope(Scope::CUSTOMER_WRITE, '/department/relays');
global /** @var response $response */ $response;
$this->requirePermission('add_department_relay');
$user = (new authentication())->get_user();
if (!$user) {
$response->error('Invalid session', 400);
}
self::requireParameters(['department', 'relay_id', 'name', 'type', 'config']);
self::requireType(self::getParameter('department'), self::type_int());
self::requireMinValue((int)self::getParameter('department'), 1);
self::requireType(self::getParameter('relay_id'), self::type_string());
self::requireType(self::getParameter('name'), self::type_string());
self::requireType(self::getParameter('type'), self::type_string());
self::requireTypeIn(self::getParameter('config'), ['array', 'object']);
self::requireDepartmentAccess((int)self::getParameter('department'));
$department = (new departments_o())->select((int)self::getParameter('department'));
if (!$department->exists()) {
$response->error('Department not found', 404);
}
$config = self::getParameter('config');
$config = is_array($config) ? $config : (array)$config;
$relayConfig = new department_relay_config($config);
$relay = (new department_relays_o())->add(
$department,
(string)self::getParameter('relay_id'),
(string)self::getParameter('name'),
(string)self::getParameter('type'),
$relayConfig,
);
(new logs_o())->add('department_relays', 'global', 1, $user->id, 'ADD_DEPARTMENT_RELAY', 'User added department relay');
$response->success($relay->asArray(), 201);
}, [
'add_department_relay' => 'Add department relay in departments the user has access to',
]);
$this->put('/department/relays', function () {
ScopeMiddleware::requireScope(Scope::CUSTOMER_WRITE, '/department/relays');
global /** @var response $response */ $response;
$this->requirePermission('update_department_relay');
$user = (new authentication())->get_user();
if (!$user) {
$response->error('Invalid session', 400);
}
self::requireParameters(['id']);
self::requireType(self::getParameter('id'), self::type_int());
self::requireMinValue((int)self::getParameter('id'), 1);
$relay = (new department_relays_o())->select((int)self::getParameter('id'));
if (!$relay->exists()) {
$response->error('Department relay not found', 404);
}
self::requireDepartmentAccess((int)$relay->department->value());
$data = [];
if (self::isParametersSet(['relay_id'])) {
self::requireType(self::getParameter('relay_id'), self::type_string());
$data['relay_id'] = (string)self::getParameter('relay_id');
}
if (self::isParametersSet(['name'])) {
self::requireType(self::getParameter('name'), self::type_string());
$data['name'] = (string)self::getParameter('name');
}
if (self::isParametersSet(['type'])) {
self::requireType(self::getParameter('type'), self::type_string());
$data['type'] = (string)self::getParameter('type');
}
if (self::isParametersSet(['config'])) {
self::requireTypeIn(self::getParameter('config'), ['array', 'object']);
$config = self::getParameter('config');
$configArray = is_array($config) ? $config : (array)$config;
$relayConfig = new department_relay_config($configArray);
$relayConfig->validate($data['type'] ?? (string)$relay->type->value());
$data['config'] = $relayConfig->toArray();
}
if (empty($data)) {
$response->error('No data to update', 400);
}
$relay->update($data);
(new logs_o())->add('department_relays', 'global', 1, $user->id, 'UPDATE_DEPARTMENT_RELAY', 'User updated department relay');
$response->success($relay->asArray());
}, [
'update_department_relay' => 'Update department relay in departments the user has access to',
]);
$this->delete('/department/relays', function () {
ScopeMiddleware::requireScope(Scope::CUSTOMER_WRITE, '/department/relays');
global /** @var response $response */ $response;
$this->requirePermission('delete_department_relay');
$user = (new authentication())->get_user();
if (!$user) {
$response->error('Invalid session', 400);
}
self::requireParameters(['id']);
self::requireType(self::getParameter('id'), self::type_int());
self::requireMinValue((int)self::getParameter('id'), 1);
$relay = (new department_relays_o())->select((int)self::getParameter('id'));
if (!$relay->exists()) {
$response->error('Department relay not found', 404);
}
self::requireDepartmentAccess((int)$relay->department->value());
$relay->delete();
(new logs_o())->add('department_relays', 'global', 1, $user->id, 'DELETE_DEPARTMENT_RELAY', 'User deleted department relay');
$response->success('Department relay deleted');
}, [
'delete_department_relay' => 'Delete department relay in departments the user has access to',
]);
}
}