## Summary - introduce the governance subject taxonomy registry and canonical Baseline Scope V2 normalization and persistence - update baseline profile Filament surfaces, validation, capture/compare gating, and add the optional scope backfill command with audit logging - add focused unit, feature, Filament, and browser smoke coverage for save-forward behavior, operation truth, authorization continuity, and invalid-scope rendering - remove the duplicate legacy spec plan under `specs/001-governance-subject-taxonomy/plan.md` ## Verification - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec202GovernanceSubjectTaxonomySmokeTest.php` - focused Spec 202 regression pack: `56 passed (300 assertions)` - `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` ## Notes - no schema migration required - no new Filament asset registration required - branch includes the final browser smoke test coverage for the current feature Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #232
684 lines
24 KiB
PHP
684 lines
24 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\Baselines;
|
|
|
|
use App\Support\Governance\GovernanceDomainKey;
|
|
use App\Support\Governance\GovernanceSubjectClass;
|
|
use App\Support\Governance\GovernanceSubjectTaxonomyRegistry;
|
|
use App\Support\Inventory\InventoryPolicyTypeMeta;
|
|
use InvalidArgumentException;
|
|
|
|
/**
|
|
* Value object for baseline scope resolution.
|
|
*
|
|
* Canonical storage uses versioned Governance Scope V2 entries.
|
|
* Presentation compatibility for the current Intune-first UI still projects
|
|
* back to legacy policy and foundation buckets.
|
|
*/
|
|
final class BaselineScope
|
|
{
|
|
/**
|
|
* @param list<string> $policyTypes
|
|
* @param list<string> $foundationTypes
|
|
* @param list<array{domain_key: string, subject_class: string, subject_type_keys: list<string>, filters: array<string, mixed>}> $entries
|
|
* @param list<string> $legacyKeysPresent
|
|
*/
|
|
public function __construct(
|
|
public readonly array $policyTypes = [],
|
|
public readonly array $foundationTypes = [],
|
|
public readonly array $entries = [],
|
|
public readonly int $version = 2,
|
|
public readonly string $sourceShape = 'canonical_v2',
|
|
public readonly bool $normalizedOnRead = false,
|
|
public readonly array $legacyKeysPresent = [],
|
|
public readonly bool $saveForwardRequired = false,
|
|
) {}
|
|
|
|
/**
|
|
* Create from the scope_jsonb column value.
|
|
*
|
|
* @param array<string, mixed>|null $scopeJsonb
|
|
*/
|
|
public static function fromJsonb(?array $scopeJsonb, bool $allowEmptyLegacyAsNoOverride = false): self
|
|
{
|
|
if ($scopeJsonb === null) {
|
|
return self::fromLegacyPayload(
|
|
policyTypes: [],
|
|
foundationTypes: [],
|
|
legacyKeysPresent: [],
|
|
normalizedOnRead: true,
|
|
saveForwardRequired: true,
|
|
);
|
|
}
|
|
|
|
if (isset($scopeJsonb['canonical_scope']) && is_array($scopeJsonb['canonical_scope'])) {
|
|
return self::fromJsonb($scopeJsonb['canonical_scope'], $allowEmptyLegacyAsNoOverride);
|
|
}
|
|
|
|
$hasLegacyKeys = array_key_exists('policy_types', $scopeJsonb) || array_key_exists('foundation_types', $scopeJsonb);
|
|
$hasCanonicalKeys = array_key_exists('version', $scopeJsonb) || array_key_exists('entries', $scopeJsonb);
|
|
|
|
if ($hasLegacyKeys && $hasCanonicalKeys) {
|
|
throw new InvalidArgumentException('Baseline scope payload must not mix legacy buckets with canonical V2 keys.');
|
|
}
|
|
|
|
if ($hasCanonicalKeys) {
|
|
return self::fromCanonicalPayload($scopeJsonb);
|
|
}
|
|
|
|
if (! $hasLegacyKeys) {
|
|
throw new InvalidArgumentException('Baseline scope payload must contain either legacy buckets or canonical V2 keys.');
|
|
}
|
|
|
|
$legacyKeysPresent = array_values(array_filter([
|
|
array_key_exists('policy_types', $scopeJsonb) ? 'policy_types' : null,
|
|
array_key_exists('foundation_types', $scopeJsonb) ? 'foundation_types' : null,
|
|
]));
|
|
|
|
$policyTypes = self::stringList($scopeJsonb['policy_types'] ?? []);
|
|
$foundationTypes = self::stringList($scopeJsonb['foundation_types'] ?? []);
|
|
|
|
if ($allowEmptyLegacyAsNoOverride && $policyTypes === [] && $foundationTypes === []) {
|
|
return new self(
|
|
policyTypes: [],
|
|
foundationTypes: [],
|
|
entries: [],
|
|
sourceShape: 'legacy',
|
|
normalizedOnRead: false,
|
|
legacyKeysPresent: $legacyKeysPresent,
|
|
saveForwardRequired: false,
|
|
);
|
|
}
|
|
|
|
return self::fromLegacyPayload(
|
|
policyTypes: $policyTypes,
|
|
foundationTypes: $foundationTypes,
|
|
legacyKeysPresent: $legacyKeysPresent,
|
|
normalizedOnRead: true,
|
|
saveForwardRequired: true,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Normalize the effective scope by intersecting profile scope with an optional override.
|
|
*
|
|
* Override can only narrow the profile scope (subset enforcement).
|
|
* Empty override means "no override".
|
|
*/
|
|
public static function effective(self $profileScope, ?self $overrideScope): self
|
|
{
|
|
$profileScope = $profileScope->expandDefaults();
|
|
|
|
if ($overrideScope === null || $overrideScope->isEmpty()) {
|
|
return $profileScope;
|
|
}
|
|
|
|
$overridePolicyTypes = self::normalizePolicyTypes($overrideScope->policyTypes);
|
|
$overrideFoundationTypes = self::normalizeFoundationTypes($overrideScope->foundationTypes);
|
|
$entries = [];
|
|
|
|
foreach ($profileScope->entries as $entry) {
|
|
$subjectTypeKeys = $entry['subject_type_keys'];
|
|
|
|
if ($entry['domain_key'] === GovernanceDomainKey::Intune->value
|
|
&& $entry['subject_class'] === GovernanceSubjectClass::Policy->value
|
|
&& $overridePolicyTypes !== []) {
|
|
$subjectTypeKeys = array_values(array_intersect($subjectTypeKeys, $overridePolicyTypes));
|
|
}
|
|
|
|
if ($entry['domain_key'] === GovernanceDomainKey::PlatformFoundation->value
|
|
&& $entry['subject_class'] === GovernanceSubjectClass::ConfigurationResource->value
|
|
&& $overrideFoundationTypes !== []) {
|
|
$subjectTypeKeys = array_values(array_intersect($subjectTypeKeys, $overrideFoundationTypes));
|
|
}
|
|
|
|
$subjectTypeKeys = self::uniqueSorted($subjectTypeKeys);
|
|
|
|
if ($subjectTypeKeys === []) {
|
|
continue;
|
|
}
|
|
|
|
$entries[] = [
|
|
'domain_key' => $entry['domain_key'],
|
|
'subject_class' => $entry['subject_class'],
|
|
'subject_type_keys' => $subjectTypeKeys,
|
|
'filters' => $entry['filters'],
|
|
];
|
|
}
|
|
|
|
return self::fromCanonicalPayload([
|
|
'version' => 2,
|
|
'entries' => $entries,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* An empty scope means "no override" (for override_scope semantics).
|
|
*/
|
|
public function isEmpty(): bool
|
|
{
|
|
return $this->entries === [] && $this->policyTypes === [] && $this->foundationTypes === [];
|
|
}
|
|
|
|
/**
|
|
* Apply Spec 116 defaults and filter to supported types.
|
|
*/
|
|
public function expandDefaults(): self
|
|
{
|
|
if ($this->entries !== []) {
|
|
return $this;
|
|
}
|
|
|
|
return self::fromLegacyPayload(
|
|
policyTypes: $this->policyTypes,
|
|
foundationTypes: $this->foundationTypes,
|
|
legacyKeysPresent: $this->legacyKeysPresent,
|
|
normalizedOnRead: $this->normalizedOnRead,
|
|
saveForwardRequired: $this->saveForwardRequired,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
public function allTypes(): array
|
|
{
|
|
$expanded = $this->expandDefaults();
|
|
|
|
return self::uniqueSorted(array_merge(
|
|
$expanded->policyTypes,
|
|
$expanded->foundationTypes,
|
|
));
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
public function truthfulTypes(string $operation, ?BaselineSupportCapabilityGuard $guard = null): array
|
|
{
|
|
$guard ??= app(BaselineSupportCapabilityGuard::class);
|
|
$guardResult = $guard->guardTypes($this->allTypes(), $operation);
|
|
|
|
return $guardResult['allowed_types'];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function toJsonb(): array
|
|
{
|
|
$supportedPolicyTypes = self::supportedPolicyTypes();
|
|
$policyTypes = $this->policyTypes;
|
|
|
|
if ($policyTypes === $supportedPolicyTypes) {
|
|
$policyTypes = [];
|
|
}
|
|
|
|
return [
|
|
'policy_types' => $policyTypes,
|
|
'foundation_types' => $this->foundationTypes,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{version: 2, entries: list<array{domain_key: string, subject_class: string, subject_type_keys: list<string>, filters: array<string, mixed>}>}
|
|
*/
|
|
public function toStoredJsonb(): array
|
|
{
|
|
return [
|
|
'version' => 2,
|
|
'entries' => $this->entries,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{source_shape: string, normalized_on_read: bool, legacy_keys_present: list<string>, save_forward_required: bool}
|
|
*/
|
|
public function normalizationLineage(): array
|
|
{
|
|
return [
|
|
'source_shape' => $this->sourceShape,
|
|
'normalized_on_read' => $this->normalizedOnRead,
|
|
'legacy_keys_present' => $this->legacyKeysPresent,
|
|
'save_forward_required' => $this->saveForwardRequired,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return list<array{domain_key: string, subject_class: string, group_label: string, selected_subject_types: list<string>, capture_supported_count: int, compare_supported_count: int, inactive_count: int}>
|
|
*/
|
|
public function summaryGroups(?GovernanceSubjectTaxonomyRegistry $registry = null): array
|
|
{
|
|
$registry ??= app(GovernanceSubjectTaxonomyRegistry::class);
|
|
$groups = [];
|
|
|
|
foreach ($this->entries as $entry) {
|
|
$selectedSubjectTypes = [];
|
|
$captureSupportedCount = 0;
|
|
$compareSupportedCount = 0;
|
|
$inactiveCount = 0;
|
|
|
|
foreach ($entry['subject_type_keys'] as $subjectTypeKey) {
|
|
$subjectType = $registry->find($entry['domain_key'], $subjectTypeKey);
|
|
$selectedSubjectTypes[] = $subjectType?->label ?? $subjectTypeKey;
|
|
|
|
if ($subjectType?->captureSupported) {
|
|
$captureSupportedCount++;
|
|
}
|
|
|
|
if ($subjectType?->compareSupported) {
|
|
$compareSupportedCount++;
|
|
}
|
|
|
|
if ($subjectType !== null && ! $subjectType->active) {
|
|
$inactiveCount++;
|
|
}
|
|
}
|
|
|
|
sort($selectedSubjectTypes, SORT_STRING);
|
|
|
|
$groups[] = [
|
|
'domain_key' => $entry['domain_key'],
|
|
'subject_class' => $entry['subject_class'],
|
|
'group_label' => $registry->groupLabel($entry['domain_key'], $entry['subject_class']),
|
|
'selected_subject_types' => $selectedSubjectTypes,
|
|
'capture_supported_count' => $captureSupportedCount,
|
|
'compare_supported_count' => $compareSupportedCount,
|
|
'inactive_count' => $inactiveCount,
|
|
];
|
|
}
|
|
|
|
return $groups;
|
|
}
|
|
|
|
/**
|
|
* Effective scope payload for OperationRun.context.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function toEffectiveScopeContext(?BaselineSupportCapabilityGuard $guard = null, ?string $operation = null): array
|
|
{
|
|
$expanded = $this->expandDefaults();
|
|
$allTypes = self::uniqueSorted(array_merge($expanded->policyTypes, $expanded->foundationTypes));
|
|
|
|
$context = [
|
|
'canonical_scope' => $expanded->toStoredJsonb(),
|
|
'legacy_projection' => $expanded->toJsonb(),
|
|
'policy_types' => $expanded->policyTypes,
|
|
'foundation_types' => $expanded->foundationTypes,
|
|
'all_types' => $allTypes,
|
|
'selected_type_keys' => $allTypes,
|
|
'foundations_included' => $expanded->foundationTypes !== [],
|
|
];
|
|
|
|
if (! is_string($operation) || $operation === '') {
|
|
return $context;
|
|
}
|
|
|
|
$guard ??= app(BaselineSupportCapabilityGuard::class);
|
|
$guardResult = $guard->guardTypes($allTypes, $operation);
|
|
|
|
return array_merge($context, [
|
|
'truthful_types' => $guardResult['allowed_types'],
|
|
'limited_types' => $guardResult['limited_types'],
|
|
'unsupported_types' => $guardResult['unsupported_types'],
|
|
'invalid_support_types' => $guardResult['invalid_support_types'],
|
|
'capabilities' => $guardResult['capabilities'],
|
|
'allowed_type_keys' => $guardResult['allowed_types'],
|
|
'limited_type_keys' => $guardResult['limited_types'],
|
|
'unsupported_type_keys' => $guardResult['unsupported_types'],
|
|
'capabilities_by_type' => $guardResult['capabilities'],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return array{ok: bool, unsupported_types: list<string>, invalid_support_types: list<string>}
|
|
*/
|
|
public function operationEligibility(string $operation, ?BaselineSupportCapabilityGuard $guard = null): array
|
|
{
|
|
$guard ??= app(BaselineSupportCapabilityGuard::class);
|
|
$guardResult = $guard->guardTypes($this->allTypes(), $operation);
|
|
|
|
return [
|
|
'ok' => $guardResult['unsupported_types'] === [] && $guardResult['invalid_support_types'] === [],
|
|
'unsupported_types' => $guardResult['unsupported_types'],
|
|
'invalid_support_types' => $guardResult['invalid_support_types'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
private static function supportedPolicyTypes(): array
|
|
{
|
|
return app(GovernanceSubjectTaxonomyRegistry::class)->activeLegacyBucketKeys('policy_types');
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
private static function supportedFoundationTypes(): array
|
|
{
|
|
return app(GovernanceSubjectTaxonomyRegistry::class)->activeLegacyBucketKeys('foundation_types');
|
|
}
|
|
|
|
/**
|
|
* @param array<string> $types
|
|
* @return list<string>
|
|
*/
|
|
private static function normalizePolicyTypes(array $types): array
|
|
{
|
|
$supported = self::supportedPolicyTypes();
|
|
|
|
return self::uniqueSorted(array_values(array_intersect($types, $supported)));
|
|
}
|
|
|
|
/**
|
|
* @param array<string> $types
|
|
* @return list<string>
|
|
*/
|
|
private static function normalizeFoundationTypes(array $types): array
|
|
{
|
|
$supported = self::supportedFoundationTypes();
|
|
|
|
return self::uniqueSorted(array_values(array_intersect($types, $supported)));
|
|
}
|
|
|
|
/**
|
|
* @param array<int, string> $types
|
|
* @return list<string>
|
|
*/
|
|
private static function uniqueSorted(array $types): array
|
|
{
|
|
$types = array_values(array_unique(array_filter($types, fn (mixed $type): bool => is_string($type) && $type !== '')));
|
|
|
|
sort($types, SORT_STRING);
|
|
|
|
return $types;
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $policyTypes
|
|
* @param list<string> $foundationTypes
|
|
* @param list<string> $legacyKeysPresent
|
|
*/
|
|
private static function fromLegacyPayload(
|
|
array $policyTypes,
|
|
array $foundationTypes,
|
|
array $legacyKeysPresent,
|
|
bool $normalizedOnRead,
|
|
bool $saveForwardRequired,
|
|
): self {
|
|
$policyTypes = $policyTypes === []
|
|
? self::supportedPolicyTypes()
|
|
: self::normalizePolicyTypes($policyTypes);
|
|
|
|
$foundationTypes = self::normalizeFoundationTypes($foundationTypes);
|
|
$entries = [];
|
|
|
|
if ($policyTypes !== []) {
|
|
$entries[] = [
|
|
'domain_key' => GovernanceDomainKey::Intune->value,
|
|
'subject_class' => GovernanceSubjectClass::Policy->value,
|
|
'subject_type_keys' => $policyTypes,
|
|
'filters' => [],
|
|
];
|
|
}
|
|
|
|
if ($foundationTypes !== []) {
|
|
$entries[] = [
|
|
'domain_key' => GovernanceDomainKey::PlatformFoundation->value,
|
|
'subject_class' => GovernanceSubjectClass::ConfigurationResource->value,
|
|
'subject_type_keys' => $foundationTypes,
|
|
'filters' => [],
|
|
];
|
|
}
|
|
|
|
return new self(
|
|
policyTypes: $policyTypes,
|
|
foundationTypes: $foundationTypes,
|
|
entries: $entries,
|
|
sourceShape: 'legacy',
|
|
normalizedOnRead: $normalizedOnRead,
|
|
legacyKeysPresent: $legacyKeysPresent,
|
|
saveForwardRequired: $saveForwardRequired,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $scopeJsonb
|
|
*/
|
|
private static function fromCanonicalPayload(array $scopeJsonb): self
|
|
{
|
|
if (($scopeJsonb['version'] ?? null) !== 2) {
|
|
throw new InvalidArgumentException('Baseline scope version must equal 2.');
|
|
}
|
|
|
|
$entries = $scopeJsonb['entries'] ?? null;
|
|
|
|
if (! is_array($entries) || $entries === []) {
|
|
throw new InvalidArgumentException('Baseline scope V2 entries must be a non-empty array.');
|
|
}
|
|
|
|
$normalizedEntries = self::normalizeEntries($entries);
|
|
[$policyTypes, $foundationTypes] = self::legacyProjectionFromEntries($normalizedEntries);
|
|
|
|
return new self(
|
|
policyTypes: $policyTypes,
|
|
foundationTypes: $foundationTypes,
|
|
entries: $normalizedEntries,
|
|
sourceShape: 'canonical_v2',
|
|
normalizedOnRead: false,
|
|
legacyKeysPresent: [],
|
|
saveForwardRequired: false,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param list<mixed> $entries
|
|
* @return list<array{domain_key: string, subject_class: string, subject_type_keys: list<string>, filters: array<string, mixed>}>
|
|
*/
|
|
private static function normalizeEntries(array $entries): array
|
|
{
|
|
$registry = app(GovernanceSubjectTaxonomyRegistry::class);
|
|
$normalizedEntries = [];
|
|
$subjectFilters = [];
|
|
|
|
foreach ($entries as $entry) {
|
|
if (! is_array($entry)) {
|
|
throw new InvalidArgumentException('Each canonical baseline scope entry must be an array.');
|
|
}
|
|
|
|
$domainKey = trim((string) ($entry['domain_key'] ?? ''));
|
|
$subjectClass = trim((string) ($entry['subject_class'] ?? ''));
|
|
|
|
if (! $registry->isKnownDomain($domainKey)) {
|
|
throw new InvalidArgumentException('Unknown governance domain ['.$domainKey.'].');
|
|
}
|
|
|
|
if (! $registry->allowsSubjectClass($domainKey, $subjectClass)) {
|
|
throw new InvalidArgumentException('Subject class ['.$subjectClass.'] is not valid for domain ['.$domainKey.'].');
|
|
}
|
|
|
|
$subjectTypeKeys = self::stringList($entry['subject_type_keys'] ?? null);
|
|
|
|
if ($subjectTypeKeys === []) {
|
|
throw new InvalidArgumentException('Canonical baseline scope entries must include at least one subject type key.');
|
|
}
|
|
|
|
$filters = $entry['filters'] ?? [];
|
|
|
|
if (! is_array($filters)) {
|
|
throw new InvalidArgumentException('Baseline scope entry filters must be an object-shaped array.');
|
|
}
|
|
|
|
$filters = self::normalizeFilters($filters);
|
|
|
|
if ($filters !== [] && ! $registry->supportsFilters($domainKey, $subjectClass)) {
|
|
throw new InvalidArgumentException('Filters are not supported for the current governance domain and subject class.');
|
|
}
|
|
|
|
$subjectTypeKeys = array_map(
|
|
static fn (string $subjectTypeKey): string => trim($subjectTypeKey),
|
|
self::uniqueSorted($subjectTypeKeys),
|
|
);
|
|
|
|
foreach ($subjectTypeKeys as $subjectTypeKey) {
|
|
$subjectType = $registry->find($domainKey, $subjectTypeKey);
|
|
|
|
if ($subjectType === null) {
|
|
throw new InvalidArgumentException('Unknown subject type ['.$subjectTypeKey.'] for domain ['.$domainKey.'].');
|
|
}
|
|
|
|
if ($subjectType->subjectClass->value !== $subjectClass) {
|
|
throw new InvalidArgumentException('Subject type ['.$subjectTypeKey.'] does not belong to subject class ['.$subjectClass.'].');
|
|
}
|
|
|
|
if (! $subjectType->active) {
|
|
throw new InvalidArgumentException('Inactive subject type ['.$subjectTypeKey.'] cannot be selected.');
|
|
}
|
|
}
|
|
|
|
$filtersHash = self::filtersHash($filters);
|
|
|
|
foreach ($subjectTypeKeys as $subjectTypeKey) {
|
|
$subjectKey = implode('|', [$domainKey, $subjectClass, $subjectTypeKey]);
|
|
$existingFiltersHash = $subjectFilters[$subjectKey] ?? null;
|
|
|
|
if ($existingFiltersHash !== null && $existingFiltersHash !== $filtersHash) {
|
|
throw new InvalidArgumentException('Ambiguous baseline scope filters were provided for ['.$subjectTypeKey.'].');
|
|
}
|
|
|
|
$subjectFilters[$subjectKey] = $filtersHash;
|
|
}
|
|
|
|
$entryKey = implode('|', [$domainKey, $subjectClass, $filtersHash]);
|
|
|
|
if (! array_key_exists($entryKey, $normalizedEntries)) {
|
|
$normalizedEntries[$entryKey] = [
|
|
'domain_key' => $domainKey,
|
|
'subject_class' => $subjectClass,
|
|
'subject_type_keys' => [],
|
|
'filters' => $filters,
|
|
];
|
|
}
|
|
|
|
$normalizedEntries[$entryKey]['subject_type_keys'] = self::uniqueSorted(array_merge(
|
|
$normalizedEntries[$entryKey]['subject_type_keys'],
|
|
$subjectTypeKeys,
|
|
));
|
|
}
|
|
|
|
$normalizedEntries = array_values($normalizedEntries);
|
|
|
|
usort($normalizedEntries, static function (array $left, array $right): int {
|
|
$leftKey = implode('|', [
|
|
$left['domain_key'],
|
|
$left['subject_class'],
|
|
self::filtersHash($left['filters']),
|
|
implode(',', $left['subject_type_keys']),
|
|
]);
|
|
$rightKey = implode('|', [
|
|
$right['domain_key'],
|
|
$right['subject_class'],
|
|
self::filtersHash($right['filters']),
|
|
implode(',', $right['subject_type_keys']),
|
|
]);
|
|
|
|
return $leftKey <=> $rightKey;
|
|
});
|
|
|
|
return $normalizedEntries;
|
|
}
|
|
|
|
/**
|
|
* @param list<array{domain_key: string, subject_class: string, subject_type_keys: list<string>, filters: array<string, mixed>}> $entries
|
|
* @return array{0: list<string>, 1: list<string>}
|
|
*/
|
|
private static function legacyProjectionFromEntries(array $entries): array
|
|
{
|
|
$policyTypes = [];
|
|
$foundationTypes = [];
|
|
|
|
foreach ($entries as $entry) {
|
|
if ($entry['domain_key'] === GovernanceDomainKey::Intune->value
|
|
&& $entry['subject_class'] === GovernanceSubjectClass::Policy->value) {
|
|
$policyTypes = array_merge($policyTypes, $entry['subject_type_keys']);
|
|
}
|
|
|
|
if ($entry['domain_key'] === GovernanceDomainKey::PlatformFoundation->value
|
|
&& $entry['subject_class'] === GovernanceSubjectClass::ConfigurationResource->value) {
|
|
$foundationTypes = array_merge($foundationTypes, $entry['subject_type_keys']);
|
|
}
|
|
}
|
|
|
|
return [
|
|
self::uniqueSorted($policyTypes),
|
|
self::uniqueSorted($foundationTypes),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param mixed $values
|
|
* @return list<string>
|
|
*/
|
|
private static function stringList(mixed $values): array
|
|
{
|
|
if (! is_array($values)) {
|
|
return [];
|
|
}
|
|
|
|
return array_values(array_filter($values, 'is_string'));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $filters
|
|
* @return array<string, mixed>
|
|
*/
|
|
private static function normalizeFilters(array $filters): array
|
|
{
|
|
ksort($filters);
|
|
|
|
foreach ($filters as $key => $value) {
|
|
if (is_array($value)) {
|
|
$filters[$key] = self::normalizeFilterArray($value);
|
|
}
|
|
}
|
|
|
|
return $filters;
|
|
}
|
|
|
|
/**
|
|
* @param array<int|string, mixed> $values
|
|
* @return array<int|string, mixed>
|
|
*/
|
|
private static function normalizeFilterArray(array $values): array
|
|
{
|
|
foreach ($values as $key => $value) {
|
|
if (is_array($value)) {
|
|
$values[$key] = self::normalizeFilterArray($value);
|
|
}
|
|
}
|
|
|
|
if (array_is_list($values)) {
|
|
sort($values);
|
|
|
|
return array_values($values);
|
|
}
|
|
|
|
ksort($values);
|
|
|
|
return $values;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $filters
|
|
*/
|
|
private static function filtersHash(array $filters): string
|
|
{
|
|
return json_encode($filters, JSON_THROW_ON_ERROR);
|
|
}
|
|
}
|