tenant_id ?? $tenant->external_id; $context = [ 'tenant' => $tenantIdentifier, 'policy_type' => $policy->policy_type, 'policy_id' => $policy->external_id, ]; $this->graphLogger->logRequest('get_policy', $context); try { $response = $this->graphClient->getPolicy($policy->policy_type, $policy->external_id, [ 'tenant' => $tenantIdentifier, 'client_id' => $tenant->app_client_id, 'client_secret' => $tenant->app_client_secret, 'platform' => $policy->platform, ]); } catch (Throwable $throwable) { $mapped = GraphErrorMapper::fromThrowable($throwable, $context); return [ 'failure' => [ 'policy_id' => $policy->id, 'reason' => $mapped->getMessage(), 'status' => $mapped->status, ], ]; } $this->graphLogger->logResponse('get_policy', $response, $context); $payload = $response->data['payload'] ?? $response->data; $metadata = Arr::except($response->data, ['payload']); $metadataWarnings = $metadata['warnings'] ?? []; if ($policy->policy_type === 'settingsCatalogPolicy') { [$payload, $metadata] = $this->hydrateSettingsCatalog( tenantIdentifier: $tenantIdentifier, tenant: $tenant, policyId: $policy->external_id, payload: is_array($payload) ? $payload : [], metadata: $metadata ); } if ($response->failed()) { $reason = $response->warnings[0] ?? 'Graph request failed'; $failure = [ 'policy_id' => $policy->id, 'reason' => $reason, 'status' => $response->status, ]; if (! config('graph.stub_on_failure')) { return ['failure' => $failure]; } $payload = [ 'id' => $policy->external_id, 'type' => $policy->policy_type, 'source' => 'stub', 'warning' => $reason, ]; $metadataWarnings = $response->warnings ?? [$reason]; } $validation = $this->snapshotValidator->validate(is_array($payload) ? $payload : []); $metadataWarnings = array_merge($metadataWarnings, $validation['warnings']); $odataWarning = Policy::odataTypeWarning(is_array($payload) ? $payload : [], $policy->policy_type, $policy->platform); if ($odataWarning) { $metadataWarnings[] = $odataWarning; } if (! empty($metadataWarnings)) { $metadata['warnings'] = array_values(array_unique($metadataWarnings)); } return [ 'payload' => is_array($payload) ? $payload : [], 'metadata' => $metadata, 'warnings' => $metadataWarnings, ]; } /** * Hydrate settings catalog policies with configuration settings subresource. * * @return array{0:array,1:array} */ private function hydrateSettingsCatalog(string $tenantIdentifier, Tenant $tenant, string $policyId, array $payload, array $metadata): array { $strategy = $this->contracts->memberHydrationStrategy('settingsCatalogPolicy'); $settingsPath = $this->contracts->subresourceSettingsPath('settingsCatalogPolicy', $policyId); if ($strategy !== 'subresource_settings' || ! $settingsPath) { return [$payload, $metadata]; } $settings = []; $nextPath = $settingsPath; $hydrationStatus = 'complete'; while ($nextPath) { $response = $this->graphClient->request('GET', $nextPath, [ 'tenant' => $tenantIdentifier, 'client_id' => $tenant->app_client_id, 'client_secret' => $tenant->app_client_secret, ]); if ($response->failed()) { $hydrationStatus = 'failed'; break; } $data = $response->data; $pageItems = $data['value'] ?? (is_array($data) ? $data : []); $settings = array_merge($settings, $pageItems); $nextLink = $data['@odata.nextLink'] ?? null; if (! $nextLink) { break; } $nextPath = $this->stripGraphBaseUrl((string) $nextLink); } if (! empty($settings)) { $payload['settings'] = $settings; // Extract definition IDs and warm cache (T008-T010) $definitionIds = $this->extractDefinitionIds($settings); $metadata['definition_count'] = count($definitionIds); // Warm cache for definitions (non-blocking) $this->definitionResolver->warmCache($definitionIds); $metadata['definitions_cached'] = true; } $metadata['settings_hydration'] = $hydrationStatus; return [$payload, $metadata]; } /** * Extract all settingDefinitionId from settings array, including nested children. */ private function extractDefinitionIds(array $settings): array { $definitionIds = []; foreach ($settings as $setting) { // Extract definition ID from settingInstance if (isset($setting['settingInstance']['settingDefinitionId'])) { $definitionIds[] = $setting['settingInstance']['settingDefinitionId']; } // Handle groupSettingCollectionInstance with children if (isset($setting['settingInstance']['@odata.type']) && str_contains($setting['settingInstance']['@odata.type'], 'groupSettingCollectionInstance')) { if (isset($setting['settingInstance']['groupSettingCollectionValue'])) { foreach ($setting['settingInstance']['groupSettingCollectionValue'] as $group) { if (isset($group['children'])) { $childIds = $this->extractDefinitionIds($group['children']); $definitionIds = array_merge($definitionIds, $childIds); } } } } } return array_unique($definitionIds); } private function stripGraphBaseUrl(string $nextLink): string { $base = rtrim(config('graph.base_url', 'https://graph.microsoft.com'), '/').'/'.trim(config('graph.version', 'beta'), '/'); if (str_starts_with($nextLink, $base)) { return ltrim(substr($nextLink, strlen($base)), '/'); } return ltrim($nextLink, '/'); } }