TenantAtlas/app/Services/Intune/ConfigurationPolicyTemplateResolver.php

389 lines
13 KiB
PHP

<?php
namespace App\Services\Intune;
use App\Models\Tenant;
use App\Services\Graph\GraphClientInterface;
use Illuminate\Support\Arr;
class ConfigurationPolicyTemplateResolver
{
/**
* @var array<string, array<string, array{success:bool,template:?array,reason:?string}>>
*/
private array $templateCache = [];
/**
* @var array<string, array<string, array{success:bool,templates:array<int,array>,reason:?string}>>
*/
private array $familyCache = [];
/**
* @var array<string, array<string, array{success:bool,definition_ids:array<int,string>,reason:?string}>>
*/
private array $templateDefinitionCache = [];
public function __construct(
private readonly GraphClientInterface $graphClient,
) {}
/**
* @param array<string, mixed> $templateReference
* @param array<string, mixed> $graphOptions
* @return array{success:bool,template_id:?string,template_reference:?array,reason:?string,warnings:array<int,string>}
*/
public function resolveTemplateReference(Tenant $tenant, array $templateReference, array $graphOptions = []): array
{
$warnings = [];
$templateId = $this->extractString($templateReference, ['templateId', 'TemplateId']);
$templateFamily = $this->extractString($templateReference, ['templateFamily', 'TemplateFamily']);
$templateDisplayName = $this->extractString($templateReference, ['templateDisplayName', 'TemplateDisplayName']);
$templateDisplayVersion = $this->extractString($templateReference, ['templateDisplayVersion', 'TemplateDisplayVersion']);
if ($templateId !== null) {
$templateOutcome = $this->getTemplate($tenant, $templateId, $graphOptions);
if ($templateOutcome['success']) {
return [
'success' => true,
'template_id' => $templateId,
'template_reference' => $templateReference,
'reason' => null,
'warnings' => $warnings,
];
}
if ($templateFamily === null) {
return [
'success' => false,
'template_id' => null,
'template_reference' => null,
'reason' => $templateOutcome['reason'] ?? "Template '{$templateId}' is not available in the tenant.",
'warnings' => $warnings,
];
}
}
if ($templateFamily === null) {
return [
'success' => false,
'template_id' => null,
'template_reference' => null,
'reason' => 'Template reference is missing templateFamily and cannot be resolved.',
'warnings' => $warnings,
];
}
$listOutcome = $this->listTemplatesByFamily($tenant, $templateFamily, $graphOptions);
if (! $listOutcome['success']) {
return [
'success' => false,
'template_id' => null,
'template_reference' => null,
'reason' => $listOutcome['reason'] ?? "Unable to list templates for family '{$templateFamily}'.",
'warnings' => $warnings,
];
}
$candidates = $this->chooseTemplateCandidate(
templates: $listOutcome['templates'],
templateDisplayName: $templateDisplayName,
templateDisplayVersion: $templateDisplayVersion,
);
if (count($candidates) !== 1) {
$reason = count($candidates) === 0
? "No templates found for family '{$templateFamily}'."
: "Multiple templates found for family '{$templateFamily}' (cannot resolve automatically).";
return [
'success' => false,
'template_id' => null,
'template_reference' => null,
'reason' => $reason,
'warnings' => $warnings,
];
}
$candidate = $candidates[0];
$resolvedId = is_array($candidate) ? ($candidate['id'] ?? null) : null;
if (! is_string($resolvedId) || $resolvedId === '') {
return [
'success' => false,
'template_id' => null,
'template_reference' => null,
'reason' => "Template candidate for family '{$templateFamily}' is missing an id.",
'warnings' => $warnings,
];
}
if ($templateId !== null && $templateId !== $resolvedId) {
$warnings[] = sprintf("TemplateId '%s' not found; mapped to '%s' via templateFamily.", $templateId, $resolvedId);
}
$templateReference['templateId'] = $resolvedId;
if (! isset($templateReference['templateDisplayName']) && isset($candidate['displayName'])) {
$templateReference['templateDisplayName'] = $candidate['displayName'];
}
if (! isset($templateReference['templateDisplayVersion']) && isset($candidate['displayVersion'])) {
$templateReference['templateDisplayVersion'] = $candidate['displayVersion'];
}
return [
'success' => true,
'template_id' => $resolvedId,
'template_reference' => $templateReference,
'reason' => null,
'warnings' => $warnings,
];
}
/**
* @param array<string, mixed> $graphOptions
* @return array{success:bool,template:?array,reason:?string}
*/
public function getTemplate(Tenant $tenant, string $templateId, array $graphOptions = []): array
{
$tenantKey = $this->tenantKey($tenant, $graphOptions);
if (isset($this->templateCache[$tenantKey][$templateId])) {
return $this->templateCache[$tenantKey][$templateId];
}
$context = array_merge($tenant->graphOptions(), Arr::except($graphOptions, ['platform']));
$path = sprintf('/deviceManagement/configurationPolicyTemplates/%s', urlencode($templateId));
$response = $this->graphClient->request('GET', $path, $context);
if ($response->failed()) {
return $this->templateCache[$tenantKey][$templateId] = [
'success' => false,
'template' => null,
'reason' => $response->meta['error_message'] ?? 'Template lookup failed.',
];
}
return $this->templateCache[$tenantKey][$templateId] = [
'success' => true,
'template' => $response->data,
'reason' => null,
];
}
/**
* @param array<string, mixed> $graphOptions
* @return array{success:bool,templates:array<int,array>,reason:?string}
*/
public function listTemplatesByFamily(Tenant $tenant, string $templateFamily, array $graphOptions = []): array
{
$tenantKey = $this->tenantKey($tenant, $graphOptions);
$cacheKey = strtolower($templateFamily);
if (isset($this->familyCache[$tenantKey][$cacheKey])) {
return $this->familyCache[$tenantKey][$cacheKey];
}
$escapedFamily = str_replace("'", "''", $templateFamily);
$context = array_merge($tenant->graphOptions(), Arr::except($graphOptions, ['platform']), [
'query' => [
'$filter' => "templateFamily eq '{$escapedFamily}'",
'$top' => 999,
],
]);
$response = $this->graphClient->request('GET', '/deviceManagement/configurationPolicyTemplates', $context);
if ($response->failed()) {
return $this->familyCache[$tenantKey][$cacheKey] = [
'success' => false,
'templates' => [],
'reason' => $response->meta['error_message'] ?? 'Template list failed.',
];
}
$value = $response->data['value'] ?? [];
$templates = is_array($value) ? array_values(array_filter($value, static fn ($item) => is_array($item))) : [];
return $this->familyCache[$tenantKey][$cacheKey] = [
'success' => true,
'templates' => $templates,
'reason' => null,
];
}
/**
* @param array<string, mixed> $graphOptions
* @return array{success:bool,definition_ids:array<int,string>,reason:?string}
*/
public function fetchTemplateSettingDefinitionIds(Tenant $tenant, string $templateId, array $graphOptions = []): array
{
$tenantKey = $this->tenantKey($tenant, $graphOptions);
if (isset($this->templateDefinitionCache[$tenantKey][$templateId])) {
return $this->templateDefinitionCache[$tenantKey][$templateId];
}
$context = array_merge($tenant->graphOptions(), Arr::except($graphOptions, ['platform']), [
'query' => [
'$expand' => 'settingDefinitions',
'$top' => 999,
],
]);
$path = sprintf('/deviceManagement/configurationPolicyTemplates/%s/settingTemplates', urlencode($templateId));
$response = $this->graphClient->request('GET', $path, $context);
if ($response->failed()) {
return $this->templateDefinitionCache[$tenantKey][$templateId] = [
'success' => false,
'definition_ids' => [],
'reason' => $response->meta['error_message'] ?? 'Template definitions lookup failed.',
];
}
$value = $response->data['value'] ?? [];
$templates = is_array($value) ? $value : [];
$definitionIds = [];
foreach ($templates as $settingTemplate) {
if (! is_array($settingTemplate)) {
continue;
}
$definitions = $settingTemplate['settingDefinitions'] ?? null;
if (! is_array($definitions)) {
continue;
}
foreach ($definitions as $definition) {
if (! is_array($definition)) {
continue;
}
$id = $definition['id'] ?? null;
if (is_string($id) && $id !== '') {
$definitionIds[] = $id;
}
}
}
$definitionIds = array_values(array_unique($definitionIds));
return $this->templateDefinitionCache[$tenantKey][$templateId] = [
'success' => true,
'definition_ids' => $definitionIds,
'reason' => null,
];
}
/**
* @param array<int, mixed> $settings
* @return array<int, string>
*/
public function extractSettingDefinitionIds(array $settings): array
{
$ids = [];
$walk = function (mixed $node) use (&$walk, &$ids): void {
if (! is_array($node)) {
return;
}
foreach ($node as $key => $value) {
if (is_string($key) && strtolower($key) === 'settingdefinitionid' && is_string($value) && $value !== '') {
$ids[] = $value;
}
$walk($value);
}
};
$walk($settings);
return array_values(array_unique($ids));
}
/**
* @param array<int, array> $templates
* @return array<int, array>
*/
private function chooseTemplateCandidate(array $templates, ?string $templateDisplayName, ?string $templateDisplayVersion): array
{
$candidates = $templates;
$active = array_values(array_filter($candidates, static function (array $template): bool {
$state = $template['lifecycleState'] ?? null;
return is_string($state) && strtolower($state) === 'active';
}));
if ($active !== []) {
$candidates = $active;
}
if ($templateDisplayVersion !== null) {
$byVersion = array_values(array_filter($candidates, static function (array $template) use ($templateDisplayVersion): bool {
$version = $template['displayVersion'] ?? null;
return is_string($version) && $version === $templateDisplayVersion;
}));
if ($byVersion !== []) {
$candidates = $byVersion;
}
}
if ($templateDisplayName !== null) {
$byName = array_values(array_filter($candidates, static function (array $template) use ($templateDisplayName): bool {
$name = $template['displayName'] ?? null;
return is_string($name) && $name === $templateDisplayName;
}));
if ($byName !== []) {
$candidates = $byName;
}
}
return $candidates;
}
/**
* @param array<string, mixed> $payload
* @param array<int, string> $keys
*/
private function extractString(array $payload, array $keys): ?string
{
$normalized = array_map('strtolower', $keys);
foreach ($payload as $key => $value) {
if (! is_string($key) || ! in_array(strtolower($key), $normalized, true)) {
continue;
}
if (is_string($value) && trim($value) !== '') {
return $value;
}
}
return null;
}
/**
* @param array<string, mixed> $graphOptions
*/
private function tenantKey(Tenant $tenant, array $graphOptions): string
{
$tenantId = $graphOptions['tenant'] ?? $tenant->graphTenantId() ?? (string) $tenant->getKey();
return (string) $tenantId;
}
}