getMembership($user, $workspace); if ($membership === null) { return null; } return WorkspaceRole::tryFrom($membership['role']); } public function can(User $user, Workspace $workspace, string $capability): bool { if (! Capabilities::isKnown($capability)) { throw new \InvalidArgumentException("Unknown capability: {$capability}"); } $role = $this->getRole($user, $workspace); if ($role === null) { $this->logDenial($user, $workspace, $capability); return false; } $allowed = WorkspaceRoleCapabilityMap::hasCapability($role, $capability); if (! $allowed) { $this->logDenial($user, $workspace, $capability); } return $allowed; } public function isMember(User $user, Workspace $workspace): bool { return $this->getMembership($user, $workspace) !== null; } public function clearCache(): void { $this->resolvedMemberships = []; } private function logDenial(User $user, Workspace $workspace, string $capability): void { $key = implode(':', [(string) $user->getKey(), (string) $workspace->getKey(), $capability]); if (isset($this->loggedDenials[$key])) { return; } $this->loggedDenials[$key] = true; Log::warning('rbac.workspace.denied', [ 'capability' => $capability, 'workspace_id' => (int) $workspace->getKey(), 'actor_user_id' => (int) $user->getKey(), ]); } private function getMembership(User $user, Workspace $workspace): ?array { $cacheKey = "workspace_membership_{$user->id}_{$workspace->id}"; if (! isset($this->resolvedMemberships[$cacheKey])) { $membership = WorkspaceMembership::query() ->where('user_id', $user->id) ->where('workspace_id', $workspace->id) ->first(['role']); $this->resolvedMemberships[$cacheKey] = $membership?->toArray(); } return $this->resolvedMemberships[$cacheKey]; } }