> */ 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 */ 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|TenantConfigurationSupportedScope $scope * @param iterable|TenantConfigurationResourceType>|null $resourceTypes * @return array{scope_key: string, minimum_coverage_level: CoverageLevel, included_resource_types: list, excluded_resource_types: list, 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 $observedCanonicalTypes * @param array|TenantConfigurationSupportedScope $scope * @param iterable|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> $definitions * @return list> */ 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, 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|TenantConfigurationResourceType> $resourceTypes * @return array */ 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'], ]; } }