Files
api/services/nginx/app/classes/auth/scope.php
T

106 lines
3.8 KiB
PHP

<?php
namespace app\auth;
/**
* Scope constants and helpers.
*
* LOCAL STUB for TRU-149 — will be replaced/extended by TRU-145
* (branch feat/api-key-foundation). Keeping this minimal so we
* don't conflict with the parallel scope-system work.
*
* Adding a new scope? Add the constant here AND register it in
* Scope::all() AND in Scope::forRole() (whichever roles should
* carry it). Centralising role → scope mapping here keeps
* permission decisions auditable in one place.
*/
class Scope
{
const CUSTOMER_READ = 'customer:read';
const CUSTOMER_WRITE = 'customer:write';
const BOOKING_READ = 'booking:read';
const BOOKING_WRITE = 'booking:write';
const SUBUSER_READ = 'subuser:read';
const SUBUSER_WRITE = 'subuser:write';
const INVOICE_READ = 'invoice:read';
const INVOICE_WRITE = 'invoice:write';
const SUPERUSER_READ = 'superuser:read';
const SUPERUSER_WRITE = 'superuser:write';
/**
* Canonical list of every scope. Used for validation and to
* build the audit trail of "what scopes exist" in tests.
*/
public static function all(): array
{
return [
self::CUSTOMER_READ, self::CUSTOMER_WRITE,
self::BOOKING_READ, self::BOOKING_WRITE,
self::SUBUSER_READ, self::SUBUSER_WRITE,
self::INVOICE_READ, self::INVOICE_WRITE,
self::SUPERUSER_READ, self::SUPERUSER_WRITE,
];
}
/**
* Return the scopes carried by a given role. Single source of
* truth for role-based scope assignment.
*/
public static function forRole(string $role): array
{
switch ($role) {
case 'superuser':
return self::all();
case 'admin':
return [
self::CUSTOMER_READ, self::CUSTOMER_WRITE,
self::BOOKING_READ, self::BOOKING_WRITE,
self::SUBUSER_READ, self::SUBUSER_WRITE,
self::INVOICE_READ, self::INVOICE_WRITE,
];
case 'customer':
// TRU-149 (fix): customers get WRITE on their own data so
// self-service endpoints (own vehicles, own subusers, own
// discount / security / notification settings, own bookings)
// work end-to-end. The existing fine-grained
// requirePermission() calls in each route still gate which
// specific actions are allowed — scope here only answers
// "can this caller write customer data at all".
return [
self::CUSTOMER_READ, self::CUSTOMER_WRITE,
self::BOOKING_READ, self::BOOKING_WRITE,
self::SUBUSER_READ, self::SUBUSER_WRITE,
self::INVOICE_READ,
];
case 'subuser':
return [self::BOOKING_READ, self::BOOKING_WRITE];
default:
return [];
}
}
/**
* Normalize/validate a scope string. Returns null on invalid input
* (empty string, non-string, or not in the canonical set).
*
* Wildcards: "*" matches every scope. "customer:*" matches every
* scope starting with "customer:". "customer:read" matches itself.
*/
public static function matches(string $granted, string $required): bool
{
$granted = trim($granted);
$required = trim($required);
if ($granted === '' || $required === '') {
return false;
}
if ($granted === '*') {
return true;
}
if (str_ends_with($granted, ':*')) {
$prefix = substr($granted, 0, -2);
return str_starts_with($required, $prefix . ':');
}
return $granted === $required;
}
}