|null> */ private array $scopeIdsByUserWorkspace = []; public function __construct( private readonly WorkspaceCapabilityResolver $workspaceCapabilityResolver, ) {} public function decision(User $user, ManagedEnvironment $tenant, ?string $requiredCapability = null): ManagedEnvironmentAccessDecision { $tenant = $this->hydrateTenantBoundary($tenant); $workspaceId = (int) ($tenant?->workspace_id ?? 0); $tenantId = (int) ($tenant?->getKey() ?? 0); if (! $tenant instanceof ManagedEnvironment || $workspaceId <= 0 || $tenantId <= 0) { return new ManagedEnvironmentAccessDecision( workspaceId: $workspaceId, managedEnvironmentId: $tenantId, userId: (int) $user->getKey(), workspaceMember: false, workspaceRole: null, explicitScopeRowsPresent: false, managedEnvironmentAllowed: false, failedBoundary: 'managed_environment_scope', requiredCapability: $requiredCapability, capabilityAllowed: false, denialHttpStatus: 404, ); } $workspace = Workspace::query()->whereKey($workspaceId)->first(); if (! $workspace instanceof Workspace) { return new ManagedEnvironmentAccessDecision( workspaceId: $workspaceId, managedEnvironmentId: $tenantId, userId: (int) $user->getKey(), workspaceMember: false, workspaceRole: null, explicitScopeRowsPresent: false, managedEnvironmentAllowed: false, failedBoundary: 'workspace_membership', requiredCapability: $requiredCapability, capabilityAllowed: false, denialHttpStatus: 404, ); } $workspaceRole = $this->workspaceCapabilityResolver->getRole($user, $workspace); if ($workspaceRole === null) { return new ManagedEnvironmentAccessDecision( workspaceId: $workspaceId, managedEnvironmentId: $tenantId, userId: (int) $user->getKey(), workspaceMember: false, workspaceRole: null, explicitScopeRowsPresent: false, managedEnvironmentAllowed: false, failedBoundary: 'workspace_membership', requiredCapability: $requiredCapability, capabilityAllowed: false, denialHttpStatus: 404, ); } $scopeIds = $this->scopeIdsForWorkspace($user, $workspaceId); $explicitScopeRowsPresent = $scopeIds !== null; $managedEnvironmentAllowed = $scopeIds === null || isset($scopeIds[$tenantId]); if (! $managedEnvironmentAllowed) { return new ManagedEnvironmentAccessDecision( workspaceId: $workspaceId, managedEnvironmentId: $tenantId, userId: (int) $user->getKey(), workspaceMember: true, workspaceRole: $workspaceRole->value, explicitScopeRowsPresent: true, managedEnvironmentAllowed: false, failedBoundary: 'managed_environment_scope', requiredCapability: $requiredCapability, capabilityAllowed: false, denialHttpStatus: 404, ); } $capabilityAllowed = true; if (is_string($requiredCapability) && $requiredCapability !== '') { if (! Capabilities::isKnown($requiredCapability)) { throw new \InvalidArgumentException("Unknown capability: {$requiredCapability}"); } $capabilityAllowed = WorkspaceRoleCapabilityMap::hasCapability($workspaceRole, $requiredCapability); } return new ManagedEnvironmentAccessDecision( workspaceId: $workspaceId, managedEnvironmentId: $tenantId, userId: (int) $user->getKey(), workspaceMember: true, workspaceRole: $workspaceRole->value, explicitScopeRowsPresent: $explicitScopeRowsPresent, managedEnvironmentAllowed: true, failedBoundary: $capabilityAllowed ? null : 'capability', requiredCapability: $requiredCapability, capabilityAllowed: $capabilityAllowed, denialHttpStatus: $capabilityAllowed ? null : 403, ); } public function canAccess(User $user, ManagedEnvironment $tenant): bool { $decision = $this->decision($user, $tenant); return $decision->workspaceMember && $decision->managedEnvironmentAllowed; } /** * Returns null when access is inherited across all environments in the workspace. * * @return array|null */ public function allowedManagedEnvironmentIdsForWorkspace(User $user, int $workspaceId): ?array { $workspace = Workspace::query()->whereKey($workspaceId)->first(); if (! $workspace instanceof Workspace || $this->workspaceCapabilityResolver->getRole($user, $workspace) === null) { return []; } $scopeIds = $this->scopeIdsForWorkspace($user, $workspaceId); if ($scopeIds === null) { return null; } return array_map('intval', array_keys($scopeIds)); } /** * @param array $tenantIds */ public function prime(User $user, array $tenantIds): void { $tenantIds = array_values(array_unique(array_map(static fn ($id): int => (int) $id, $tenantIds))); if ($tenantIds === []) { return; } $tenants = ManagedEnvironment::query() ->whereIn('id', $tenantIds) ->get(['id', 'workspace_id']); $workspaceIds = $tenants ->pluck('workspace_id') ->filter(fn ($id): bool => is_numeric($id)) ->map(fn ($id): int => (int) $id) ->unique() ->values() ->all(); $this->workspaceCapabilityResolver->primeMemberships($user, $workspaceIds); foreach ($workspaceIds as $workspaceId) { $this->scopeIdsForWorkspace($user, $workspaceId); } } public function clearCache(): void { $this->scopeIdsByUserWorkspace = []; } public function applyWorkspaceScopeToQuery(Builder $query, User $user, int $workspaceId, string $qualifiedEnvironmentColumn): Builder { $allowedIds = $this->allowedManagedEnvironmentIdsForWorkspace($user, $workspaceId); if ($allowedIds === []) { return $query->whereRaw('1 = 0'); } if ($allowedIds === null) { return $query; } return $query->whereIn($qualifiedEnvironmentColumn, $allowedIds); } /** * Null means inherited access; an array means explicit allowlist. * * @return array|null */ private function scopeIdsForWorkspace(User $user, int $workspaceId): ?array { $cacheKey = $this->scopeCacheKey($user, $workspaceId); if (array_key_exists($cacheKey, $this->scopeIdsByUserWorkspace)) { return $this->scopeIdsByUserWorkspace[$cacheKey]; } $scopeIds = ManagedEnvironmentMembership::query() ->join('managed_environments', 'managed_environments.id', '=', 'managed_environment_memberships.managed_environment_id') ->where('managed_environment_memberships.user_id', (int) $user->getKey()) ->where('managed_environments.workspace_id', $workspaceId) ->pluck('managed_environment_memberships.managed_environment_id') ->map(fn ($id): int => (int) $id) ->unique() ->values(); $this->scopeIdsByUserWorkspace[$cacheKey] = $scopeIds->isEmpty() ? null : $scopeIds->mapWithKeys(fn (int $id): array => [$id => true])->all(); return $this->scopeIdsByUserWorkspace[$cacheKey]; } private function hydrateTenantBoundary(ManagedEnvironment $tenant): ?ManagedEnvironment { if ($tenant->exists && $tenant->workspace_id !== null) { return $tenant; } $tenantKey = $tenant->getKey(); if (! is_numeric($tenantKey)) { return null; } return ManagedEnvironment::query() ->withTrashed() ->whereKey((int) $tenantKey) ->first(['id', 'workspace_id']); } private function scopeCacheKey(User $user, int $workspaceId): string { return implode(':', [(string) $user->getKey(), (string) $workspaceId]); } }