Core move is deferred). */ class RoleResolver { public function __construct(private ChecksProjectAccess $projectAccess) {} /** The current user's global role string, or null when not authenticated. */ public function globalRole(): ?string { $role = session('userdata.role'); return ($role === null || $role === '') ? null : $role; } /** * Effective role for the current session project (delegates to the existing * dual-scope resolution). `$forceGlobal` short-circuits to the global role for * company-wide screens (users/clients/settings). */ public function effectiveRole(bool $forceGlobal = false): string|false { return AuthService::getRoleToCheck($forceGlobal); } /** * Effective role for a specific project. Manager/admin/owner keep their global role * everywhere; otherwise the explicit project role applies, falling back to the * global role when none is set. Returns false when not authenticated. */ public function effectiveRoleForProject(int $projectId): string|false { $globalRole = $this->globalRole(); if ($globalRole === null) { return false; } $roles = Roles::getRoles(); $globalKey = array_search($globalRole, $roles, true); $managerKey = array_search(Roles::$manager, $roles, true); // Manager and above keep their global role across every project. if ($globalKey !== false && $managerKey !== false && $globalKey >= $managerKey) { return $globalRole; } $projectRole = $this->projectAccess->getProjectRole((int) session('userdata.id'), $projectId); // No explicit project role -> inherit the global role. if ($projectRole === '') { return $globalRole; } // getProjectRole() returns either a numeric role key or, for legacy rows, a role name. // Resolve a numeric key to its role string; accept an already-valid role name as-is. Anything // that still can't be resolved falls back to the global role rather than denying a genuine // member (false -> 403 is worse than granting their inherited global role) (#3618). $resolvedRole = ctype_digit((string) $projectRole) ? Roles::getRoleString((int) $projectRole) : (in_array($projectRole, $roles, true) ? $projectRole : false); return $resolvedRole === false ? $globalRole : $resolvedRole; } /** * True when $effectiveRole ranks at or above $requiredRole in the role hierarchy, * using the same ordering as {@see Roles::getRoles()}. */ public function atLeast(string $requiredRole, string|false $effectiveRole): bool { if ($effectiveRole === false) { return false; } $roles = Roles::getRoles(); $requiredKey = array_search($requiredRole, $roles, true); $effectiveKey = array_search($effectiveRole, $roles, true); return $requiredKey !== false && $effectiveKey !== false && $effectiveKey >= $requiredKey; } }