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 = []; } /** * Prime workspace membership cache for a set of workspaces in one query. * * @param array $workspaceIds */ public function primeMemberships(User $user, array $workspaceIds): void { $workspaceIds = array_values(array_unique(array_map(static fn ($id): int => (int) $id, $workspaceIds))); if ($workspaceIds === []) { return; } $memberships = WorkspaceMembership::query() ->where('user_id', (int) $user->getKey()) ->whereIn('workspace_id', $workspaceIds) ->get(['workspace_id', 'role']); $byWorkspaceId = $memberships->keyBy('workspace_id'); foreach ($workspaceIds as $workspaceId) { $cacheKey = "workspace_membership_{$user->id}_{$workspaceId}"; $membership = $byWorkspaceId->get($workspaceId); $this->resolvedMemberships[$cacheKey] = $membership?->toArray(); } } 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 (! array_key_exists($cacheKey, $this->resolvedMemberships)) { $membership = WorkspaceMembership::query() ->where('user_id', $user->id) ->where('workspace_id', $workspace->id) ->first(['role']); $this->resolvedMemberships[$cacheKey] = $membership?->toArray(); } return $this->resolvedMemberships[$cacheKey]; } }