contracts->get($policyType); $listPathTemplate = $contract['assignments_list_path'] ?? null; $resource = $contract['resource'] ?? null; $requestOptions = array_merge($options, ['tenant' => $tenantId]); $context = [ 'tenant_id' => $tenantId, 'policy_type' => $policyType, 'policy_id' => $policyId, ]; $primaryException = null; $assignments = []; // Try primary endpoint try { $assignments = $this->fetchPrimary( $listPathTemplate, $policyId, $requestOptions, $context, $throwOnFailure ); } catch (GraphException $e) { $primaryException = $e; } if (! empty($assignments)) { Log::debug('Fetched assignments via primary endpoint', [ 'tenant_id' => $tenantId, 'policy_type' => $policyType, 'policy_id' => $policyId, 'count' => count($assignments), ]); return $assignments; } // Try fallback with $expand Log::debug('Primary endpoint returned empty, trying fallback', [ 'tenant_id' => $tenantId, 'policy_type' => $policyType, 'policy_id' => $policyId, ]); if (! is_string($resource) || $resource === '') { Log::debug('Assignments resource not configured for policy type', [ 'tenant_id' => $tenantId, 'policy_type' => $policyType, 'policy_id' => $policyId, ]); if ($throwOnFailure && $primaryException) { Log::warning('Failed to fetch assignments', [ 'tenant_id' => $tenantId, 'policy_type' => $policyType, 'policy_id' => $policyId, 'error' => $primaryException->getMessage(), 'context' => $primaryException->context, ]); throw $primaryException; } return []; } $fallbackException = null; try { $assignments = $this->fetchWithExpand( $resource, $policyId, $requestOptions, $context, $throwOnFailure ); } catch (GraphException $e) { $fallbackException = $e; } if (! empty($assignments)) { Log::debug('Fetched assignments via fallback endpoint', [ 'tenant_id' => $tenantId, 'policy_type' => $policyType, 'policy_id' => $policyId, 'count' => count($assignments), ]); return $assignments; } // Both methods returned empty Log::debug('No assignments found for policy', [ 'tenant_id' => $tenantId, 'policy_type' => $policyType, 'policy_id' => $policyId, ]); if ($throwOnFailure && ($fallbackException || $primaryException)) { $exception = $fallbackException ?? $primaryException; Log::warning('Failed to fetch assignments', [ 'tenant_id' => $tenantId, 'policy_type' => $policyType, 'policy_id' => $policyId, 'error' => $exception->getMessage(), 'context' => $exception->context, ]); throw $exception; } return []; } /** * Fetch assignments using primary endpoint. */ private function fetchPrimary( ?string $listPathTemplate, string $policyId, array $options, array $context, bool $throwOnFailure ): array { if (! is_string($listPathTemplate) || $listPathTemplate === '') { return []; } $path = $this->resolvePath($listPathTemplate, $policyId); if ($path === null) { return []; } $response = $this->graphClient->request('GET', $path, $options); if ($response->failed()) { $this->logAssignmentFailure('primary', $response, $context + ['path' => $path]); if ($throwOnFailure) { throw new GraphException( $this->resolveErrorMessage($response), $response->status, $context + ['path' => $path] ); } return []; } return $response->data['value'] ?? []; } /** * Fetch assignments using $expand fallback. */ private function fetchWithExpand( string $resource, string $policyId, array $options, array $context, bool $throwOnFailure ): array { $path = $resource; $params = [ '$expand' => 'assignments', '$filter' => "id eq '{$policyId}'", ]; $response = $this->graphClient->request('GET', $path, array_merge($options, [ 'query' => $params, ])); if ($response->failed()) { $this->logAssignmentFailure('fallback', $response, $context + ['path' => $path]); if ($throwOnFailure) { throw new GraphException( $this->resolveErrorMessage($response), $response->status, $context + ['path' => $path] ); } return []; } $policies = $response->data['value'] ?? []; if (empty($policies)) { return []; } return $policies[0]['assignments'] ?? []; } private function resolvePath(string $template, string $policyId): ?string { if ($template === '') { return null; } return str_replace('{id}', urlencode($policyId), $template); } private function resolveErrorMessage(GraphResponse $response): string { $error = $response->errors[0] ?? null; if (is_array($error)) { if (isset($error['message']) && is_string($error['message'])) { return $error['message']; } return json_encode($error, JSON_UNESCAPED_SLASHES) ?: 'Graph request failed'; } if (is_string($error) && $error !== '') { return $error; } return 'Graph request failed'; } private function logAssignmentFailure(string $stage, GraphResponse $response, array $context): void { Log::warning('Assignment fetch failed', $context + [ 'stage' => $stage, 'status' => $response->status, 'errors' => $response->errors, ]); } }