256 lines
10 KiB
PHP
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'],
|
|
];
|
|
}
|
|
}
|