*/ 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 default scope set carried by a role. Wildcards are * returned as-is; resolve them with `expand()` before checking * membership if you need a flat list. * * @return array */ public static function scopesForRole(string $role): array { switch (strtolower(trim($role))) { case 'superuser': return ['*']; case 'admin': return [ 'customer:*', 'booking:*', 'subuser:*', 'invoice:*', ]; case 'customer': return [ self::CUSTOMER_READ, self::BOOKING_READ, self::INVOICE_READ, ]; case 'subuser': return [ self::BOOKING_READ, self::BOOKING_WRITE, ]; default: return []; } } /** * Does the granted scope (or wildcard) match the required scope? * * - "*" matches anything. * - "customer:*" matches "customer:read" and "customer:write". * - "customer:read" matches itself exactly. * * @param array $granted */ public static function hasScope(array $granted, string $required): bool { $required = trim($required); if ($required === '') { return false; } foreach ($granted as $candidate) { if (!is_string($candidate)) { continue; } if (self::matches($candidate, $required)) { return true; } } return false; } /** * Expand a list of scopes (which may include wildcards) into the * full set of concrete scopes they grant. Useful for showing a * user what their key can do, or for caching decisions. * * The wildcard "*" expands to the full `all()` set. A wildcard * like "customer:*" expands to every concrete scope starting with * "customer:". Duplicate entries are removed. * * @param array $scopes * @return array */ public static function expand(array $scopes): array { $concrete = self::all(); $expanded = []; foreach ($scopes as $scope) { if (!is_string($scope)) { continue; } $scope = trim($scope); if ($scope === '') { continue; } if ($scope === '*') { $expanded = array_merge($expanded, $concrete); continue; } if (str_ends_with($scope, ':*')) { $prefix = substr($scope, 0, -2) . ':'; foreach ($concrete as $candidate) { if (str_starts_with($candidate, $prefix)) { $expanded[] = $candidate; } } continue; } // Already concrete — pass through if it looks canonical. if (in_array($scope, $concrete, true)) { $expanded[] = $scope; } } return array_values(array_unique($expanded)); } /** * Internal wildcard matcher — public for testing. */ 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; } /** * Validate a scope string. Returns true iff the value is either * a canonical concrete scope, "*", or a ":*" wildcard * for a known resource. */ public static function isValid(string $scope): bool { $scope = trim($scope); if ($scope === '' || $scope === '*') { return $scope !== ''; } if (str_ends_with($scope, ':*')) { $prefix = substr($scope, 0, -2); foreach (self::all() as $concrete) { if (str_starts_with($concrete, $prefix . ':')) { return true; } } return false; } return in_array($scope, self::all(), true); } }