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
This commit is contained in:
OpenClaw
2026-08-17 11:43:13 +00:00
parent 935b2d58ce
commit 51a87655d6
86 changed files with 1872 additions and 0 deletions
@@ -24,6 +24,9 @@ use objects\products_o;
use objects\users_o;
use traits\route_t;
use app\auth\Scope;
use app\auth\ScopeMiddleware;
class InvoicingPeriodRoute
{
use route_t;
@@ -1412,6 +1415,7 @@ class InvoicingPeriodRoute
public function run(): void
{
$this->post('/superuser/invoicing/period/object-tree/canary', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_WRITE, '/superuser/invoicing/period/object-tree/canary');
global $response;
$this->requirePermission('superuser');
self::requireParameters(['enabled']);
@@ -1463,6 +1467,7 @@ class InvoicingPeriodRoute
]);
$this->get('/superuser/invoicing/period', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_READ, '/superuser/invoicing/period');
// Require the user to be logged in
global $response;
$this->requirePermission('superuser_invoicing_period');
@@ -1507,6 +1512,7 @@ class InvoicingPeriodRoute
);
$this->get('/superuser/invoicing/period/tree', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_READ, '/superuser/invoicing/period/tree');
global $response;
$this->requirePermission('superuser_invoicing_period');
$user = (new authentication())->get_user();
@@ -1546,6 +1552,7 @@ class InvoicingPeriodRoute
);
$this->post('/superuser/invoicing/period/flags', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_WRITE, '/superuser/invoicing/period/flags');
global $response;
$this->requirePermission('add_invoice_period_flag');
$user = (new authentication())->get_user();
@@ -1571,6 +1578,7 @@ class InvoicingPeriodRoute
);
$this->patch('/superuser/invoicing/period/flags/{id}/status', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_WRITE, '/superuser/invoicing/period/flags/{id}/status');
global $response;
$this->requirePermission('update_invoice_period_flag_status');
$user = (new authentication())->get_user();
@@ -1599,6 +1607,7 @@ class InvoicingPeriodRoute
);
$this->post('/superuser/invoicing/period/flags/automatic/status', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_WRITE, '/superuser/invoicing/period/flags/automatic/status');
global $response;
$this->requirePermission('update_invoice_period_flag_status');
$user = (new authentication())->get_user();
@@ -1625,6 +1634,7 @@ class InvoicingPeriodRoute
$this->get('/superuser/invoicing/period/distribution/all', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_READ, '/superuser/invoicing/period/distribution/all');
// Require the user to be logged in
global $response;
$this->requirePermission('superuser_invoicing_period');
@@ -1647,6 +1657,7 @@ class InvoicingPeriodRoute
);
$this->get('/superuser/invoicing/period/distribution/fixed-pricing', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_READ, '/superuser/invoicing/period/distribution/fixed-pricing');
// Require the user to be logged in
global $response;
$this->requirePermission('superuser_invoicing_period');
@@ -1664,6 +1675,7 @@ class InvoicingPeriodRoute
);
$this->get('/superuser/invoicing/period/distribution/wash-subscriptions', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_READ, '/superuser/invoicing/period/distribution/wash-subscriptions');
// Require the user to be logged in
global $response;
$this->requirePermission('superuser_invoicing_period');
@@ -1681,6 +1693,7 @@ class InvoicingPeriodRoute
);
$this->get('/superuser/invoicing/period/distribution/v2/all', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_READ, '/superuser/invoicing/period/distribution/v2/all');
global $response;
$this->requirePermission('superuser_invoicing_period_distribution_v2');
$dateRange = $this->requireAndNormalizeDateRange();
@@ -1699,6 +1712,7 @@ class InvoicingPeriodRoute
);
$this->get('/superuser/invoicing/period/distribution/v2/fixed-pricing', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_READ, '/superuser/invoicing/period/distribution/v2/fixed-pricing');
global $response;
$this->requirePermission('superuser_invoicing_period_distribution_v2');
$dateRange = $this->requireAndNormalizeDateRange();
@@ -1717,6 +1731,7 @@ class InvoicingPeriodRoute
);
$this->get('/superuser/invoicing/period/distribution/v2/wash-subscriptions', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_READ, '/superuser/invoicing/period/distribution/v2/wash-subscriptions');
global $response;
$this->requirePermission('superuser_invoicing_period_distribution_v2');
$dateRange = $this->requireAndNormalizeDateRange();
@@ -1735,6 +1750,7 @@ class InvoicingPeriodRoute
);
$this->get('/superuser/invoicing/period/distribution/v2/customer-prices', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_READ, '/superuser/invoicing/period/distribution/v2/customer-prices');
global $response;
$this->requirePermission('superuser_invoicing_period_distribution_v2');
$dateRange = $this->requireAndNormalizeDateRange();
@@ -1753,6 +1769,7 @@ class InvoicingPeriodRoute
);
$this->get('/superuser/invoicing/period/distribution/v2/booked-department-75', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_READ, '/superuser/invoicing/period/distribution/v2/booked-department-75');
global $response;
$this->requirePermission('superuser_invoicing_period_distribution_v2');
$dateRange = $this->requireAndNormalizeDateRange();
@@ -1771,6 +1788,7 @@ class InvoicingPeriodRoute
);
$this->get('/superuser/customers/pricing-history', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_READ, '/superuser/customers/pricing-history');
global $response;
$this->requirePermission('superuser_customer_pricing_history_v2');
self::requireParameters(['customer_number']);
@@ -1832,6 +1850,7 @@ class InvoicingPeriodRoute
);
$this->get('/superuser/invoicing/period/distribution/wash-subscriptions/historical', function () {
ScopeMiddleware::requireScope(Scope::SUPERUSER_READ, '/superuser/invoicing/period/distribution/wash-subscriptions/historical');
// Require the user to be logged in
global $response;
$this->requirePermission('superuser_invoicing_period');