106 lines
3.8 KiB
PHP
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;
|
|
}
|
|
}
|