## 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
230 lines
8.2 KiB
PHP
230 lines
8.2 KiB
PHP
<?php
|
|
|
|
use App\Support\Baselines\BaselineScope;
|
|
|
|
it('expands empty policy_types to supported policy types (excluding foundations) and defaults foundation_types to none', function () {
|
|
config()->set('tenantpilot.supported_policy_types', [
|
|
['type' => 'deviceConfiguration', 'label' => 'Device Configuration'],
|
|
['type' => 'deviceCompliancePolicy', 'label' => 'Device Compliance'],
|
|
]);
|
|
|
|
config()->set('tenantpilot.foundation_types', [
|
|
['type' => 'assignmentFilter', 'label' => 'Assignment Filter', 'baseline_compare' => ['supported' => true]],
|
|
]);
|
|
|
|
$scope = BaselineScope::fromJsonb([
|
|
'policy_types' => [],
|
|
'foundation_types' => [],
|
|
])->expandDefaults();
|
|
|
|
expect($scope->policyTypes)->toBe([
|
|
'deviceCompliancePolicy',
|
|
'deviceConfiguration',
|
|
]);
|
|
|
|
expect($scope->foundationTypes)->toBe([]);
|
|
expect($scope->allTypes())->toBe([
|
|
'deviceCompliancePolicy',
|
|
'deviceConfiguration',
|
|
]);
|
|
});
|
|
|
|
it('filters unknown types and does not allow foundations inside policy_types', function () {
|
|
config()->set('tenantpilot.supported_policy_types', [
|
|
['type' => 'deviceConfiguration'],
|
|
]);
|
|
|
|
config()->set('tenantpilot.foundation_types', [
|
|
['type' => 'assignmentFilter', 'baseline_compare' => ['supported' => true]],
|
|
]);
|
|
|
|
$scope = BaselineScope::fromJsonb([
|
|
'policy_types' => ['deviceConfiguration', 'assignmentFilter', 'unknown'],
|
|
'foundation_types' => ['assignmentFilter', 'unknown'],
|
|
])->expandDefaults();
|
|
|
|
expect($scope->policyTypes)->toBe(['deviceConfiguration']);
|
|
expect($scope->foundationTypes)->toBe(['assignmentFilter']);
|
|
expect($scope->allTypes())->toBe(['assignmentFilter', 'deviceConfiguration']);
|
|
});
|
|
|
|
it('normalizes canonical v2 entries and preserves canonical storage', function (): void {
|
|
config()->set('tenantpilot.supported_policy_types', [
|
|
['type' => 'deviceConfiguration', 'label' => 'Device Configuration'],
|
|
['type' => 'deviceCompliancePolicy', 'label' => 'Device Compliance'],
|
|
]);
|
|
|
|
config()->set('tenantpilot.foundation_types', [
|
|
['type' => 'assignmentFilter', 'label' => 'Assignment Filter', 'baseline_compare' => ['supported' => true]],
|
|
]);
|
|
|
|
$scope = BaselineScope::fromJsonb([
|
|
'version' => 2,
|
|
'entries' => [
|
|
[
|
|
'domain_key' => 'intune',
|
|
'subject_class' => 'policy',
|
|
'subject_type_keys' => ['deviceConfiguration', 'deviceCompliancePolicy', 'deviceConfiguration'],
|
|
'filters' => [],
|
|
],
|
|
[
|
|
'domain_key' => 'intune',
|
|
'subject_class' => 'policy',
|
|
'subject_type_keys' => ['deviceCompliancePolicy'],
|
|
'filters' => [],
|
|
],
|
|
[
|
|
'domain_key' => 'platform_foundation',
|
|
'subject_class' => 'configuration_resource',
|
|
'subject_type_keys' => ['assignmentFilter'],
|
|
'filters' => [],
|
|
],
|
|
],
|
|
]);
|
|
|
|
expect($scope->policyTypes)->toBe(['deviceCompliancePolicy', 'deviceConfiguration'])
|
|
->and($scope->foundationTypes)->toBe(['assignmentFilter'])
|
|
->and($scope->toStoredJsonb())->toBe([
|
|
'version' => 2,
|
|
'entries' => [
|
|
[
|
|
'domain_key' => 'intune',
|
|
'subject_class' => 'policy',
|
|
'subject_type_keys' => ['deviceCompliancePolicy', 'deviceConfiguration'],
|
|
'filters' => [],
|
|
],
|
|
[
|
|
'domain_key' => 'platform_foundation',
|
|
'subject_class' => 'configuration_resource',
|
|
'subject_type_keys' => ['assignmentFilter'],
|
|
'filters' => [],
|
|
],
|
|
],
|
|
])
|
|
->and($scope->normalizationLineage())->toMatchArray([
|
|
'source_shape' => 'canonical_v2',
|
|
'normalized_on_read' => false,
|
|
'save_forward_required' => false,
|
|
]);
|
|
});
|
|
|
|
it('treats a missing legacy bucket like its empty default when the other bucket is present', function (): void {
|
|
config()->set('tenantpilot.supported_policy_types', [
|
|
['type' => 'deviceConfiguration'],
|
|
['type' => 'deviceCompliancePolicy'],
|
|
]);
|
|
|
|
config()->set('tenantpilot.foundation_types', [
|
|
['type' => 'assignmentFilter', 'baseline_compare' => ['supported' => true]],
|
|
]);
|
|
|
|
$policyOnly = BaselineScope::fromJsonb([
|
|
'policy_types' => ['deviceConfiguration'],
|
|
]);
|
|
$foundationOnly = BaselineScope::fromJsonb([
|
|
'foundation_types' => ['assignmentFilter'],
|
|
]);
|
|
|
|
expect($policyOnly->policyTypes)->toBe(['deviceConfiguration'])
|
|
->and($policyOnly->foundationTypes)->toBe([])
|
|
->and($foundationOnly->policyTypes)->toBe(['deviceCompliancePolicy', 'deviceConfiguration'])
|
|
->and($foundationOnly->foundationTypes)->toBe(['assignmentFilter']);
|
|
});
|
|
|
|
it('rejects mixed legacy and canonical payloads', function (): void {
|
|
expect(fn () => BaselineScope::fromJsonb([
|
|
'policy_types' => ['deviceConfiguration'],
|
|
'version' => 2,
|
|
'entries' => [
|
|
[
|
|
'domain_key' => 'intune',
|
|
'subject_class' => 'policy',
|
|
'subject_type_keys' => ['deviceConfiguration'],
|
|
],
|
|
],
|
|
]))->toThrow(InvalidArgumentException::class, 'must not mix legacy buckets');
|
|
});
|
|
|
|
it('rejects unsupported filters for current domains', function (): void {
|
|
config()->set('tenantpilot.supported_policy_types', [
|
|
['type' => 'deviceConfiguration'],
|
|
]);
|
|
|
|
expect(fn () => BaselineScope::fromJsonb([
|
|
'version' => 2,
|
|
'entries' => [
|
|
[
|
|
'domain_key' => 'intune',
|
|
'subject_class' => 'policy',
|
|
'subject_type_keys' => ['deviceConfiguration'],
|
|
'filters' => ['tenant_ids' => ['tenant-a']],
|
|
],
|
|
],
|
|
]))->toThrow(InvalidArgumentException::class, 'Filters are not supported');
|
|
});
|
|
|
|
it('treats empty legacy override payloads as no override when requested', function (): void {
|
|
$scope = BaselineScope::fromJsonb([
|
|
'policy_types' => [],
|
|
'foundation_types' => [],
|
|
], allowEmptyLegacyAsNoOverride: true);
|
|
|
|
expect($scope->isEmpty())->toBeTrue();
|
|
});
|
|
|
|
it('rejects unknown governance domains', function (): void {
|
|
expect(fn () => BaselineScope::fromJsonb([
|
|
'version' => 2,
|
|
'entries' => [
|
|
[
|
|
'domain_key' => 'unknown_domain',
|
|
'subject_class' => 'policy',
|
|
'subject_type_keys' => ['deviceConfiguration'],
|
|
'filters' => [],
|
|
],
|
|
],
|
|
]))->toThrow(InvalidArgumentException::class, 'Unknown governance domain');
|
|
});
|
|
|
|
it('rejects invalid subject classes for known domains', function (): void {
|
|
expect(fn () => BaselineScope::fromJsonb([
|
|
'version' => 2,
|
|
'entries' => [
|
|
[
|
|
'domain_key' => 'intune',
|
|
'subject_class' => 'configuration_resource',
|
|
'subject_type_keys' => ['deviceConfiguration'],
|
|
'filters' => [],
|
|
],
|
|
],
|
|
]))->toThrow(InvalidArgumentException::class, 'is not valid for domain');
|
|
});
|
|
|
|
it('rejects inactive subject types in canonical scope entries', function (): void {
|
|
expect(fn () => BaselineScope::fromJsonb([
|
|
'version' => 2,
|
|
'entries' => [
|
|
[
|
|
'domain_key' => 'platform_foundation',
|
|
'subject_class' => 'configuration_resource',
|
|
'subject_type_keys' => ['intuneRoleAssignment'],
|
|
'filters' => [],
|
|
],
|
|
],
|
|
]))->toThrow(InvalidArgumentException::class, 'Inactive subject type');
|
|
});
|
|
|
|
it('rejects future-domain selections that have no active subject type mapping yet', function (): void {
|
|
expect(fn () => BaselineScope::fromJsonb([
|
|
'version' => 2,
|
|
'entries' => [
|
|
[
|
|
'domain_key' => 'entra',
|
|
'subject_class' => 'control',
|
|
'subject_type_keys' => ['conditionalAccessPolicy'],
|
|
'filters' => [],
|
|
],
|
|
],
|
|
]))->toThrow(InvalidArgumentException::class, 'Unknown subject type');
|
|
});
|