canBypass($userId)) { $ids = $this->tenantRepository->listIds(); } else { $ids = array_values(array_unique(array_map( 'intval', $this->userTenantRepository->listTenantIdsByUserId($userId) ))); } if ($onlyActive) { return $this->filterActiveTenantIds($ids); } return $ids; } public function canAccess(string $resource, int $resourceId, int $userId): bool { if ($resourceId <= 0 || $userId <= 0) { return false; } if ($this->canBypass($userId)) { return true; } $resourceTenantIdsRaw = $this->getResourceTenantIds($resource, $resourceId); $userTenantIdsRaw = $this->getUserTenantIds($userId, false); $resourceTenantIds = $this->filterActiveTenantIds($resourceTenantIdsRaw); $userTenantIds = $this->filterActiveTenantIds($userTenantIdsRaw); // Resource had tenants, but all are inactive — deny access rather than treating it as unscoped. if ($resourceTenantIdsRaw && !$resourceTenantIds) { return false; } // Resource has no tenant assignment — allow in permissive mode, deny in strict mode. if (!$resourceTenantIds) { return $this->isStrict() ? false : true; } if (!$userTenantIds) { return false; } return (bool) array_intersect($resourceTenantIds, $userTenantIds); } public function filterTenantIdsForUser(array $tenantIds, int $userId): array { $tenantIds = toIntIds($tenantIds); $userTenantIds = $this->getUserTenantIds($userId); if (!$userTenantIds) { return []; } return array_values(array_intersect($tenantIds, $userTenantIds)); } public function mergeTenantIdsPreservingOutOfScope( array $requestedTenantIds, array $existingTenantIds, array $allowedTenantIds ): array { $requestedTenantIds = toIntIds($requestedTenantIds); $existingTenantIds = toIntIds($existingTenantIds); $allowedTenantIds = toIntIds($allowedTenantIds); if (!$allowedTenantIds) { return $existingTenantIds; } $outOfScope = array_values(array_diff($existingTenantIds, $allowedTenantIds)); return array_values(array_unique(array_merge($requestedTenantIds, $outOfScope))); } public function hasGlobalAccess(int $userId): bool { return $this->canBypass($userId); } public function resourceBelongsToTenant(string $resource, int $resourceId, int $tenantId): bool { if ($resourceId <= 0 || $tenantId <= 0) { return false; } $resourceTenantIds = $this->getResourceTenantIds($resource, $resourceId); if (!$resourceTenantIds) { return false; } $activeResourceTenantIds = $this->filterActiveTenantIds($resourceTenantIds); if (!$activeResourceTenantIds) { return false; } return in_array($tenantId, $activeResourceTenantIds, true); } private function getResourceTenantIds(string $resource, int $resourceId): array { $resource = strtolower(trim($resource)); if ($resource === 'tenants') { return $resourceId > 0 ? [$resourceId] : []; } if ($resource === 'users') { return array_values(array_unique(array_map( 'intval', $this->userTenantRepository->listTenantIdsByUserId($resourceId) ))); } if ($resource === 'departments') { $department = $this->departmentRepository->find($resourceId); $tenantId = (int) ($department['tenant_id'] ?? 0); return $tenantId > 0 ? [$tenantId] : []; } return []; } private function filterActiveTenantIds(array $tenantIds): array { $tenantIds = toIntIds($tenantIds); if (!$tenantIds) { return []; } return $this->tenantRepository->listActiveIdsByIds($tenantIds); } private function canBypass(int $userId): bool { if ($userId <= 0) { return false; } return $this->permissionService->userHas($userId, PermissionService::TENANT_SCOPE_GLOBAL); } }