## 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
178 lines
6.0 KiB
PHP
178 lines
6.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\AuditLog;
|
|
use App\Models\BaselineProfile;
|
|
use App\Models\Workspace;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
function spec202ForceLegacyBaselineScope(BaselineProfile $profile, array $scope): void
|
|
{
|
|
DB::table('baseline_profiles')
|
|
->where('id', (int) $profile->getKey())
|
|
->update([
|
|
'scope_jsonb' => json_encode($scope, JSON_THROW_ON_ERROR),
|
|
'updated_at' => now(),
|
|
]);
|
|
}
|
|
|
|
it('previews only legacy baseline profile scope rows in mixed datasets without mutating them', 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]],
|
|
]);
|
|
|
|
$workspace = Workspace::factory()->create();
|
|
|
|
$legacyProfile = BaselineProfile::factory()->active()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'name' => 'Legacy preview profile',
|
|
]);
|
|
$canonicalProfile = BaselineProfile::factory()->active()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'name' => 'Canonical preview profile',
|
|
'scope_jsonb' => [
|
|
'version' => 2,
|
|
'entries' => [
|
|
[
|
|
'domain_key' => 'intune',
|
|
'subject_class' => 'policy',
|
|
'subject_type_keys' => ['deviceCompliancePolicy'],
|
|
'filters' => [],
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
|
|
spec202ForceLegacyBaselineScope($legacyProfile, [
|
|
'policy_types' => ['deviceConfiguration'],
|
|
'foundation_types' => [],
|
|
]);
|
|
|
|
$this->artisan('tenantpilot:baseline-scope-v2:backfill', [
|
|
'--workspace' => (string) $workspace->getKey(),
|
|
])
|
|
->expectsOutputToContain('Mode: preview')
|
|
->expectsOutputToContain('Scope surface: baseline_profiles_only')
|
|
->expectsOutputToContain('Candidate count: 1')
|
|
->expectsOutputToContain('Rewritten count: 0')
|
|
->expectsOutputToContain('Audit logged: no')
|
|
->expectsOutputToContain('Legacy preview profile')
|
|
->assertSuccessful();
|
|
|
|
$legacyProfile->refresh();
|
|
$canonicalProfile->refresh();
|
|
|
|
expect($legacyProfile->rawScopeJsonb())->toBe([
|
|
'policy_types' => ['deviceConfiguration'],
|
|
'foundation_types' => [],
|
|
])->and($canonicalProfile->rawScopeJsonb())->toBe([
|
|
'version' => 2,
|
|
'entries' => [
|
|
[
|
|
'domain_key' => 'intune',
|
|
'subject_class' => 'policy',
|
|
'subject_type_keys' => ['deviceCompliancePolicy'],
|
|
'filters' => [],
|
|
],
|
|
],
|
|
]);
|
|
});
|
|
|
|
it('requires explicit write confirmation before mutating baseline profile scope rows', function (): void {
|
|
$workspace = Workspace::factory()->create();
|
|
|
|
$profile = BaselineProfile::factory()->active()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'name' => 'Legacy confirm profile',
|
|
]);
|
|
|
|
spec202ForceLegacyBaselineScope($profile, [
|
|
'policy_types' => ['deviceConfiguration'],
|
|
'foundation_types' => [],
|
|
]);
|
|
|
|
$this->artisan('tenantpilot:baseline-scope-v2:backfill', [
|
|
'--workspace' => (string) $workspace->getKey(),
|
|
'--write' => true,
|
|
])
|
|
->expectsOutputToContain('Explicit write confirmation required.')
|
|
->assertFailed();
|
|
|
|
$profile->refresh();
|
|
|
|
expect($profile->rawScopeJsonb())->toBe([
|
|
'policy_types' => ['deviceConfiguration'],
|
|
'foundation_types' => [],
|
|
]);
|
|
});
|
|
|
|
it('rewrites legacy baseline profile scopes to canonical v2, logs audits, and stays idempotent on rerun', function (): void {
|
|
config()->set('tenantpilot.supported_policy_types', [
|
|
['type' => 'deviceConfiguration', 'label' => 'Device Configuration'],
|
|
['type' => 'deviceCompliancePolicy', 'label' => 'Device Compliance'],
|
|
]);
|
|
|
|
$workspace = Workspace::factory()->create();
|
|
|
|
$profile = BaselineProfile::factory()->active()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'name' => 'Legacy commit profile',
|
|
]);
|
|
|
|
spec202ForceLegacyBaselineScope($profile, [
|
|
'policy_types' => ['deviceConfiguration'],
|
|
'foundation_types' => [],
|
|
]);
|
|
|
|
$this->artisan('tenantpilot:baseline-scope-v2:backfill', [
|
|
'--workspace' => (string) $workspace->getKey(),
|
|
'--write' => true,
|
|
'--confirm-write' => true,
|
|
])
|
|
->expectsOutputToContain('Mode: commit')
|
|
->expectsOutputToContain('Candidate count: 1')
|
|
->expectsOutputToContain('Rewritten count: 1')
|
|
->expectsOutputToContain('Audit logged: yes')
|
|
->assertSuccessful();
|
|
|
|
$profile->refresh();
|
|
|
|
expect($profile->rawScopeJsonb())->toBe([
|
|
'version' => 2,
|
|
'entries' => [
|
|
[
|
|
'domain_key' => 'intune',
|
|
'subject_class' => 'policy',
|
|
'subject_type_keys' => ['deviceConfiguration'],
|
|
'filters' => [],
|
|
],
|
|
],
|
|
])->and($profile->requiresScopeSaveForward())->toBeFalse();
|
|
|
|
$this->assertDatabaseHas('audit_logs', [
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'action' => 'baseline_profile.scope_backfilled',
|
|
'resource_type' => 'baseline_profile',
|
|
'resource_id' => (string) $profile->getKey(),
|
|
]);
|
|
|
|
$auditLog = AuditLog::query()
|
|
->where('workspace_id', (int) $workspace->getKey())
|
|
->where('action', 'baseline_profile.scope_backfilled')
|
|
->latest('id')
|
|
->first();
|
|
|
|
expect($auditLog)->not->toBeNull();
|
|
|
|
$this->artisan('tenantpilot:baseline-scope-v2:backfill', [
|
|
'--workspace' => (string) $workspace->getKey(),
|
|
])
|
|
->expectsOutputToContain('Candidate count: 0')
|
|
->expectsOutputToContain('No baseline profile scope rows require backfill.')
|
|
->assertSuccessful();
|
|
}); |