TenantAtlas/apps/platform/app/Services/TenantConfiguration/SupportedScopeResolver.php
ahmido dfda397eb6 feat: migrate tcm first coverage core cutover (#481)
Automated PR provided by Codex via Gitea API.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #481
2026-06-25 12:54:56 +00:00

256 lines
10 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\TenantConfiguration;
use App\Models\TenantConfigurationResourceType;
use App\Models\TenantConfigurationSupportedScope;
use App\Support\TenantConfiguration\CoverageLevel;
use App\Support\TenantConfiguration\SourceClass;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
use UnexpectedValueException;
final class SupportedScopeResolver
{
/**
* @return list<array<string, mixed>>
*/
public static function defaultDefinitions(): array
{
$tcmCoreTypes = [
'deviceAndAppManagementAssignmentFilter',
'deviceEnrollmentLimitRestriction',
'deviceEnrollmentPlatformRestriction',
'deviceEnrollmentStatusPageWindows10',
'appProtectionPolicyAndroid',
'appProtectionPolicyiOS',
];
return [
[
'scope_key' => 'intune_tcm_core',
'display_name' => 'Intune TCM core',
'description' => 'Initial TCM-backed Intune configuration denominator.',
'minimum_coverage_level' => CoverageLevel::ContentBacked->value,
'included_resource_types' => $tcmCoreTypes,
'allow_beta' => false,
'allow_graph_fallback' => false,
'customer_claims_allowed' => true,
'is_active' => true,
'metadata' => ['kernel' => 'coverage_v2', 'claim_surface' => 'future_activation'],
],
[
'scope_key' => 'intune_tcm_core_with_graph_fallback',
'display_name' => 'Intune TCM core with Graph fallback',
'description' => 'Initial TCM-backed denominator with explicitly allowed Graph v1 fallback resource types.',
'minimum_coverage_level' => CoverageLevel::Detected->value,
'included_resource_types' => [
...$tcmCoreTypes,
'notificationMessageTemplate',
],
'allow_beta' => false,
'allow_graph_fallback' => true,
'customer_claims_allowed' => true,
'is_active' => true,
'metadata' => ['kernel' => 'coverage_v2', 'claim_surface' => 'future_activation'],
],
];
}
public function syncDefaults(): void
{
DB::table('tenant_configuration_supported_scopes')->upsert(
$this->rowsForUpsert(self::defaultDefinitions()),
['scope_key'],
[
'display_name',
'description',
'minimum_coverage_level',
'included_resource_types',
'allow_beta',
'allow_graph_fallback',
'customer_claims_allowed',
'is_active',
'metadata',
'updated_at',
],
);
}
/**
* @return Collection<int, TenantConfigurationSupportedScope>
*/
public function activeScopes(): Collection
{
return TenantConfigurationSupportedScope::query()
->active()
->orderBy('scope_key')
->get();
}
public function findActive(string $scopeKey): ?TenantConfigurationSupportedScope
{
return TenantConfigurationSupportedScope::query()
->active()
->where('scope_key', $scopeKey)
->first();
}
/**
* @param array<string, mixed>|TenantConfigurationSupportedScope $scope
* @param iterable<int, array<string, mixed>|TenantConfigurationResourceType>|null $resourceTypes
* @return array{scope_key: string, minimum_coverage_level: CoverageLevel, included_resource_types: list<string>, excluded_resource_types: list<string>, allow_beta: bool, allow_graph_fallback: bool, customer_claims_allowed: bool}
*/
public function resolveDefinition(array|TenantConfigurationSupportedScope $scope, ?iterable $resourceTypes = null): array
{
$scopeDefinition = $this->normalizeScope($scope);
$resourceTypes ??= ResourceTypeRegistry::defaultDefinitions();
$explicitDenominator = array_values(array_map('strval', $scopeDefinition['included_resource_types']));
$resourceTypesByCanonicalType = $this->indexResourceTypesByCanonicalType($resourceTypes);
$unknownResourceTypes = array_values(array_diff($explicitDenominator, array_keys($resourceTypesByCanonicalType)));
$included = [];
$excluded = [];
if ($unknownResourceTypes !== []) {
throw new UnexpectedValueException(sprintf(
'Supported scope [%s] references unknown resource type(s): %s.',
$scopeDefinition['scope_key'],
implode(', ', $unknownResourceTypes),
));
}
foreach ($explicitDenominator as $canonicalType) {
$definition = $resourceTypesByCanonicalType[$canonicalType];
$sourceClass = SourceClass::from($definition['source_class']);
if ($sourceClass->isBetaExperimental() && ! $scopeDefinition['allow_beta']) {
$excluded[] = $definition['canonical_type'];
continue;
}
if ($sourceClass->isGraphFallback() && ! $scopeDefinition['allow_graph_fallback']) {
$excluded[] = $definition['canonical_type'];
continue;
}
$included[] = $definition['canonical_type'];
}
return [
'scope_key' => $scopeDefinition['scope_key'],
'minimum_coverage_level' => CoverageLevel::from($scopeDefinition['minimum_coverage_level']),
'included_resource_types' => array_values(array_unique($included)),
'excluded_resource_types' => array_values(array_unique($excluded)),
'allow_beta' => $scopeDefinition['allow_beta'],
'allow_graph_fallback' => $scopeDefinition['allow_graph_fallback'],
'customer_claims_allowed' => $scopeDefinition['customer_claims_allowed'],
];
}
/**
* @param list<string> $observedCanonicalTypes
* @param array<string, mixed>|TenantConfigurationSupportedScope $scope
* @param iterable<int, array<string, mixed>|TenantConfigurationResourceType>|null $resourceTypes
*/
public function isScopeComplete(array $observedCanonicalTypes, array|TenantConfigurationSupportedScope $scope, ?iterable $resourceTypes = null): bool
{
$resolved = $this->resolveDefinition($scope, $resourceTypes);
return array_diff($resolved['included_resource_types'], $observedCanonicalTypes) === [];
}
public function meetsMinimum(CoverageLevel|string $actualLevel, array|TenantConfigurationSupportedScope $scope): bool
{
$actual = $actualLevel instanceof CoverageLevel ? $actualLevel : CoverageLevel::from($actualLevel);
$scopeDefinition = $this->normalizeScope($scope);
return $actual->meets(CoverageLevel::from($scopeDefinition['minimum_coverage_level']));
}
/**
* @param list<array<string, mixed>> $definitions
* @return list<array<string, mixed>>
*/
private function rowsForUpsert(array $definitions): array
{
$now = now();
return array_map(static function (array $definition) use ($now): array {
$definition['included_resource_types'] = json_encode($definition['included_resource_types'], JSON_THROW_ON_ERROR);
$definition['metadata'] = json_encode($definition['metadata'] ?? null, JSON_THROW_ON_ERROR);
$definition['created_at'] = $now;
$definition['updated_at'] = $now;
return $definition;
}, $definitions);
}
/**
* @return array{scope_key: string, minimum_coverage_level: string, included_resource_types: list<string>, allow_beta: bool, allow_graph_fallback: bool, customer_claims_allowed: bool}
*/
private function normalizeScope(array|TenantConfigurationSupportedScope $scope): array
{
if ($scope instanceof TenantConfigurationSupportedScope) {
return [
'scope_key' => (string) $scope->scope_key,
'minimum_coverage_level' => $scope->minimum_coverage_level instanceof CoverageLevel
? $scope->minimum_coverage_level->value
: (string) $scope->minimum_coverage_level,
'included_resource_types' => array_values($scope->included_resource_types ?? []),
'allow_beta' => (bool) $scope->allow_beta,
'allow_graph_fallback' => (bool) $scope->allow_graph_fallback,
'customer_claims_allowed' => (bool) $scope->customer_claims_allowed,
];
}
return [
'scope_key' => (string) $scope['scope_key'],
'minimum_coverage_level' => (string) $scope['minimum_coverage_level'],
'included_resource_types' => array_values($scope['included_resource_types'] ?? []),
'allow_beta' => (bool) ($scope['allow_beta'] ?? false),
'allow_graph_fallback' => (bool) ($scope['allow_graph_fallback'] ?? false),
'customer_claims_allowed' => (bool) ($scope['customer_claims_allowed'] ?? false),
];
}
/**
* @param iterable<int, array<string, mixed>|TenantConfigurationResourceType> $resourceTypes
* @return array<string, array{canonical_type: string, source_class: string}>
*/
private function indexResourceTypesByCanonicalType(iterable $resourceTypes): array
{
$indexed = [];
foreach ($resourceTypes as $resourceType) {
$definition = $this->normalizeResourceType($resourceType);
$indexed[$definition['canonical_type']] = $definition;
}
return $indexed;
}
/**
* @return array{canonical_type: string, source_class: string}
*/
private function normalizeResourceType(array|TenantConfigurationResourceType $resourceType): array
{
if ($resourceType instanceof TenantConfigurationResourceType) {
return [
'canonical_type' => (string) $resourceType->canonical_type,
'source_class' => $resourceType->source_class instanceof SourceClass
? $resourceType->source_class->value
: (string) $resourceType->source_class,
];
}
return [
'canonical_type' => (string) $resourceType['canonical_type'],
'source_class' => (string) $resourceType['source_class'],
];
}
}