Compare commits

..

3 Commits

Author SHA1 Message Date
Ahmed Darrazi
9ae98b0705 fix: tighten workspace RBAC access boundaries
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m37s
2026-05-15 15:59:14 +02:00
Ahmed Darrazi
adb3737298 Merge branch 'platform-dev' of git.cloudarix.de:ahmido/TenantAtlas into platform-dev 2026-05-15 14:55:38 +02:00
77c343fb35 feat: implement decision register summary in environment review packs (#363)
## Summary
- add decision register summary output to environment review packs
- update environment review evidence composition and localized summary rendering
- add coverage for executive pack and derived review pack behavior
- include spec artifacts for feature 308

## Testing
- cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/EnvironmentReview/EnvironmentReviewExecutivePackTest.php tests/Feature/ReviewPack/EnvironmentReviewDerivedReviewPackTest.php

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #363
2026-05-15 12:54:41 +00:00
31 changed files with 2963 additions and 31 deletions

View File

@ -279,6 +279,9 @@ private function executeReviewDerivedGeneration(
$fingerprint = app(ReviewPackService::class)->computeFingerprintForReview($review, $options);
$reviewSummary = is_array($review->summary) ? $review->summary : [];
$governancePackage = is_array($reviewSummary['governance_package'] ?? null)
? $this->redactReportPayload($reviewSummary['governance_package'], $includePii)
: [];
$summary = [
'environment_review_id' => (int) $review->getKey(),
'review_status' => (string) $review->status,
@ -289,6 +292,7 @@ private function executeReviewDerivedGeneration(
'operation_count' => $includeOperations ? (int) ($reviewSummary['operation_count'] ?? 0) : 0,
'highlights' => is_array($reviewSummary['highlights'] ?? null) ? $reviewSummary['highlights'] : [],
'publish_blockers' => is_array($reviewSummary['publish_blockers'] ?? null) ? $reviewSummary['publish_blockers'] : [],
'governance_package' => $governancePackage,
'delivery_bundle' => $this->deliveryBundleSummary($review),
'evidence_resolution' => [
'outcome' => 'resolved',
@ -541,7 +545,24 @@ private function redactReportPayload(array $payload, bool $includePii): array
*/
private function redactArrayPii(array $data): array
{
$piiKeys = ['displayName', 'display_name', 'userPrincipalName', 'user_principal_name', 'email', 'mail'];
$piiKeys = [
'displayName',
'display_name',
'userPrincipalName',
'user_principal_name',
'email',
'mail',
'tenant_name',
'tenant_label',
'customer_name',
'owner_label',
'owner_name',
'actor_label',
'actor_name',
'initiator_name',
'requested_by',
'approved_by',
];
foreach ($data as $key => $value) {
if (is_string($key) && in_array($key, $piiKeys, true)) {
@ -825,7 +846,10 @@ private function buildExecutiveEntrypoint(
$tenantName = $includePii ? $tenant->name : '[REDACTED]';
$topFindings = is_array($package['top_findings'] ?? null) ? $package['top_findings'] : [];
$acceptedRisks = is_array($package['accepted_risks'] ?? null) ? $package['accepted_risks'] : [];
$governanceDecisions = is_array($package['governance_decisions'] ?? null) ? $package['governance_decisions'] : [];
$decisionSummary = is_array($package['decision_summary'] ?? null) ? $package['decision_summary'] : [];
$governanceDecisions = is_array($decisionSummary['entries'] ?? null)
? $decisionSummary['entries']
: (is_array($package['governance_decisions'] ?? null) ? $package['governance_decisions'] : []);
$nextActions = is_array($reviewSummary['recommended_next_actions'] ?? null) ? $reviewSummary['recommended_next_actions'] : [];
$lines = [
@ -857,7 +881,7 @@ private function buildExecutiveEntrypoint(
'',
'## Governance decisions requiring awareness',
'',
...$this->entryBullets($governanceDecisions, 'No governance decisions require awareness in this released review.'),
...$this->decisionSummaryLines($decisionSummary, $governanceDecisions),
'',
'## Next actions',
'',
@ -876,6 +900,35 @@ private function buildExecutiveEntrypoint(
return implode("\n", $lines);
}
/**
* @param array<string, mixed> $decisionSummary
* @param array<int, mixed> $entries
* @return list<string>
*/
private function decisionSummaryLines(array $decisionSummary, array $entries): array
{
$lines = [];
$summary = $this->plainText($decisionSummary['summary'] ?? null, '');
$nextAction = $this->plainText($decisionSummary['next_action'] ?? null, '');
if ($summary !== '') {
$lines[] = $summary;
}
if ($nextAction !== '') {
$lines[] = 'Next action: '.$nextAction;
}
if ($lines !== []) {
$lines[] = '';
}
return [
...$lines,
...$this->entryBullets($entries, 'No governance decisions require awareness in this released review.'),
];
}
/**
* @param array<int, mixed> $entries
* @return list<string>

View File

@ -69,7 +69,7 @@ protected function casts(): array
public function canAccessPanel(Panel $panel): bool
{
return true;
return $panel->getId() === 'admin';
}
public function tenants(): BelongsToMany

View File

@ -47,7 +47,6 @@ class WorkspaceRoleCapabilityMap
WorkspaceRole::Manager->value => [
Capabilities::WORKSPACE_VIEW,
Capabilities::WORKSPACE_MEMBERSHIP_VIEW,
Capabilities::WORKSPACE_MEMBERSHIP_MANAGE,
Capabilities::WORKSPACE_MANAGED_ENVIRONMENT_ONBOARD,
Capabilities::WORKSPACE_MANAGED_ENVIRONMENT_ONBOARD_IDENTIFY,
Capabilities::WORKSPACE_MANAGED_ENVIRONMENT_ONBOARD_CANCEL,
@ -102,10 +101,6 @@ public static function getCapabilities(WorkspaceRole|string $role): array
RoleCapabilityMap::getCapabilities($roleValue),
);
if ($roleValue === WorkspaceRole::Manager->value) {
$capabilities[] = Capabilities::TENANT_MEMBERSHIP_MANAGE;
}
return array_values(array_unique($capabilities));
}

View File

@ -148,6 +148,11 @@ private function governancePackageSummary(
->map(fn (array $entry): array => $this->packageGovernanceDecisionEntry($entry))
->values()
->all();
$decisionSummary = $this->packageDecisionSummary(
snapshot: $snapshot,
acceptedRisksSection: $acceptedRisksSection,
governanceDecisions: $governanceDecisions,
);
return [
'delivery_artifact_family' => 'review_pack',
@ -163,6 +168,7 @@ private function governancePackageSummary(
'top_findings' => $openRiskEntries,
'accepted_risks' => $stableAcceptedRiskEntries->all(),
'governance_decisions' => $governanceDecisions,
'decision_summary' => $decisionSummary,
'evidence_basis_summary' => $this->governancePackageEvidenceBasisSummary(
snapshot: $snapshot,
controlInterpretationSummary: $controlInterpretationSummary,
@ -236,15 +242,118 @@ private function packageGovernanceDecisionEntry(array $entry): array
'finding_id' => $entry['finding_id'] ?? null,
'title' => $entry['title'] ?? 'Governance decision',
'governance_state' => $governanceState,
'awareness_reason' => $this->governanceDecisionAwarenessReason($governanceState),
'summary' => match ($governanceState) {
'expired_exception' => 'The accepted-risk exception has expired and needs follow-up before stakeholder delivery.',
'revoked_exception' => 'The accepted-risk exception was revoked and needs follow-up before stakeholder delivery.',
'risk_accepted_without_valid_exception' => 'The accepted-risk entry has no currently valid exception basis and needs follow-up before stakeholder delivery.',
default => 'This governance decision needs follow-up before stakeholder delivery.',
},
'next_action' => $this->governanceDecisionNextAction($governanceState),
'evidence_basis' => 'Included in the anchored released-review evidence basis.',
];
}
/**
* @param array<string, mixed> $acceptedRisksSection
* @param list<array<string, mixed>> $governanceDecisions
* @return array<string, mixed>
*/
private function packageDecisionSummary(
EvidenceSnapshot $snapshot,
array $acceptedRisksSection,
array $governanceDecisions,
): array {
$totalCount = count($governanceDecisions);
$evidenceState = is_string($acceptedRisksSection['completeness_state'] ?? null)
? (string) $acceptedRisksSection['completeness_state']
: (string) $snapshot->completeness_state;
$decisionDataState = $totalCount > 0 || $this->decisionEvidenceIsAvailable($evidenceState)
? 'available'
: 'incomplete';
$status = match (true) {
$totalCount > 0 => 'requires_awareness',
$decisionDataState === 'incomplete' => 'unavailable',
default => 'none',
};
return [
'customer_safe' => true,
'status' => $status,
'decision_data_state' => $decisionDataState,
'evidence_state' => $evidenceState,
'total_count' => $totalCount,
'requires_awareness' => $totalCount > 0,
'summary' => $this->decisionSummaryText($status, $totalCount),
'empty_state' => $this->decisionSummaryEmptyState($status),
'next_action' => $this->decisionSummaryNextAction($status),
'evidence_basis' => sprintf(
'Anchored to evidence snapshot #%d with %s decision-evidence completeness.',
(int) $snapshot->getKey(),
$evidenceState,
),
'source_section_state' => is_string($acceptedRisksSection['completeness_state'] ?? null)
? $acceptedRisksSection['completeness_state']
: null,
'entries' => $governanceDecisions,
];
}
private function decisionEvidenceIsAvailable(string $evidenceState): bool
{
return in_array($evidenceState, ['complete'], true);
}
private function decisionSummaryText(string $status, int $totalCount): string
{
return match ($status) {
'requires_awareness' => sprintf(
'%d governance decision%s require%s customer awareness before relying on this released review.',
$totalCount,
$totalCount === 1 ? '' : 's',
$totalCount === 1 ? 's' : '',
),
'unavailable' => 'Decision evidence is incomplete for this released review; no customer-aware decisions can be confirmed from the current evidence basis.',
default => 'No governance decisions require customer awareness in this released review.',
};
}
private function decisionSummaryEmptyState(string $status): string
{
return match ($status) {
'unavailable' => 'Decision evidence is incomplete in the current released-review basis.',
'requires_awareness' => '',
default => 'No governance decisions require awareness in this released review.',
};
}
private function decisionSummaryNextAction(string $status): string
{
return match ($status) {
'requires_awareness' => 'Review the accepted-risk decision basis before customer delivery.',
'unavailable' => 'Open the review evidence before treating the decision register summary as complete.',
default => 'No customer action is needed for Decision Register follow-up from this review.',
};
}
private function governanceDecisionAwarenessReason(string $governanceState): string
{
return match ($governanceState) {
'expired_exception' => 'The accepted-risk approval has expired and needs customer awareness before the review is relied on.',
'revoked_exception' => 'The accepted-risk approval was revoked and needs customer awareness before the review is relied on.',
'risk_accepted_without_valid_exception' => 'The accepted risk has no valid governance backing in the released review evidence.',
default => 'This accepted-risk governance decision needs customer awareness before the review is relied on.',
};
}
private function governanceDecisionNextAction(string $governanceState): string
{
return match ($governanceState) {
'expired_exception', 'revoked_exception', 'risk_accepted_without_valid_exception' => 'Confirm whether this accepted risk should be renewed, remediated, or removed before relying on the review.',
default => 'Confirm the accepted-risk decision before relying on the review.',
};
}
/**
* @param array<string, mixed> $executiveSummaryPayload
* @param array<string, mixed> $executiveRenderPayload

View File

@ -50,8 +50,8 @@ public function collect(ManagedEnvironment $tenant): array
'finding_type' => (string) $finding->finding_type,
'severity' => (string) $finding->severity,
'status' => (string) $finding->status,
'title' => $finding->title,
'description' => $finding->description,
'title' => $finding->resolvedSubjectDisplayName(),
'description' => null,
'created_at' => $finding->created_at?->toIso8601String(),
'updated_at' => $finding->updated_at?->toIso8601String(),
'verification_state' => $outcome['verification_state'],

View File

@ -371,6 +371,7 @@
'download_governance_package' => 'Governance-Paket herunterladen',
'governance_package' => 'Governance-Paket',
'governance_decisions' => 'Governance-Entscheidungen',
'governance_decisions_requiring_awareness' => 'Governance-Entscheidungen mit Aufmerksamkeitsbedarf',
'governance_package_delivery_note' => 'Dieses Governance-Paket wird über das aktuelle Export-Review-Pack des veröffentlichten Reviews ausgeliefert.',
'executive_entrypoint' => 'Executive-Einstieg',
'executive_entrypoint_description' => 'Beginnen Sie im heruntergeladenen Paket mit executive-summary.md.',

View File

@ -371,6 +371,7 @@
'download_governance_package' => 'Download governance package',
'governance_package' => 'Governance package',
'governance_decisions' => 'Governance decisions',
'governance_decisions_requiring_awareness' => 'Governance decisions requiring awareness',
'governance_package_delivery_note' => 'This governance package is delivered through the current export review pack for the released review.',
'executive_entrypoint' => 'Executive entrypoint',
'executive_entrypoint_description' => 'Start with executive-summary.md in the downloaded package.',

View File

@ -16,7 +16,13 @@
$packageAvailability = is_array($governancePackage['availability'] ?? null) ? $governancePackage['availability'] : [];
$packageTopFindings = is_array($governancePackage['top_findings'] ?? null) ? $governancePackage['top_findings'] : [];
$packageAcceptedRisks = is_array($governancePackage['accepted_risks'] ?? null) ? $governancePackage['accepted_risks'] : [];
$packageGovernanceDecisions = is_array($governancePackage['governance_decisions'] ?? null) ? $governancePackage['governance_decisions'] : [];
$packageDecisionSummary = is_array($governancePackage['decision_summary'] ?? null) ? $governancePackage['decision_summary'] : [];
$packageGovernanceDecisions = is_array($packageDecisionSummary['entries'] ?? null)
? $packageDecisionSummary['entries']
: (is_array($governancePackage['governance_decisions'] ?? null) ? $governancePackage['governance_decisions'] : []);
$packageDecisionSummaryText = is_string($packageDecisionSummary['summary'] ?? null) ? trim((string) $packageDecisionSummary['summary']) : null;
$packageDecisionNextAction = is_string($packageDecisionSummary['next_action'] ?? null) ? trim((string) $packageDecisionSummary['next_action']) : null;
$packageDecisionEmptyState = is_string($packageDecisionSummary['empty_state'] ?? null) ? trim((string) $packageDecisionSummary['empty_state']) : null;
$controlControls = is_array($controlInterpretation['controls'] ?? null) ? $controlInterpretation['controls'] : [];
$controlVersion = is_string($controlInterpretation['version_key'] ?? null) ? $controlInterpretation['version_key'] : null;
$controlDisclosure = is_string($controlInterpretation['non_certification_disclosure'] ?? null)
@ -205,14 +211,29 @@
</div>
@endif
@if ($packageGovernanceDecisions !== [])
@if ($packageDecisionSummary !== [] || $packageGovernanceDecisions !== [])
<div class="space-y-2">
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ __('localization.review.governance_decisions') }}</div>
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ __('localization.review.governance_decisions_requiring_awareness') }}</div>
@if ($packageDecisionSummaryText !== null && $packageDecisionSummaryText !== '')
<div class="rounded-md border border-gray-100 bg-gray-50 px-3 py-2 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-950/60 dark:text-gray-300">
{{ $packageDecisionSummaryText }}
</div>
@endif
@if ($packageDecisionNextAction !== null && $packageDecisionNextAction !== '')
<div class="rounded-md border border-primary-100 bg-primary-50 px-3 py-2 text-sm text-primary-900 dark:border-primary-900/40 dark:bg-primary-950/30 dark:text-primary-100">
{{ $packageDecisionNextAction }}
</div>
@endif
@if ($packageGovernanceDecisions !== [])
<ul class="space-y-1 text-sm text-amber-800 dark:text-amber-200">
@foreach ($packageGovernanceDecisions as $decision)
@php
$decisionTitle = is_string($decision['title'] ?? null) ? $decision['title'] : __('localization.review.governance_decisions');
$decisionSummary = is_string($decision['summary'] ?? null) ? $decision['summary'] : null;
$decisionNextAction = is_string($decision['next_action'] ?? null) ? $decision['next_action'] : null;
@endphp
<li class="rounded-md border border-amber-100 bg-amber-50 px-3 py-2 dark:border-amber-900/40 dark:bg-amber-950/30">
@ -220,9 +241,17 @@
@if ($decisionSummary !== null && trim($decisionSummary) !== '')
<div class="mt-1 text-xs">{{ $decisionSummary }}</div>
@endif
@if ($decisionNextAction !== null && trim($decisionNextAction) !== '')
<div class="mt-2 text-xs">{{ $decisionNextAction }}</div>
@endif
</li>
@endforeach
</ul>
@elseif ($packageDecisionEmptyState !== null && $packageDecisionEmptyState !== '')
<div class="rounded-md border border-gray-100 bg-gray-50 px-3 py-2 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-950/60 dark:text-gray-300">
{{ $packageDecisionEmptyState }}
</div>
@endif
</div>
@endif
</div>

View File

@ -76,7 +76,7 @@
/** @var \Filament\Panel $panel */
$panel = app(PanelRegistry::class)->get('admin');
$allowedDecision = app(ManagedEnvironmentAccessScopeResolver::class)->decision($user, $allowedTenant, Capabilities::WORKSPACE_MEMBERSHIP_MANAGE);
$allowedDecision = app(ManagedEnvironmentAccessScopeResolver::class)->decision($user, $allowedTenant, Capabilities::WORKSPACE_SETTINGS_VIEW);
$defaultTenant = $user->getDefaultTenant($panel);
$tenants = $user->getTenants($panel);

View File

@ -5,7 +5,13 @@
use App\Filament\Resources\EnvironmentReviewResource;
use App\Filament\Widgets\ManagedEnvironment\ManagedEnvironmentReviewPackCard;
use App\Jobs\GenerateReviewPackJob;
use App\Models\Finding;
use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Services\Findings\FindingExceptionService;
use App\Services\Findings\FindingRiskGovernanceResolver;
use App\Services\ReviewPackService;
use App\Support\EnvironmentReviewStatus;
use Illuminate\Support\Facades\Storage;
use Livewire\Livewire;
@ -13,6 +19,40 @@
Storage::fake('exports');
});
function spec308SeedExpiredDecisionFinding(ManagedEnvironment $tenant, User $requester, string $title): Finding
{
$approver = User::factory()->create(['name' => 'Spec 308 Approver']);
createUserWithTenant(tenant: $tenant, user: $approver, role: 'owner', workspaceRole: 'manager');
/** @var FindingExceptionService $exceptionService */
$exceptionService = app(FindingExceptionService::class);
$finding = Finding::factory()->for($tenant)->riskAccepted()->create([
'fingerprint' => 'spec-308-raw-fingerprint-'.$title,
'evidence_jsonb' => [
'display_name' => $title,
'internal_url' => 'https://tenantpilot.test/admin/operations/raw-run',
],
]);
$requested = $exceptionService->request($finding, $tenant, $requester, [
'owner_user_id' => (int) $requester->getKey(),
'request_reason' => 'Temporary exception for staged remediation.',
'review_due_at' => now()->addDays(5)->toDateTimeString(),
'expires_at' => now()->addDays(14)->toDateTimeString(),
]);
$exceptionService->approve($requested, $approver, [
'effective_from' => now()->subDays(10)->toDateTimeString(),
'expires_at' => now()->subDay()->toDateTimeString(),
'approval_reason' => 'Approved with customer controls.',
]);
app(FindingRiskGovernanceResolver::class)->syncExceptionState($finding->findingException()->firstOrFail());
return $finding->refresh();
}
it('renders an executive-ready environment review and exports a pack with matching section order and summary truth', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$review = composeEnvironmentReviewForTest($tenant, $user);
@ -88,3 +128,93 @@
->test(ManagedEnvironmentReviewPackCard::class, ['record' => $tenant])
->assertSee('View review');
});
it('builds an explicit customer-safe decision summary for released review consumption', function (): void {
[$user, $tenant] = createUserWithTenant(
tenant: ManagedEnvironment::factory()->create(['name' => 'Visible Customer Environment']),
role: 'owner',
);
spec308SeedExpiredDecisionFinding($tenant, $user, 'Privileged role exception');
$hiddenTenant = ManagedEnvironment::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'name' => 'Hidden Customer Environment',
]);
[$hiddenUser, $hiddenTenant] = createUserWithTenant(tenant: $hiddenTenant, role: 'owner');
spec308SeedExpiredDecisionFinding($hiddenTenant, $hiddenUser, 'Hidden tenant exception');
$snapshot = seedEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
$review = composeEnvironmentReviewForTest($tenant, $user, $snapshot);
$review->forceFill([
'status' => EnvironmentReviewStatus::Published->value,
'published_at' => now(),
'published_by_user_id' => (int) $user->getKey(),
])->save();
$decisionSummary = data_get($review->fresh(), 'summary.governance_package.decision_summary');
expect($decisionSummary)->toBeArray()
->and($decisionSummary['status'] ?? null)->toBe('requires_awareness')
->and($decisionSummary['total_count'] ?? null)->toBe(1)
->and($decisionSummary['summary'] ?? null)->toContain('1 governance decision requires customer awareness')
->and($decisionSummary['next_action'] ?? null)->toBe('Review the accepted-risk decision basis before customer delivery.')
->and(data_get($decisionSummary, 'entries.0.title'))->toBe('Privileged role exception')
->and(data_get($decisionSummary, 'entries.0.awareness_reason'))->toContain('expired')
->and(data_get($decisionSummary, 'entries.0.next_action'))->toBe('Confirm whether this accepted risk should be renewed, remediated, or removed before relying on the review.')
->and(json_encode($decisionSummary, JSON_THROW_ON_ERROR))->not->toContain(
'Hidden tenant exception',
'spec-308-raw-fingerprint',
'raw-run',
'OperationRun',
'Reason owner',
'Platform reason family',
);
$this->actingAs($user)
->get(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant).'?customer_workspace=1&source_surface=customer_review_workspace')
->assertOk()
->assertSee('Governance decisions requiring awareness')
->assertSee('Privileged role exception')
->assertSee('Review the accepted-risk decision basis before customer delivery.')
->assertDontSee('Hidden tenant exception')
->assertDontSee('raw-run')
->assertDontSeeText('Approve exception')
->assertDontSeeText('Reject exception')
->assertDontSeeText('Renew exception')
->assertDontSeeText('Revoke exception');
});
it('distinguishes no decision awareness from incomplete decision evidence', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$reviewWithNoDecisions = composeEnvironmentReviewForTest(
tenant: $tenant,
user: $user,
snapshot: seedEnvironmentReviewEvidence($tenant, findingCount: 2, driftCount: 0),
);
$noDecisionSummary = data_get($reviewWithNoDecisions, 'summary.governance_package.decision_summary');
expect($noDecisionSummary)->toBeArray()
->and($noDecisionSummary['status'] ?? null)->toBe('none')
->and($noDecisionSummary['total_count'] ?? null)->toBe(0)
->and($noDecisionSummary['summary'] ?? null)->toBe('No governance decisions require customer awareness in this released review.');
$partialTenant = ManagedEnvironment::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'name' => 'Partial Evidence Environment',
]);
createUserWithTenant(tenant: $partialTenant, user: $user, role: 'owner');
$partialReview = composeEnvironmentReviewForTest(
tenant: $partialTenant,
user: $user,
snapshot: seedPartialEnvironmentReviewEvidence($partialTenant, findingCount: 0, driftCount: 0),
);
$unavailableSummary = data_get($partialReview, 'summary.governance_package.decision_summary');
expect($unavailableSummary)->toBeArray()
->and($unavailableSummary['status'] ?? null)->toBe('unavailable')
->and($unavailableSummary['total_count'] ?? null)->toBe(0)
->and($unavailableSummary['summary'] ?? null)->toContain('Decision evidence is incomplete');
});

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
use App\Models\PlatformUser;
use App\Models\User;
use Filament\PanelRegistry;
it('keeps tenant users scoped to the admin panel contract', function (): void {
$user = User::factory()->make();
expect($user->canAccessPanel(app(PanelRegistry::class)->get('admin')))->toBeTrue()
->and($user->canAccessPanel(app(PanelRegistry::class)->get('system')))->toBeFalse();
});
it('redirects unauthenticated direct admin panel access to admin login', function (): void {
$this->get('/admin')->assertRedirectContains('/admin/login');
});
it('denies platform sessions on admin panel routes as not found', function (): void {
$platformUser = PlatformUser::factory()->create();
$this->actingAs($platformUser, 'platform')
->get('/admin')
->assertNotFound();
});
it('does not render workspace admin surfaces for users without workspace authority', function (): void {
$user = User::factory()->create();
$this->actingAs($user)
->get('/admin')
->assertRedirect('/admin/choose-workspace');
$this->actingAs($user)
->get('/admin/alerts')
->assertRedirect('/admin/choose-workspace');
});

View File

@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
use App\Models\Finding;
use App\Models\FindingException;
use App\Models\ManagedEnvironment;
use App\Models\ManagedEnvironmentMembership;
use App\Models\User;
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Services\Auth\CapabilityResolver;
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
use App\Support\Auth\WorkspaceRole;
use App\Support\Workspaces\WorkspaceContext;
use Illuminate\Support\Facades\Gate;
it('denies readonly accepted-risk approval lifecycle actions server-side', function (): void {
[$owner, $tenant] = createUserWithTenant(role: WorkspaceRole::Owner->value);
[$readonly] = createUserWithTenant(tenant: $tenant, role: WorkspaceRole::Readonly->value);
$finding = Finding::factory()->for($tenant)->create([
'workspace_id' => (int) $tenant->workspace_id,
]);
$exception = FindingException::query()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'finding_id' => (int) $finding->getKey(),
'requested_by_user_id' => (int) $owner->getKey(),
'owner_user_id' => (int) $owner->getKey(),
'status' => FindingException::STATUS_PENDING,
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
'request_reason' => 'Boundary denial proof',
'requested_at' => now(),
'review_due_at' => now()->addDays(7),
'evidence_summary' => ['reference_count' => 0],
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
foreach (['approve', 'reject'] as $ability) {
$response = Gate::forUser($readonly)->inspect($ability, $exception);
expect($response->denied())->toBeTrue()
->and($response->status())->not->toBe(404);
}
});
it('denies same-workspace wrong-environment finding exceptions as not found', function (): void {
$workspace = Workspace::factory()->create();
$allowedTenant = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
]);
$deniedTenant = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
]);
$user = User::factory()->create();
$owner = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => WorkspaceRole::Manager->value,
]);
ManagedEnvironmentMembership::query()->create([
'managed_environment_id' => (int) $allowedTenant->getKey(),
'user_id' => (int) $user->getKey(),
'role' => WorkspaceRole::Readonly->value,
'source' => 'manual',
]);
app(CapabilityResolver::class)->clearCache();
app(ManagedEnvironmentAccessScopeResolver::class)->clearCache();
$finding = Finding::factory()->for($deniedTenant)->create([
'workspace_id' => (int) $workspace->getKey(),
]);
$exception = FindingException::query()->create([
'workspace_id' => (int) $workspace->getKey(),
'managed_environment_id' => (int) $deniedTenant->getKey(),
'finding_id' => (int) $finding->getKey(),
'requested_by_user_id' => (int) $owner->getKey(),
'owner_user_id' => (int) $owner->getKey(),
'status' => FindingException::STATUS_PENDING,
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
'request_reason' => 'Wrong environment boundary proof',
'requested_at' => now(),
'review_due_at' => now()->addDays(7),
'evidence_summary' => ['reference_count' => 0],
]);
$allowedTenant->makeCurrent();
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
app(WorkspaceContext::class)->rememberLastTenantId((int) $workspace->getKey(), (int) $allowedTenant->getKey());
$response = Gate::forUser($user)->inspect('view', $exception);
expect($response->denied())->toBeTrue()
->and($response->status())->toBe(404);
});

View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
use App\Filament\Resources\EnvironmentReviewResource;
use App\Filament\Resources\EvidenceSnapshotResource;
use App\Models\EvidenceSnapshot;
use App\Models\ManagedEnvironment;
use App\Models\ManagedEnvironmentMembership;
use App\Models\EnvironmentReview;
use App\Models\User;
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Services\Auth\CapabilityResolver;
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
use App\Support\Auth\WorkspaceRole;
use App\Support\Evidence\EvidenceCompletenessState;
use App\Support\Evidence\EvidenceSnapshotStatus;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Facades\Filament;
it('denies same-workspace wrong-environment review direct access as not found', function (): void {
$workspace = Workspace::factory()->create();
$allowedTenant = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
]);
$deniedTenant = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
]);
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => WorkspaceRole::Manager->value,
]);
ManagedEnvironmentMembership::query()->create([
'managed_environment_id' => (int) $allowedTenant->getKey(),
'user_id' => (int) $user->getKey(),
'role' => WorkspaceRole::Readonly->value,
'source' => 'manual',
]);
app(CapabilityResolver::class)->clearCache();
app(ManagedEnvironmentAccessScopeResolver::class)->clearCache();
$snapshot = EvidenceSnapshot::query()->create([
'workspace_id' => (int) $workspace->getKey(),
'managed_environment_id' => (int) $deniedTenant->getKey(),
'status' => EvidenceSnapshotStatus::Active->value,
'completeness_state' => EvidenceCompletenessState::Complete->value,
'summary' => ['finding_count' => 1],
'generated_at' => now(),
]);
$review = EnvironmentReview::factory()->ready()->create([
'workspace_id' => (int) $workspace->getKey(),
'managed_environment_id' => (int) $deniedTenant->getKey(),
'evidence_snapshot_id' => (int) $snapshot->getKey(),
]);
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
->get(EnvironmentReviewResource::getUrl('view', ['record' => $review], panel: 'admin', tenant: $allowedTenant))
->assertNotFound();
});
it('denies same-workspace wrong-environment evidence direct access as not found', function (): void {
$workspace = Workspace::factory()->create();
$allowedTenant = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
]);
$deniedTenant = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
]);
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => WorkspaceRole::Manager->value,
]);
ManagedEnvironmentMembership::query()->create([
'managed_environment_id' => (int) $allowedTenant->getKey(),
'user_id' => (int) $user->getKey(),
'role' => WorkspaceRole::Readonly->value,
'source' => 'manual',
]);
app(CapabilityResolver::class)->clearCache();
app(ManagedEnvironmentAccessScopeResolver::class)->clearCache();
$snapshot = EvidenceSnapshot::query()->create([
'workspace_id' => (int) $workspace->getKey(),
'managed_environment_id' => (int) $deniedTenant->getKey(),
'status' => EvidenceSnapshotStatus::Active->value,
'completeness_state' => EvidenceCompletenessState::Complete->value,
'summary' => ['finding_count' => 1],
'generated_at' => now(),
]);
$allowedTenant->makeCurrent();
Filament::setTenant($allowedTenant, true);
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
->get(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], panel: 'admin', tenant: $allowedTenant))
->assertNotFound();
});

View File

@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
use App\Models\ManagedEnvironment;
use App\Models\ManagedEnvironmentMembership;
use App\Models\OperationRun;
use App\Models\User;
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
use App\Support\Auth\WorkspaceRole;
use Illuminate\Support\Facades\Gate;
it('denies cross-workspace operation runs as not found', function (): void {
$workspace = Workspace::factory()->create();
$otherWorkspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => WorkspaceRole::Readonly->value,
]);
$run = OperationRun::factory()->tenantlessForWorkspace($otherWorkspace)->create([
'type' => 'environment.review.compose',
]);
$response = Gate::forUser($user)->inspect('view', $run);
expect($response->denied())->toBeTrue()
->and($response->status())->toBe(404);
});
it('denies same-workspace wrong-environment operation runs as not found', function (): void {
$workspace = Workspace::factory()->create();
$allowedTenant = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
]);
$deniedTenant = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
]);
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => WorkspaceRole::Operator->value,
]);
ManagedEnvironmentMembership::query()->create([
'managed_environment_id' => (int) $allowedTenant->getKey(),
'user_id' => (int) $user->getKey(),
'role' => WorkspaceRole::Operator->value,
'source' => 'manual',
]);
app(ManagedEnvironmentAccessScopeResolver::class)->clearCache();
$run = OperationRun::factory()->forTenant($deniedTenant)->create([
'type' => 'provider.connection.check',
]);
$response = Gate::forUser($user)->inspect('view', $run);
expect($response->denied())->toBeTrue()
->and($response->status())->toBe(404);
});
it('keeps in-scope operation capability denials distinct from scope boundaries', function (): void {
$workspace = Workspace::factory()->create();
$tenant = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
]);
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => WorkspaceRole::Readonly->value,
]);
$run = OperationRun::factory()->forTenant($tenant)->create([
'type' => 'inventory.sync',
]);
$response = Gate::forUser($user)->inspect('view', $run);
expect($response->denied())->toBeTrue()
->and($response->status())->not->toBe(404);
});

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
use App\Models\ManagedEnvironment;
use App\Models\ManagedEnvironmentMembership;
use App\Models\ProviderConnection;
use App\Models\User;
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Services\Auth\CapabilityResolver;
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
use App\Support\Auth\WorkspaceRole;
use App\Support\Workspaces\WorkspaceContext;
use Illuminate\Support\Facades\Gate;
it('keeps provider credential-level authority owner only', function (): void {
$workspace = Workspace::factory()->create();
$tenant = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
]);
$connection = ProviderConnection::factory()->withCredential()->create([
'workspace_id' => (int) $workspace->getKey(),
'managed_environment_id' => (int) $tenant->getKey(),
]);
foreach (WorkspaceRole::cases() as $role) {
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => $role->value,
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
app(CapabilityResolver::class)->clearCache();
app(ManagedEnvironmentAccessScopeResolver::class)->clearCache();
expect(Gate::forUser($user)->allows('manageDedicatedCredential', $connection))->toBe($role === WorkspaceRole::Owner);
expect(Gate::forUser($user)->allows('deleteDedicatedCredential', $connection))->toBe($role === WorkspaceRole::Owner);
}
});
it('denies same-workspace wrong-environment provider connections as not found', function (): void {
$workspace = Workspace::factory()->create();
$allowedTenant = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
]);
$deniedTenant = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
]);
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => WorkspaceRole::Manager->value,
]);
ManagedEnvironmentMembership::query()->create([
'managed_environment_id' => (int) $allowedTenant->getKey(),
'user_id' => (int) $user->getKey(),
'role' => WorkspaceRole::Readonly->value,
'source' => 'manual',
]);
app(CapabilityResolver::class)->clearCache();
app(ManagedEnvironmentAccessScopeResolver::class)->clearCache();
$connection = ProviderConnection::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'managed_environment_id' => (int) $deniedTenant->getKey(),
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$response = Gate::forUser($user)->inspect('view', $connection);
expect($response->denied())->toBeTrue()
->and($response->status())->toBe(404);
});

View File

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
use App\Filament\Resources\ReviewPackResource;
use App\Models\ManagedEnvironment;
use App\Models\ManagedEnvironmentMembership;
use App\Models\ReviewPack;
use App\Models\User;
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Services\Auth\CapabilityResolver;
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
use App\Support\Auth\WorkspaceRole;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Facades\Filament;
use Illuminate\Support\Facades\Gate;
it('denies cross-workspace review pack direct access as not found', function (): void {
$targetTenant = ManagedEnvironment::factory()->active()->create();
$otherTenant = ManagedEnvironment::factory()->active()->create();
[$user] = createUserWithTenant($otherTenant, role: WorkspaceRole::Owner->value);
$pack = ReviewPack::factory()->ready()->create([
'managed_environment_id' => (int) $targetTenant->getKey(),
'workspace_id' => (int) $targetTenant->workspace_id,
]);
$this->actingAs($user)
->get(ReviewPackResource::getUrl('view', ['record' => $pack], panel: 'admin', tenant: $targetTenant))
->assertNotFound();
});
it('denies same-workspace wrong-environment review pack direct access as not found', function (): void {
$workspace = Workspace::factory()->create();
$allowedTenant = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
]);
$deniedTenant = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
]);
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => WorkspaceRole::Manager->value,
]);
ManagedEnvironmentMembership::query()->create([
'managed_environment_id' => (int) $allowedTenant->getKey(),
'user_id' => (int) $user->getKey(),
'role' => WorkspaceRole::Readonly->value,
'source' => 'manual',
]);
app(CapabilityResolver::class)->clearCache();
app(ManagedEnvironmentAccessScopeResolver::class)->clearCache();
$pack = ReviewPack::factory()->ready()->create([
'managed_environment_id' => (int) $deniedTenant->getKey(),
'workspace_id' => (int) $workspace->getKey(),
]);
$allowedTenant->makeCurrent();
Filament::setTenant($allowedTenant, true);
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
->get(ReviewPackResource::getUrl('view', ['record' => $pack], panel: 'admin', tenant: $allowedTenant))
->assertNotFound();
});
it('denies readonly review pack mutation server-side', function (): void {
[$user, $tenant] = createUserWithTenant(role: WorkspaceRole::Readonly->value);
$pack = ReviewPack::factory()->ready()->create([
'managed_environment_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
]);
$tenant->makeCurrent();
Filament::setTenant($tenant, true);
$response = Gate::forUser($user)->inspect('delete', $pack);
expect($response->denied())->toBeTrue()
->and($response->status())->not->toBe(404);
});

View File

@ -23,6 +23,6 @@
expect($gate->allows(Capabilities::AUDIT_VIEW, $tenant))->toBeTrue();
expect($gate->allows(Capabilities::TENANT_MEMBERSHIP_MANAGE, $tenant))->toBeTrue();
expect($gate->allows(Capabilities::TENANT_MEMBERSHIP_MANAGE, $tenant))->toBeFalse();
expect($gate->allows(Capabilities::TENANT_DELETE, $tenant))->toBeFalse();
});

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
use App\Models\PlatformUser;
use App\Models\User;
use App\Support\Auth\PlatformCapabilities;
use Filament\PanelRegistry;
it('keeps platform users scoped to the system panel contract', function (): void {
$platformUser = PlatformUser::factory()->make([
'capabilities' => [PlatformCapabilities::ACCESS_SYSTEM_PANEL],
]);
expect($platformUser->canAccessPanel(app(PanelRegistry::class)->get('system')))->toBeTrue()
->and($platformUser->canAccessPanel(app(PanelRegistry::class)->get('admin')))->toBeFalse();
});
it('denies ordinary workspace users on system panel routes as not found', function (): void {
[$user] = createUserWithTenant(role: 'owner');
$this->actingAs($user)
->get('/system')
->assertNotFound();
$this->actingAs($user)
->get('/system/directory/workspaces')
->assertNotFound();
});
it('keeps missing system capability as forbidden for platform users', function (): void {
$platformUser = PlatformUser::factory()->create([
'capabilities' => [],
'is_active' => true,
]);
$this->actingAs($platformUser, 'platform')
->get('/system')
->assertForbidden();
});

View File

@ -77,4 +77,28 @@
return $action->getTooltip() === 'You do not have permission to manage workspace memberships.';
});
});
it('shows membership actions as visible but disabled for manager members', function () {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'manager',
]);
$this->actingAs($user);
Livewire::test(WorkspaceMembershipsRelationManager::class, [
'ownerRecord' => $workspace,
'pageClass' => EditWorkspace::class,
])
->assertTableActionVisible('add_member')
->assertTableActionDisabled('add_member')
->assertTableActionVisible('change_role')
->assertTableActionDisabled('change_role')
->assertTableActionVisible('remove')
->assertTableActionDisabled('remove');
});
});

View File

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Services\Auth\ManagedEnvironmentMembershipManager;
use App\Services\Auth\WorkspaceMembershipManager;
use App\Support\Auth\Capabilities;
use App\Support\Auth\WorkspaceRole;
use Illuminate\Support\Facades\Gate;
it('gates workspace membership management as owner only', function (): void {
$workspace = Workspace::factory()->create();
foreach (WorkspaceRole::cases() as $role) {
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => $role->value,
]);
expect(Gate::forUser($user)->allows(Capabilities::WORKSPACE_MEMBERSHIP_VIEW, $workspace))->toBe($role !== WorkspaceRole::Readonly);
expect(Gate::forUser($user)->allows(Capabilities::WORKSPACE_MEMBERSHIP_MANAGE, $workspace))->toBe($role === WorkspaceRole::Owner);
}
});
it('denies non-owner workspace membership service mutations', function (string $role): void {
$workspace = Workspace::factory()->create();
$actor = User::factory()->create();
$member = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $actor->getKey(),
'role' => $role,
]);
app(WorkspaceMembershipManager::class)->addMember(
workspace: $workspace,
actor: $actor,
member: $member,
role: WorkspaceRole::Readonly->value,
);
})->throws(\DomainException::class, 'Forbidden.')->with([
'manager' => [WorkspaceRole::Manager->value],
'operator' => [WorkspaceRole::Operator->value],
'readonly' => [WorkspaceRole::Readonly->value],
]);
it('allows owner workspace membership service mutations', function (): void {
$workspace = Workspace::factory()->create();
$owner = User::factory()->create();
$member = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $owner->getKey(),
'role' => WorkspaceRole::Owner->value,
]);
$membership = app(WorkspaceMembershipManager::class)->addMember(
workspace: $workspace,
actor: $owner,
member: $member,
role: WorkspaceRole::Readonly->value,
);
expect($membership->workspace_id)->toBe((int) $workspace->getKey())
->and($membership->user_id)->toBe((int) $member->getKey())
->and($membership->role)->toBe(WorkspaceRole::Readonly->value);
});
it('denies manager managed-environment access-scope management', function (): void {
$workspace = Workspace::factory()->create();
$tenant = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
]);
$manager = User::factory()->create();
$member = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $manager->getKey(),
'role' => WorkspaceRole::Manager->value,
]);
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $member->getKey(),
'role' => WorkspaceRole::Readonly->value,
]);
app(ManagedEnvironmentMembershipManager::class)->grantScope(
tenant: $tenant,
actor: $manager,
member: $member,
);
})->throws(\DomainException::class, 'Forbidden.');

View File

@ -4,7 +4,13 @@
use App\Filament\Resources\ReviewPackResource;
use App\Jobs\GenerateReviewPackJob;
use App\Models\Finding;
use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Services\Findings\FindingExceptionService;
use App\Services\Findings\FindingRiskGovernanceResolver;
use App\Services\ReviewPackService;
use App\Support\EnvironmentReviewStatus;
use App\Support\ReviewPackStatus;
use Illuminate\Support\Facades\Storage;
@ -12,6 +18,40 @@
Storage::fake('exports');
});
function spec308SeedPackDecisionFinding(ManagedEnvironment $tenant, User $requester, string $title): Finding
{
$approver = User::factory()->create(['name' => 'Risk Owner']);
createUserWithTenant(tenant: $tenant, user: $approver, role: 'owner', workspaceRole: 'manager');
/** @var FindingExceptionService $exceptionService */
$exceptionService = app(FindingExceptionService::class);
$finding = Finding::factory()->for($tenant)->riskAccepted()->create([
'fingerprint' => 'spec-308-pack-fingerprint',
'evidence_jsonb' => [
'display_name' => $title,
'internal_url' => 'https://tenantpilot.test/admin/operations/raw-review-pack-run',
],
]);
$requested = $exceptionService->request($finding, $tenant, $requester, [
'owner_user_id' => (int) $approver->getKey(),
'request_reason' => 'Customer owner approved temporary exception.',
'review_due_at' => now()->addDays(5)->toDateTimeString(),
'expires_at' => now()->addDays(14)->toDateTimeString(),
]);
$exceptionService->approve($requested, $approver, [
'effective_from' => now()->subDays(10)->toDateTimeString(),
'expires_at' => now()->subDay()->toDateTimeString(),
'approval_reason' => 'Approved with customer controls.',
]);
app(FindingRiskGovernanceResolver::class)->syncExceptionState($finding->findingException()->firstOrFail());
return $finding->refresh();
}
it('generates a review-derived executive pack with environment-review metadata and filtered sections', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$review = composeEnvironmentReviewForTest($tenant, $user);
@ -75,3 +115,69 @@
->assertSee('#'.$review->getKey())
->assertSee('Review status');
});
it('includes the customer-safe decision summary in review-derived pack JSON and markdown', function (): void {
[$user, $tenant] = createUserWithTenant(
tenant: ManagedEnvironment::factory()->create(['name' => 'Contoso Decision Tenant']),
role: 'owner',
);
spec308SeedPackDecisionFinding($tenant, $user, 'Privileged access accepted risk');
$snapshot = seedEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
$review = composeEnvironmentReviewForTest($tenant, $user, $snapshot);
$review->forceFill([
'status' => EnvironmentReviewStatus::Published->value,
'published_at' => now(),
'published_by_user_id' => (int) $user->getKey(),
])->save();
$pack = app(ReviewPackService::class)->generateFromReview($review, $user, [
'include_pii' => false,
'include_operations' => false,
]);
$job = new GenerateReviewPackJob(
reviewPackId: (int) $pack->getKey(),
operationRunId: (int) $pack->operation_run_id,
);
app()->call([$job, 'handle']);
$pack->refresh();
expect(data_get($pack->summary, 'governance_package.decision_summary.status'))->toBe('requires_awareness')
->and(data_get($pack->summary, 'governance_package.decision_summary.total_count'))->toBe(1)
->and(data_get($pack->summary, 'delivery_bundle.contract'))->toBe(ReviewPackService::REVIEW_DERIVED_DELIVERY_CONTRACT);
$zipContent = Storage::disk('exports')->get((string) $pack->file_path);
$tempFile = tempnam(sys_get_temp_dir(), 'review-derived-pack-decisions-');
file_put_contents($tempFile, $zipContent);
$zip = new ZipArchive;
$zip->open($tempFile);
$filenames = collect(range(0, $zip->numFiles - 1))
->map(fn (int $index): string => (string) $zip->getNameIndex($index))
->values()
->all();
$metadata = json_decode((string) $zip->getFromName('metadata.json'), true, 512, JSON_THROW_ON_ERROR);
$summary = json_decode((string) $zip->getFromName('summary.json'), true, 512, JSON_THROW_ON_ERROR);
$executiveEntrypoint = (string) $zip->getFromName(ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME);
$summaryJson = json_encode($summary, JSON_THROW_ON_ERROR);
expect($filenames)->toContain('metadata.json', 'summary.json', 'sections.json', ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME)
->and(collect($filenames)->filter(fn (string $filename): bool => str_starts_with($filename, 'executive-'))->values()->all())->toBe([ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME])
->and(data_get($metadata, 'delivery_bundle.contract'))->toBe(ReviewPackService::REVIEW_DERIVED_DELIVERY_CONTRACT)
->and(data_get($summary, 'delivery_bundle.contract'))->toBe(ReviewPackService::REVIEW_DERIVED_DELIVERY_CONTRACT)
->and(data_get($summary, 'governance_package.decision_summary.status'))->toBe('requires_awareness')
->and(data_get($summary, 'governance_package.decision_summary.total_count'))->toBe(1)
->and(data_get($summary, 'governance_package.decision_summary.entries.0.title'))->toBe('Privileged access accepted risk')
->and($executiveEntrypoint)->toContain('## Governance decisions requiring awareness')
->and($executiveEntrypoint)->toContain('1 governance decision requires customer awareness')
->and($executiveEntrypoint)->toContain('Privileged access accepted risk')
->and($executiveEntrypoint)->toContain('Review the accepted-risk decision basis before customer delivery.')
->and($summaryJson)->not->toContain('Contoso Decision Tenant', 'Risk Owner', 'spec-308-pack-fingerprint', 'raw-review-pack-run')
->and($executiveEntrypoint)->not->toContain('Contoso Decision Tenant', 'Risk Owner', 'spec-308-pack-fingerprint', 'raw-review-pack-run', 'OperationRun URL');
$zip->close();
unlink($tempFile);
});

View File

@ -30,7 +30,7 @@
expect($resolver->can($manager, $tenant, Capabilities::PROVIDER_MANAGE))->toBeTrue();
expect($resolver->can($manager, $tenant, Capabilities::TENANT_BACKUP_SCHEDULES_MANAGE))->toBeTrue();
expect($resolver->can($manager, $tenant, Capabilities::TENANT_BACKUP_SCHEDULES_RUN))->toBeTrue();
expect($resolver->can($manager, $tenant, Capabilities::TENANT_MEMBERSHIP_MANAGE))->toBeTrue();
expect($resolver->can($manager, $tenant, Capabilities::TENANT_MEMBERSHIP_MANAGE))->toBeFalse();
expect($resolver->can($manager, $tenant, Capabilities::TENANT_ROLE_MAPPING_MANAGE))->toBeFalse();
expect($resolver->isMember($operator, $tenant))->toBeTrue();

View File

@ -0,0 +1,35 @@
<?php
use App\Services\Auth\RoleCapabilityMap;
use App\Services\Auth\WorkspaceRoleCapabilityMap;
use App\Support\Auth\Capabilities;
use App\Support\Auth\WorkspaceRole;
use App\Support\TenantRole;
it('uses only canonical capabilities in workspace role grants', function (): void {
foreach (WorkspaceRole::cases() as $role) {
$capabilities = WorkspaceRoleCapabilityMap::getCapabilities($role);
expect($capabilities)
->toBe(array_values(array_unique($capabilities)));
foreach ($capabilities as $capability) {
expect(Capabilities::isKnown($capability))->toBeTrue();
}
}
});
it('keeps workspace and tenant membership management owner only', function (): void {
expect(WorkspaceRoleCapabilityMap::rolesWithCapability(Capabilities::WORKSPACE_MEMBERSHIP_MANAGE))
->toBe([WorkspaceRole::Owner->value]);
expect(WorkspaceRoleCapabilityMap::hasCapability(WorkspaceRole::Owner, Capabilities::WORKSPACE_MEMBERSHIP_MANAGE))->toBeTrue();
expect(WorkspaceRoleCapabilityMap::hasCapability(WorkspaceRole::Owner, Capabilities::TENANT_MEMBERSHIP_MANAGE))->toBeTrue();
foreach ([WorkspaceRole::Manager, WorkspaceRole::Operator, WorkspaceRole::Readonly] as $role) {
expect(WorkspaceRoleCapabilityMap::hasCapability($role, Capabilities::WORKSPACE_MEMBERSHIP_MANAGE))->toBeFalse();
expect(WorkspaceRoleCapabilityMap::hasCapability($role, Capabilities::TENANT_MEMBERSHIP_MANAGE))->toBeFalse();
}
expect(RoleCapabilityMap::hasCapability(TenantRole::Manager, Capabilities::TENANT_MEMBERSHIP_MANAGE))->toBeFalse();
});

View File

@ -0,0 +1,44 @@
# Requirements Checklist: Decision Register Customer-Safe Summary & Review-Pack Inclusion
**Purpose**: Validate that the preparation artifacts are clear, bounded, testable, and aligned with TenantPilot constitution guardrails before implementation.
**Created**: 2026-05-15
**Feature**: [spec.md](../spec.md)
## Requirement Quality
- [x] CHK001 The selected candidate is explicitly sourced from the user request, roadmap priority ranking, and manual-promotion backlog.
- [x] CHK002 The scope is narrowed to existing Decision Register context, Environment Review summaries, Customer Review Workspace/review detail, and review-derived Review Pack export.
- [x] CHK003 Functional requirements are testable and avoid implementation-only wording where user behavior is the point.
- [x] CHK004 Acceptance criteria cover positive, empty/none, unavailable/incomplete, redaction, and cross-scope scenarios.
- [x] CHK005 Out-of-scope boundaries explicitly reject new decision persistence, new workflow engines, new customer approval actions, new OperationRun lifecycle, and broad export redesign.
## Constitution And Product Truth
- [x] CHK006 The spec identifies the source of truth for decisions, reviews, review packs, evidence, and operation runs.
- [x] CHK007 The proportionality review states no new persisted entity, enum/status family, public framework, or artifact family is planned.
- [x] CHK008 Existing completed specs 265, 306, 307, 109, 258, and 260 are treated as context only and are not modified.
- [x] CHK009 Provider-boundary requirements avoid spreading Microsoft/provider-specific semantics into platform-core review/export truth.
- [x] CHK010 Customer-safe disclosure requirements preserve non-certification wording and avoid raw/debug/internal detail.
## UI, RBAC, And Operations
- [x] CHK011 The affected surfaces carry consistent native/shared Filament classification and do not require ad-hoc styling or new assets.
- [x] CHK012 Filament v5 / Livewire v4, provider registration, global search, destructive action confirmation, and asset strategy requirements are explicit.
- [x] CHK013 RBAC requirements preserve workspace/environment isolation, non-member 404, and missing-capability 403 semantics.
- [x] CHK014 OperationRun impact is explicitly bounded to existing review-pack generation behavior with no new run UX.
- [x] CHK015 Auditability requirements preserve existing review and review-pack telemetry/audit semantics and avoid new audit action IDs unless scope changes.
## Test Governance
- [x] CHK016 Planned Pest coverage uses existing focused EnvironmentReview, ReviewPack, and Reviews feature families.
- [x] CHK017 Browser coverage is bounded to the existing CustomerReviewWorkspace smoke only if rendered UI changes.
- [x] CHK018 Fixture/helper cost risks are identified and kept feature-local.
- [x] CHK019 Validation commands are concrete and scoped.
- [x] CHK020 The task list includes explicit Filament/RBAC/asset/no-migration contract review tasks.
## Preparation Outcome
- [x] Outcome class: acceptable-special-case.
- [x] Workflow outcome: keep.
- [x] Final note location: active feature PR close-out entry `Guardrail / Smoke Coverage`.
- [x] Spec Readiness Gate passes for preparation: `spec.md`, `plan.md`, `tasks.md`, and this checklist exist, contain no placeholders, and keep implementation scope bounded to Spec 308.

View File

@ -0,0 +1,287 @@
# Implementation Plan: Decision Register Customer-Safe Summary & Review-Pack Inclusion
**Branch**: `308-decision-register-summary-review-pack` | **Date**: 2026-05-15 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/308-decision-register-summary-review-pack/spec.md`
## Summary
Add a bounded customer-safe Decision Register summary to existing released-review governance-package truth and review-derived Review Pack exports. The implementation should extend current Environment Review and Review Pack composition paths, not create a new decision store, export subsystem, customer approval workflow, or OperationRun lifecycle.
## Technical Context
**Language/Version**: PHP 8.4.15, Laravel 12.52.0
**Primary Dependencies**: Filament 5.2.1, Livewire 4.1.4, Pest 4.3.1, Laravel Sail
**Storage**: PostgreSQL; existing JSONB fields on `environment_reviews.summary`, `review_packs.summary`, and generated files on the existing `exports` disk
**Testing**: Pest 4 feature tests and optional bounded browser smoke
**Validation Lanes**: confidence; browser only if rendered UI changes
**Target Platform**: Laravel Sail locally; Dokploy container deployment for staging/production
**Project Type**: single Laravel app under `apps/platform`
**Performance Goals**: Derived summary composition remains ordinary review/pack work; no new query-heavy global scan
**Constraints**: no migrations expected; no new assets; no new operation type; no customer-safe raw diagnostic leakage
**Scale/Scope**: one managed-environment released-review and review-pack flow at a time
## UI / Surface Guardrail Plan
- **Guardrail scope**: changed customer-safe review/package surfaces and exported artifact content.
- **Native vs custom classification summary**: native/shared existing Filament surfaces plus existing export files.
- **Shared-family relevance**: customer review consumption, governance-package summary, Review Pack artifact truth, evidence/report disclosure, Decision Register context.
- **State layers in scope**: page, detail, artifact content; no shell change.
- **Audience modes in scope**: customer/read-only and operator-MSP.
- **Decision/diagnostic/raw hierarchy plan**: customer-safe decision summary first, existing detail/sections second, raw/support diagnostics absent from default customer paths.
- **Raw/support gating plan**: no raw OperationRun URLs, fingerprints, platform reason families, or provider payloads in customer-safe summary/export.
- **One-primary-action / duplicate-truth control**: preserve existing `Open review`, `Download governance package`, and `Download` actions; summary content must not add peer operator-only actions.
- **Handling modes by drift class or surface**: review-mandatory for customer-safe copy/export changes; report-only for unchanged Decision Register page.
- **Repository-signal treatment**: review-mandatory because this touches customer-safe review consumption and exported artifacts.
- **Special surface test profiles**: shared-detail-family plus standard-native-filament.
- **Required tests or manual smoke**: focused feature tests; existing bounded browser smoke if rendered customer workspace/review detail changes.
- **Exception path and spread control**: none planned.
- **Active feature PR close-out entry**: Guardrail / Smoke Coverage.
## Shared Pattern & System Fit
- **Cross-cutting feature marker**: yes.
- **Systems touched**: `EnvironmentReviewComposer`, `EnvironmentReviewSectionFactory`, `CustomerReviewWorkspace`, `EnvironmentReviewResource`, `ReviewPackService`, `GenerateReviewPackJob`, `ReviewPackResource`, review/review-pack tests.
- **Shared abstractions reused**: existing `governance_package` summary, existing `auditor_ready_executive_export.v1` delivery contract, existing `ReviewPackService::generateFromReview()`, existing `executive-summary.md` generation, existing BADGE-001 rendering if UI labels need badges.
- **New abstraction introduced? why?**: none planned. Private methods in existing classes are acceptable if needed to keep summary derivation readable.
- **Why the existing abstraction was sufficient or insufficient**: Existing review and pack composition paths already own customer-safe summary and export truth; they lack explicit Decision Register follow-through proof. Existing operator register builder is intentionally too operator-focused for direct customer export.
- **Bounded deviation / spread control**: no public framework or new artifact family. Any new summary keys stay nested under existing review/pack summary payloads.
## OperationRun UX Impact
- **Touches OperationRun start/completion/link UX?**: no new start/completion/link UX. Existing review-pack generation run behavior remains.
- **Central contract reused**: existing `ReviewPackService`, `GenerateReviewPackJob`, `OperationRunService`, and existing `OperationRunLinks` usage where already present.
- **Delegated UX behaviors**: existing Review Pack generation / run feedback only.
- **Surface-owned behavior kept local**: customer-safe copy may mention evidence/package availability, not run diagnostics.
- **Queued DB-notification policy**: N/A.
- **Terminal notification path**: existing Review Pack terminal handling remains.
- **Exception path**: none.
## Provider Boundary & Portability Fit
- **Shared provider/platform boundary touched?**: no.
- **Provider-owned seams**: none.
- **Platform-core seams**: review/governance-package summary and review-pack export artifact content.
- **Neutral platform terms / contracts preserved**: governance decision, accepted risk, evidence basis, review, review pack, managed environment, workspace.
- **Retained provider-specific semantics and why**: existing finding/evidence titles may include provider context; this feature does not add provider coupling.
- **Bounded extraction or follow-up path**: none.
## Constitution Check
- Inventory-first: PASS. The feature uses existing evidence/review snapshot truth; it does not query Graph or change inventory behavior.
- Read/write separation: PASS. Customer-safe summary is derived/read-only. Existing review-pack generation remains the only write path and already uses confirmation/authorization semantics where applicable.
- Graph contract path: PASS. No Graph calls or contracts are touched.
- Deterministic capabilities: PASS. Existing review/review-pack capabilities remain.
- RBAC-UX: PASS. Existing workspace/environment membership and review/review-pack policy behavior remain server-side boundaries.
- Workspace isolation: PASS. Summary derivation must scope to the released review workspace.
- Tenant/environment isolation: PASS. Summary derivation must scope to the managed environment under review.
- Run observability: PASS. Existing Review Pack `OperationRun` remains; no new run type or local run UX.
- TEST-GOV-001: PASS with focused feature coverage and optional bounded browser smoke.
- PROP-001 / BLOAT-001: PASS. No new table, enum family, service framework, or export family.
- PERSIST-001: PASS. Existing review and review-pack artifacts receive derived content; no new persisted truth.
- STATE-001: PASS. No new lifecycle/status family.
- UI-SEM-001: PASS. Customer-safe copy stays a derived summary, not a UI framework.
- XCUT-001: PASS. Reuses existing review and review-pack shared paths.
- PROV-001: PASS. No provider seam changes.
- UI-FIL-001: PASS. Existing Filament-native surfaces stay native/shared-primitives first; no ad-hoc CSS or new assets expected.
- DECIDE-AUD-001 / OPSURF-001: PASS. Customer-safe summary excludes raw/support/debug content by default.
## Test Governance Check
- **Test purpose / classification by changed surface**: Feature tests for review summary composition, review-pack ZIP content, customer workspace/review-pack access, redaction, and isolation. Browser smoke only if rendered UI changes.
- **Affected validation lanes**: confidence; browser optional/bounded.
- **Why this lane mix is the narrowest sufficient proof**: Export content can be verified by reading generated ZIP entries; rendered summary and access boundaries can be verified through existing feature tests. A broad suite or new browser family is not justified.
- **Narrowest proving command(s)**:
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/EnvironmentReview/EnvironmentReviewExecutivePackTest.php tests/Feature/EnvironmentReview/EnvironmentReviewCreationTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/EnvironmentReviewDerivedReviewPackTest.php tests/Feature/ReviewPack/ReviewPackResourceTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php` if rendered UI changes
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- `git diff --check`
- **Fixture / helper / factory / seed / context cost risks**: use existing helpers such as `composeEnvironmentReviewForTest()` and review-pack factories; avoid widening defaults.
- **Expensive defaults or shared helper growth introduced?**: no.
- **Heavy-family additions, promotions, or visibility changes**: none.
- **Surface-class relief / special coverage rule**: standard-native-filament plus shared-detail-family review/export checks.
- **Closing validation and reviewer handoff**: verify summary content, redaction, no raw diagnostics, no cross-scope leakage, unchanged destructive action confirmation, and unchanged asset strategy.
- **Budget / baseline / trend follow-up**: none expected.
- **Review-stop questions**: Are customer-safe summaries derived from the correct source? Are hidden records excluded? Is raw diagnostic detail absent? Did tests stay focused?
- **Escalation path**: none.
- **Active feature PR close-out entry**: Guardrail / Smoke Coverage.
- **Why no dedicated follow-up spec is needed**: This is a bounded productization pass over existing review/pack paths; broader localization, packaging cadence, and artifact lifecycle remain separate candidates.
## Project Structure
### Documentation (this feature)
```text
specs/308-decision-register-summary-review-pack/
|-- spec.md
|-- plan.md
|-- tasks.md
`-- checklists/
`-- requirements.md
```
### Source Code (likely affected later)
```text
apps/platform/app/Services/EnvironmentReviews/
|-- EnvironmentReviewComposer.php
`-- EnvironmentReviewSectionFactory.php
apps/platform/app/Jobs/
`-- GenerateReviewPackJob.php
apps/platform/app/Services/
`-- ReviewPackService.php
apps/platform/app/Filament/Pages/Reviews/
`-- CustomerReviewWorkspace.php
apps/platform/app/Filament/Resources/
|-- EnvironmentReviewResource.php
`-- ReviewPackResource.php
apps/platform/tests/Feature/EnvironmentReview/
apps/platform/tests/Feature/ReviewPack/
apps/platform/tests/Feature/Reviews/
apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php
```
**Structure Decision**: Single Laravel/Filament application under `apps/platform`. Later implementation should modify only existing review, review-pack, and focused test surfaces unless repo inspection proves a smaller private helper is necessary.
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|---|---|---|
| None planned | N/A | N/A |
## Technical Approach
1. Inspect current `EnvironmentReviewComposer::governancePackageSummary()` and `GenerateReviewPackJob::buildExecutiveEntrypoint()` behavior.
2. Add or refine a customer-safe decision-summary shape under existing `EnvironmentReview.summary['governance_package']`.
3. Keep existing `governance_decisions` entries aligned with accepted-risk / exception decision truth and add explicit count/empty/unavailable semantics if missing.
4. Ensure `GenerateReviewPackJob` includes the same summary in `summary.json` and readable markdown in `executive-summary.md`.
5. Update existing customer workspace/review detail presentation only if current surfaces do not expose the summary clearly.
6. Preserve redaction through existing `redactReportPayload()` behavior and add assertions for `include_pii=false`.
7. Add focused tests for positive, none, incomplete, cross-scope, and redaction scenarios.
## Data / Model Implications
- No migration expected.
- No new model expected.
- No new enum/status family expected.
- Existing JSONB summary fields may receive additive keys:
- `governance_package.decision_summary`
- or a clarified `governance_package.governance_decisions` shape
- Existing ZIP entries remain preferred:
- `summary.json`
- `executive-summary.md`
- `sections.json`
If implementation finds a new table, new artifact file family, or new public contract is necessary, stop and update the spec/plan before coding further.
## UI / Filament Implications
- Filament v5.2.1 and Livewire v4.1.4 remain the target.
- No panel provider changes; Laravel 12 provider registration remains in `bootstrap/providers.php`.
- No new globally searchable resource.
- No new destructive actions.
- Existing Review Pack destructive-like actions such as expire/regenerate must keep `->requiresConfirmation()` and existing authorization.
- No new assets; deployment `filament:assets` unchanged.
- If UI changes are needed, prefer existing Filament sections/infolists/table entries and shared badge primitives.
## Livewire Implications
- Existing Filament/Livewire pages may render updated summary content.
- No new Livewire component is expected.
- Avoid server-driven reactivity changes unless current page state already requires it.
## RBAC / Policy Implications
- Reuse current review and review-pack policies/capabilities.
- No new capability constant expected.
- Customer-safe content must derive only after workspace and managed-environment scope are established.
- Non-member/not-entitled remains `404`; member missing capability remains existing `403`.
- Tests must cover at least one hidden/cross-scope omission path.
## Audit / Logging / Evidence Implications
- No new audit action ID expected.
- Existing review pack request/export/download audit and telemetry remain.
- No secrets, raw JSON, fingerprints, internal reason ownership, or OperationRun URLs in customer-safe default content.
- Existing `ReviewPack`, `EnvironmentReview`, `EvidenceSnapshot`, and `OperationRun` links remain traceability for operators.
## Rollout Considerations
- No migration.
- No new env vars.
- Queue workers already process review-pack jobs; no new worker class expected beyond current job changes.
- Staging validation should generate at least one review-derived pack with decisions requiring awareness and one with no decisions.
- Production/Dokploy deploy impact is ordinary app code plus queue restart if job code changes.
## Risk Controls
- Keep customer-safe summary derived and additive.
- Keep operator Decision Register unchanged unless a test proves a small source alignment issue.
- Reject raw OperationRun/proof URL exposure in customer-safe summary.
- Preserve non-certification disclosure in `executive-summary.md`.
- Keep tests focused on current review, pack, and customer workspace families.
## Implementation Phases
### Phase 1 - Discovery and Failing Tests
Confirm current summary/export behavior, then add focused tests for missing customer-safe decision summary, export inclusion, redaction, and cross-scope omission.
### Phase 2 - Summary Composition
Extend existing Environment Review governance-package summary with bounded customer-safe decision summary content.
### Phase 3 - Review Pack Inclusion
Ensure review-derived Review Pack `summary.json` and `executive-summary.md` include matching customer-safe decision summary content.
### Phase 4 - Customer Surface Presentation
Expose the summary on existing customer-safe review surfaces only where needed and only through native/shared Filament patterns.
### Phase 5 - Validation and Close-Out
Run focused Pest commands, optional bounded browser smoke, Pint dirty, and `git diff --check`. Record no new assets, no migrations, and no application implementation outside scope.
## Spec Readiness Gate
- `spec.md`, `plan.md`, `tasks.md`, and `checklists/requirements.md` exist.
- Scope is bounded to existing Decision Register context, Environment Review summary, Customer Review Workspace/review detail, and review-derived Review Pack export.
- No open question blocks implementation.
- No new persisted entity, new status family, new public framework, new OperationRun type, or new asset bundle is planned.
- RBAC, isolation, auditability, OperationRun semantics, evidence/result truth, customer-safe disclosure, and test governance are addressed.
## Implementation Close-Out
- **Implementation status**: Completed for Spec 308.
- **Changed application files**:
- `apps/platform/app/Services/EnvironmentReviews/EnvironmentReviewComposer.php`
- `apps/platform/app/Services/Evidence/Sources/FindingsSummarySource.php`
- `apps/platform/app/Jobs/GenerateReviewPackJob.php`
- `apps/platform/resources/views/filament/infolists/entries/environment-review-summary.blade.php`
- `apps/platform/lang/en/localization.php`
- `apps/platform/lang/de/localization.php`
- **Changed tests**:
- `apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewExecutivePackTest.php`
- `apps/platform/tests/Feature/ReviewPack/EnvironmentReviewDerivedReviewPackTest.php`
- **No-migration status**: No database migration, new model, new enum/status family, new persisted decision source, or new Review Pack status was introduced.
- **No-asset status**: No frontend asset registration was added; existing deployment `filament:assets` expectations are unchanged.
- **OperationRun / audit status**: No new OperationRun type, run-link surface, or audit action ID was added. Existing review-pack generation/download traceability remains unchanged.
- **Validation results**:
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/EnvironmentReview/EnvironmentReviewExecutivePackTest.php tests/Feature/EnvironmentReview/EnvironmentReviewCreationTest.php` - passed, 5 tests / 68 assertions.
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/EnvironmentReviewDerivedReviewPackTest.php tests/Feature/ReviewPack/ReviewPackResourceTest.php` - passed, 22 tests / 142 assertions.
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php` - passed, 12 tests / 77 assertions.
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php` - passed, 1 test / 46 assertions.
- Additional regression lane: `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/EnvironmentReview/EnvironmentReviewComposerTest.php tests/Feature/Evidence/ExceptionValidityEvidenceIntegrationTest.php tests/Feature/ReviewPack/ReviewPackValidRiskAcceptanceTest.php` - passed, 6 tests / 27 assertions.
- Additional RBAC/download lane: `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/ReviewPackDownloadTest.php tests/Feature/ReviewPack/ReviewPackRbacTest.php` - passed, 20 tests / 49 assertions.
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` - passed.
- `git diff --check` - passed.
- **Browser smoke result**: Required because rendered customer review detail UI changed. Existing bounded customer workspace browser smoke passed.
- **Post-implementation analysis**: No confirmed in-scope findings remain after the first implementation/fix iteration.
- **Remaining gaps**: None inside Spec 308 scope. Localization adoption, artifact lifecycle/retention, governance-service packaging cadence, and AI-assisted review drafting remain separate follow-up candidates and were not implemented.

View File

@ -0,0 +1,382 @@
# Feature Specification: Decision Register Customer-Safe Summary & Review-Pack Inclusion
**Feature Branch**: `308-decision-register-summary-review-pack`
**Created**: 2026-05-15
**Status**: Ready for implementation
**Input**: User description: "308 - Decision Register Customer-Safe Summary & Review-Pack Inclusion"
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
- **Problem**: The operator Decision Register is now repo-real through Specs 265, 306, and 307, and Review Pack / Customer Review Workspace delivery is repo-real through Specs 109, 258, 260, and 263. The remaining gap is that customer-safe review consumption does not yet clearly carry the Decision Register accountability story into released-review summaries and review-derived Review Packs.
- **Today's failure**: Operators can inspect decisions and proof internally, and review packs can include accepted-risk and governance-package content, but stakeholder-facing summaries can still miss the current decision-register framing: which governance decisions need customer awareness, why they matter, what evidence basis supports them, and what next action is expected without exposing raw operator diagnostics.
- **User-visible improvement**: A released customer review and its review-derived Review Pack include a calm customer-safe decision summary that explains accepted-risk / exception decision follow-through, uses existing governance-package truth, preserves redaction, and avoids raw OperationRun, payload, fingerprint, or internal reason-family detail.
- **Smallest enterprise-capable version**: Extend the existing Environment Review governance-package summary and review-derived Review Pack export path with one derived customer-safe decision-summary projection. Reuse existing `EnvironmentReview.summary`, existing Review Pack `summary.json`, existing `executive-summary.md`, existing customer workspace/review detail surfaces, and existing Decision Register truth. No new table, no new approval workflow, no new customer portal, no new operation lifecycle, and no new export subsystem.
- **Explicit non-goals**: No new `governance_decisions` table; no new `ReviewPack` status; no generic decision framework; no inline customer approval; no Decision Register redesign; no new Review Pack storage family; no raw evidence payload export; no OperationRun URLs in customer-safe exported content; no PSA/ticket handoff; no AI summary generation; no broad localization pass.
- **Permanent complexity imported**: One bounded customer-safe summary shape inside existing review/governance-package payloads, focused feature tests, and one bounded browser smoke update if UI copy changes. No new persisted entity, enum family, public service framework, route family, queue family, or asset bundle.
- **Why now**: `docs/product/roadmap.md` ranks "Decision Register customer-safe summary / review-pack inclusion" as the first current manual-promotion item. `docs/product/implementation-ledger.md` says Decision Register proof/run links are now repo-backed while customer-safe summary and review-pack inclusion remain separate follow-ups.
- **Why not local**: A one-off label on the Decision Register would not reach customer-facing review consumption or exported Review Packs. A broad review-pack rewrite would be too large. The narrow correct point is the existing review/governance-package composition and review-derived pack export path.
- **Approval class**: Workflow Compression
- **Red flags triggered**: Cross-surface review/export productization and customer-safe disclosure. Defense: the slice derives from existing truth, extends existing artifacts only, and explicitly forbids new persistence, new lifecycle states, and raw diagnostic leakage.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 2 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 12/12**
- **Decision**: approve
## Spec Scope Fields *(mandatory)*
- **Scope**: canonical-view
- **Primary Routes**:
- existing `/admin/governance/decisions` Decision Register page as operator context only
- existing `EnvironmentReviewResource` view route for released-review detail
- existing `CustomerReviewWorkspace` route for customer-safe review consumption
- existing `ReviewPackResource` view and signed download route for review-derived pack delivery
- **Data Ownership**:
- `FindingException` and `FindingExceptionDecision` remain the decision source of truth.
- `FindingExceptionEvidenceReference`, `EvidenceSnapshot`, and `StoredReport` remain evidence/report truth.
- `EnvironmentReview.summary` remains released-review summary truth.
- `ReviewPack.summary` and the generated ZIP files remain review-pack artifact truth.
- `OperationRun` remains execution truth and is not copied into customer-safe decision content beyond safe count/source statements already present in review summaries.
- **RBAC**:
- Workspace membership remains the first boundary for customer review and review-pack surfaces.
- Environment/managed-environment entitlement remains required before any released review or Review Pack is visible.
- Existing review and review-pack capabilities remain authoritative: no new capability is introduced.
- Non-members or out-of-scope workspace/environment actors get deny-as-not-found (`404`).
- Members missing review or review-pack capability get existing `403` capability denial where policies already define it.
For canonical-view specs:
- **Default filter behavior when tenant-context is active**: Customer Review Workspace and released-review detail preserve existing managed-environment prefilter behavior. This feature does not change Decision Register default filters.
- **Explicit entitlement checks preventing cross-tenant leakage**: Decision summary entries are derived only from the same workspace and managed environment as the released review / review pack. Cross-workspace and cross-environment decision, proof, and operation records must not affect counts, summaries, links, or unavailable-state copy.
## Cross-Cutting / Shared Pattern Reuse *(mandatory)*
- **Cross-cutting feature?**: yes.
- **Interaction class(es)**: customer-safe review summaries, governance package summaries, review-pack export content, evidence/report disclosure, action links, and status messaging.
- **Systems touched**: `EnvironmentReviewComposer`, `EnvironmentReviewSectionFactory`, `CustomerReviewWorkspace`, `EnvironmentReviewResource`, `ReviewPackService`, `GenerateReviewPackJob`, `ReviewPackResource`, existing Review Pack download route, existing Decision Register builder/page as source context only, and focused review/review-pack tests.
- **Existing pattern(s) to extend**: existing `governance_package` payload in `EnvironmentReview.summary`, existing review-derived Review Pack contract `auditor_ready_executive_export.v1`, existing `executive-summary.md`, existing customer workspace package availability states, existing BADGE-001-backed status display.
- **Shared contract / presenter / builder / renderer to reuse**: Reuse existing review composition and review-pack export paths. Reuse `BadgeRenderer` / `BadgeCatalog` for status-like UI if UI display changes. Reuse existing resource URL helpers for review/pack navigation. Do not create a new decision-summary presenter framework unless implementation proves the current composer cannot safely host the projection.
- **Why the existing shared path is sufficient or insufficient**: Existing review/package paths already carry executive summary, accepted risks, governance decisions, evidence basis, and package metadata. They are sufficient for v1 if they get an explicit customer-safe Decision Register summary shape and tests. The operator Decision Register builder is not sufficient by itself because it intentionally exposes operator-only proof and OperationRun affordances.
- **Allowed deviation and why**: A small private helper inside the existing review composer or review-pack generation job is allowed to keep customer-safe copy bounded. A public reusable framework is not allowed in v1.
- **Consistency impact**: `Decision register`, `Governance decisions requiring awareness`, `Accepted risks`, `Evidence basis`, `Next action`, and `Review pack` wording must stay aligned across review detail, customer workspace, and review-pack export.
- **Review focus**: Block raw payload/fingerprint/internal reason export, fake customer links, duplicate lifecycle truth, second approval surfaces, and any new persistence or workflow engine.
## OperationRun UX Impact *(mandatory)*
- **Touches OperationRun start/completion/link UX?**: yes, indirectly and read-only. Existing review-derived Review Pack generation already creates an `OperationRun`; this feature must not create a new run type or local start UX. Customer-safe summaries must not expose OperationRun URLs or raw run diagnostics by default.
- **Shared OperationRun UX contract/layer reused**: Existing `ReviewPackService`, `GenerateReviewPackJob`, `OperationRunService`, and existing `OperationRunLinks` usage remain the only run paths.
- **Delegated start/completion UX behaviors**: Existing review-pack generation queue/toast/run behavior remains delegated to current Review Pack and OperationRun infrastructure. No new queued toast, browser event, DB notification, dedupe messaging, or terminal notification is introduced.
- **Local surface-owned behavior that remains**: Customer-safe summary may state that evidence was generated or that a review pack is available, but must not render raw run status, run identifiers, or operation links in customer/read-only default paths.
- **Queued DB-notification policy**: N/A - no new notification policy.
- **Terminal notification path**: Existing review-pack terminal notification behavior remains unchanged.
- **Exception required?**: none.
## Provider Boundary / Platform Core Check *(mandatory)*
- **Shared provider/platform boundary touched?**: no new provider seam.
- **Boundary classification**: platform-core review/export summary over existing governance truth.
- **Seams affected**: Existing review and review-pack summary payloads; no Graph contract, provider connection, or provider dispatch seam changes.
- **Neutral platform terms preserved or introduced**: decision, governance decision, accepted risk, evidence basis, review, review pack, managed environment, workspace.
- **Provider-specific semantics retained and why**: Existing evidence and finding content may originate from Microsoft/Intune data because those records already own that truth. The customer-safe summary must not add Microsoft-specific platform-core language unless existing evidence titles already contain it.
- **Why this does not deepen provider coupling accidentally**: The feature derives from TenantPilot governance decision and review artifacts rather than Graph payloads or provider contracts.
- **Follow-up path**: none.
## UI / Surface Guardrail Impact *(mandatory)*
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|---|---|---|---|---|---|---|
| Customer Review Workspace governance-package summary | yes | Existing Filament page / shared view primitives | customer-safe review consumption, package availability | page, table/detail context | no | Add bounded customer-safe decision summary only if current surface has a package summary location |
| Environment Review detail governance-package disclosure | yes | Native Filament resource/infolist | released-review detail, package export action | detail | no | Existing view remains the action owner; no new mutation action |
| Review Pack detail/export | yes | Native Filament resource plus existing ZIP export | review-pack artifact truth and signed download | detail, artifact content | no | Export content changes; no new asset or route |
| Decision Register page | no direct UI change expected | Existing native Filament page | operator-only context | none | no | Source context only; do not redesign register |
Implementation intent: use existing Filament-native sections/infolists/table display and existing export files. Do not add local CSS, ad-hoc cards, custom badges, hover affordances, or new assets. Filament remains v5 with Livewire v4.1.4+.
## Decision-First Surface Role *(mandatory)*
| Surface | Decision Role | Human-in-the-loop Moment | Immediately Visible for First Decision | On-Demand Detail / Evidence | Why This Is Primary or Why Not | Workflow Alignment | Attention-load Reduction |
|---|---|---|---|---|---|---|---|
| Customer Review Workspace | Primary customer-safe consumption surface | Customer/stakeholder understands which governance decisions require awareness | customer-safe decision count, short summary, evidence basis, next action wording | released-review detail and downloadable review pack | Primary for customer consumption because it avoids sending stakeholders to operator register pages | Follows released-review delivery, not internal queue work | Reduces operator translation before sharing review status |
| Environment Review detail | Secondary context | Operator/customer-safe reviewer inspects one released review before export | decision-summary section, package availability, export action | sections, evidence basis, audit context where allowed | Secondary because it deepens the chosen review and starts export | Keeps review lifecycle ownership on existing detail page | Prevents rebuilding summary from Decision Register rows |
| Review Pack export | Tertiary evidence / delivery artifact | Stakeholder consumes the exported summary offline | executive decision-awareness section and structured summary JSON | appendix files in ZIP | Delivery artifact, not an interactive decision surface | Carries the released-review truth into the customer artifact | Avoids separate manual deck or email summary |
| Decision Register page | Operator primary decision surface, unchanged | Operator decides internal follow-up | existing register row proof/link state | existing detail/proof/operation links | Not customer-facing in this slice | Remains internal action and proof surface | Avoids duplicating operator workflow in customer surfaces |
## Audience-Aware Disclosure *(mandatory)*
| Surface | Audience Modes In Scope | Decision-First Default-Visible Content | Operator Diagnostics | Support / Raw Evidence | One Dominant Next Action | Hidden / Gated By Default | Duplicate-Truth Prevention |
|---|---|---|---|---|---|---|---|
| Customer Review Workspace | customer-read-only, operator-MSP | decision summary, evidence-basis wording, readiness/follow-up state | review detail and existing diagnostics remain secondary | raw payloads, fingerprints, platform reason families, run identifiers hidden | `Open review` or `Download review pack` depending current context | raw evidence and operation details | Summary states customer impact once; detail/export add evidence rather than restating internal state |
| Environment Review detail | customer-read-only, operator-MSP | released-review decision summary and package availability | review sections, evidence/source context | low-level evidence and run detail stays secondary/capability-gated | `Download governance package` when ready | raw JSON/fingerprints/internal reason ownership | Review summary remains the source for exported pack |
| Review Pack export | customer-read-only | executive story, accepted risks, governance decisions requiring awareness, next actions, non-certification disclosure | structured appendix only | raw payloads, fingerprints, internal reason families, OperationRun URLs absent | read executive entrypoint first | raw/support diagnostics absent from default entrypoint | `summary.json` carries structured truth; `executive-summary.md` carries readable summary |
## UI/UX Surface Classification *(mandatory)*
| Surface | Action Surface Class | Surface Type | Likely Next Operator Action | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type / Justification |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Customer Review Workspace | List / Review Workspace | Customer-safe released-review list/detail handoff | Open review or download current pack | Existing row/detail handoff | current behavior preserved | package download remains contextual | none | existing customer workspace route | existing EnvironmentReview view route | workspace and managed-environment scope | Customer reviews | whether governance package and decision summary are available | none |
| Environment Review detail | Detail / Reviewable governance artifact | Released-review detail | Download governance package | Existing detail page | N/A | export action remains detail-owned | none | existing EnvironmentReview list/register | existing EnvironmentReview view | managed environment and review status | Environment review | decision summary, evidence basis, export readiness | none |
| Review Pack detail/export | Detail / Artifact delivery | Review-pack artifact | Download pack | Existing ReviewPack view/download | N/A | regenerate remains operator-only outside customer flow | existing expire/regenerate confirmation unchanged | existing ReviewPack list | existing ReviewPack view | managed environment, review link, status | Review pack | generated artifact truth and summary | none |
## Operator Surface Contract *(mandatory)*
| Surface | Primary Persona | Decision / Operator Action Supported | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|---|---|---|---|---|---|---|---|---|---|---|
| Customer Review Workspace | Customer-safe reader / MSP operator | Understand released review and choose whether to open or download | Customer-safe review workspace | What does the latest released review mean for governance follow-up? | executive summary, decision-awareness count/summary, evidence-basis copy, package availability | raw evidence and run detail remain elsewhere | review status, completeness, package availability, decision-awareness state | none | Open review / Download review pack | none |
| Environment Review detail | MSP operator / customer-safe reviewer | Validate released-review summary and export package | Detail page | Is this review ready to share and what decisions require awareness? | decision summary, package availability, evidence basis, next action | sections, evidence source detail, audit context | review status, completeness, governance-package availability | TenantPilot artifact export only | Download governance package | none in this slice |
| Review Pack detail/export | MSP operator / customer-safe reader | Consume or deliver review-pack artifact | Artifact detail/export | What is included in this package and is it safe to share? | status, customer-safe summary, review link, evidence snapshot completeness | fingerprints and operation links hidden in customer flow | pack status, evidence completeness, review status | existing review-pack generation only | Download | existing regenerate/expire only, unchanged |
## Proportionality Review *(mandatory when structural complexity is introduced)*
- **New source of truth?**: no. Decision truth remains `FindingException` / `FindingExceptionDecision`; review truth remains `EnvironmentReview.summary`; pack truth remains `ReviewPack.summary` and generated files.
- **New persisted entity/table/artifact?**: no new entity/table/artifact family. Existing review and review-pack artifacts receive derived customer-safe summary content.
- **New abstraction?**: no public framework. A private helper in the existing composer/job is allowed only if it keeps the derived summary readable and bounded.
- **New enum/state/reason family?**: no. Any customer-safe state labels are derived presentation strings from existing governance states.
- **New cross-domain UI framework/taxonomy?**: no.
- **Current operator problem**: Operators need customer-ready decision accountability to travel with released reviews and review packs without manual translation.
- **Existing structure is insufficient because**: Current operator Decision Register and proof links are internal; current review-pack content has governance-package concepts but does not yet explicitly prove the customer-safe Decision Register follow-through requirement.
- **Narrowest correct implementation**: Extend existing governance-package summary and review-derived export content; add focused tests that prove safe inclusion and no raw diagnostic leakage.
- **Ownership cost**: A small payload contract inside existing review summary, focused review/review-pack tests, and one bounded smoke if rendered customer-safe UI changes.
- **Alternative intentionally rejected**: New customer decision portal, new decision persistence, new export file family, and generic decision-summary service were rejected as broader than current-release truth.
- **Release truth**: Current-release productization over existing governance-of-record truth, not future workflow-platform preparation.
### Compatibility posture
This feature assumes a pre-production environment.
Backward compatibility, legacy aliases, migration shims, historical fixtures, and compatibility-specific tests are out of scope unless explicitly required by this spec. Existing review-pack ZIP consumers are internal/pre-production; additive structured summary keys are allowed, but the implementation should avoid breaking existing file names unless a test proves the changed contract.
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
- **Test purpose / classification**: Feature plus one bounded Browser smoke if rendered customer-review UI changes. Unit tests are optional only if implementation extracts non-trivial summary logic.
- **Validation lane(s)**: confidence for focused Pest feature tests; browser only for existing `CustomerReviewWorkspaceSmokeTest` or equivalent bounded smoke.
- **Why this classification and these lanes are sufficient**: The behavior is primarily review composition, exported ZIP content, customer-safe rendering, RBAC boundaries, and redaction. Existing feature tests can verify payloads and export files without broad suite cost. One existing browser smoke is enough if UI rendering changes.
- **New or expanded test families**: expand existing `tests/Feature/EnvironmentReview/*`, `tests/Feature/ReviewPack/*`, and `tests/Feature/Reviews/*` families. Do not create a new heavy-governance family.
- **Fixture / helper cost impact**: moderate, feature-local. Tests need released reviews, evidence snapshots, risk-accepted findings / finding exceptions, review-derived packs, and customer workspace actors. Helpers must stay opt-in and not make workspace/environment setup heavier by default.
- **Heavy-family visibility / justification**: none beyond one bounded browser smoke when rendered UI changes.
- **Special surface test profile**: shared-detail-family plus standard-native-filament.
- **Standard-native relief or required special coverage**: Existing native Filament resources/pages should need ordinary feature assertions plus one smoke path. Custom export content must be verified by reading generated ZIP entries.
- **Reviewer handoff**: Reviewers must verify the customer-safe summary excludes raw JSON, fingerprints, internal reason ownership, platform reason families, OperationRun URLs, and cross-scope decision records.
- **Budget / baseline / trend impact**: low feature-local increase only.
- **Escalation needed**: none.
- **Active feature PR close-out entry**: Guardrail / Smoke Coverage.
- **Planned validation commands**:
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/EnvironmentReview/EnvironmentReviewExecutivePackTest.php tests/Feature/EnvironmentReview/EnvironmentReviewCreationTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/EnvironmentReviewDerivedReviewPackTest.php tests/Feature/ReviewPack/ReviewPackResourceTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php` if rendered customer workspace or review detail UI changes
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- `git diff --check`
## Summary
Prepare a narrow follow-up that carries Decision Register accountability into customer-safe review consumption and review-derived Review Packs.
The feature should make current governance decisions visible to stakeholders as a bounded summary, not as an operator workflow. It should answer:
- Which accepted-risk / exception decisions require customer awareness?
- Why does the decision matter in the released review?
- What evidence basis supports the statement?
- What next action should the customer or MSP understand?
It must not expose raw proof details, operation links, run diagnostics, internal reason families, or cross-tenant state in customer-safe defaults.
## Scope Boundaries
### In Scope
- customer-safe decision-summary content inside existing released-review governance-package summary
- review-derived Review Pack inclusion through existing `summary.json` and `executive-summary.md`
- customer workspace / review detail display only where existing package-summary surfaces already exist
- redaction behavior for `include_pii=false`
- RBAC and scope isolation for customer review and pack access
- focused tests proving content, omission, redaction, and no raw diagnostic leakage
### Non-Goals
- new persisted decision table or projection store
- new customer approval workflow or customer-side mutation
- new Review Pack status, lifecycle, or retention behavior
- new OperationRun type, notification policy, or run-link surface
- broad Decision Register page redesign
- full customer-facing localization adoption
- AI-generated summaries
- generic governance artifact lifecycle runtime
## Assumptions
- Existing Environment Review summary composition is the correct source for customer-safe released-review content.
- Existing Review Pack generation from an Environment Review is the correct export path for stakeholder packages.
- Existing `governance_package.governance_decisions` content is close to the target and should be extended or made explicit rather than replaced.
- Customer-safe summaries may omit proof links where exposing a direct link would leak operator-only context.
- The current product is pre-production, so additive payload-key changes do not require compatibility shims.
## Risks
- Reusing operator Decision Register row data directly could leak proof URLs, OperationRun links, or internal state.
- Adding a new public summary service too early could violate proportionality and create another presentation layer.
- Review-pack inclusion could drift into a broad export redesign if it adds new files, statuses, or delivery workflows.
- Customer-safe wording could overstate compliance or certification if non-certification disclosure is not preserved.
## Candidate Selection Rationale
- **Selected candidate**: `308 - Decision Register Customer-Safe Summary & Review-Pack Inclusion`.
- **Source locations**:
- explicit user request
- `docs/product/roadmap.md` priority ranking item 1
- `docs/product/spec-candidates.md` manual-promotion backlog for `decision-register-review-pack-inclusion` / `decision-register-customer-safe-summary`
- `docs/product/implementation-ledger.md` open gap for Decision Register customer-safe/review-pack inclusion
- **Why selected**: The repo already proves the operator register and proof/run link polish. The highest-priority remaining productization gap is customer-safe consumption and review-pack inclusion, not another operator register pass.
- **Why this is the smallest viable implementation slice**: It reuses current review/governance-package and review-pack export infrastructure, adds only derived customer-safe content, and leaves all lifecycle actions and proof inspection on existing operator surfaces.
- **Intentional narrowing from source candidate**: This spec combines customer-safe summary and review-pack inclusion only for existing accepted-risk / exception decision truth. It does not add customer approvals, multi-family decision workflows, broad localization, artifact lifecycle runtime, or new packaging cadence.
## Completed-Spec Guardrail Result
Related existing specs are context only and must not be rewritten:
- `specs/265-decision-register-approval/` defines the operator register and is not a refresh target.
- `specs/306-decision-register-reconciliation/` carries reconciliation evidence and validation summaries.
- `specs/307-decision-register-evidence-operationrun-link-polish/` has completed task markers and browser-smoke close-out signals.
- `specs/109-review-pack-export/`, `specs/258-customer-review-productization/`, and `specs/260-governance-service-packaging/` carry completed or validation/checklist signals and provide only implementation context.
## Follow-up Candidates
- Customer-Facing Localization Adoption v1 for translated customer-safe labels and glossary discipline.
- Governance Artifact Lifecycle & Retention v1 for broader artifact hold/export/delete semantics.
- Governance Service Packaging v1 for repeatable MSP package cadence and stakeholder mapping.
- First governed AI runtime consumer for later AI-assisted review drafting only after governed runtime requirements are promoted.
## User Scenarios & Testing *(mandatory)*
### User Story 1 - See a customer-safe decision summary in released review consumption (Priority: P1)
As an MSP operator or customer-safe reader, I want the released review to summarize governance decisions requiring awareness so stakeholders understand accepted-risk follow-up without opening the internal Decision Register.
**Why this priority**: This is the visible productization gap. Without it, operators still manually translate internal decision-register truth into customer language.
**Independent Test**: Create a released Environment Review with accepted-risk / exception decision data, open customer-safe review surfaces, and verify the decision summary appears with customer-safe wording and no raw diagnostic detail.
**Acceptance Scenarios**:
1. **Given** a released review includes accepted-risk entries that require governance follow-up, **When** the customer-safe review surface renders, **Then** it shows a concise decision-awareness summary with count, impact, and next-action wording.
2. **Given** a released review has no governance decisions requiring customer awareness, **When** the surface renders, **Then** it shows a calm empty/none state and does not imply hidden risk.
3. **Given** a decision has proof or operation context internally, **When** the customer-safe summary renders, **Then** it does not expose raw proof URLs, OperationRun URLs, fingerprints, internal reason ownership, or platform reason families by default.
### User Story 2 - Include the decision summary in review-derived Review Packs (Priority: P1)
As an MSP operator, I want generated review packs to include the customer-safe Decision Register summary so the exported package can be shared without a separate manual explanation.
**Why this priority**: Review Pack inclusion is the explicit roadmap follow-through and makes the feature useful outside the interactive UI.
**Independent Test**: Generate a review-derived Review Pack from a released review with governance decisions, inspect `summary.json` and `executive-summary.md`, and verify structured and readable decision-summary content is present and redacted correctly.
**Acceptance Scenarios**:
1. **Given** a review-derived pack is generated with decisions requiring awareness, **When** the ZIP is inspected, **Then** `summary.json` includes the structured customer-safe decision summary.
2. **Given** the same pack is inspected, **When** `executive-summary.md` is read, **Then** it includes a readable `Governance decisions requiring awareness` section that matches the structured summary.
3. **Given** `include_pii=false`, **When** the pack is generated, **Then** tenant names, actor names, owner labels, and other PII-bearing summary fields are redacted or omitted according to existing redaction behavior.
### User Story 3 - Preserve security, scope, and lifecycle boundaries (Priority: P1)
As a platform owner, I need customer-safe decision summaries to preserve workspace/environment isolation, RBAC, auditability, and lifecycle ownership so a trust feature does not become a leak or a second workflow.
**Why this priority**: Customer-facing governance summaries are high-trust artifacts; leakage or duplicate lifecycle controls would be worse than omission.
**Independent Test**: Seed same-workspace and cross-workspace/environment decision data, generate reviews and packs, and verify only in-scope decision content appears while lifecycle actions remain on existing operator surfaces.
**Acceptance Scenarios**:
1. **Given** hidden environment decisions exist, **When** a visible environment review or pack is generated, **Then** hidden decisions do not affect counts, copy, or exported content.
2. **Given** a user lacks review-pack view capability, **When** they try to access a pack or download URL, **Then** existing `404`/`403` behavior remains unchanged.
3. **Given** the customer-safe summary renders, **When** actions are inspected, **Then** approve, reject, renew, revoke, and closure actions are not introduced on customer-safe surfaces.
## Edge Cases
- A review has accepted-risk entries but no current exception decision.
- A decision was revoked or expired after the review evidence snapshot was captured.
- A review pack is generated from a partial or stale evidence basis.
- Multiple decisions reference the same finding or accepted risk.
- Decision proof exists internally but is not customer-safe to link.
- `include_pii=false` is set and owner labels or tenant names would otherwise appear.
- A review is published but no ready review pack exists yet.
- A generated pack is expired, blocked by entitlement/commercial lifecycle, or unavailable.
- Cross-workspace/environment decisions exist with similar titles or finding IDs.
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: Released-review governance-package summaries MUST expose a customer-safe decision summary when in-scope accepted-risk / exception decisions require awareness.
- **FR-002**: The decision summary MUST include a total count, readable summary text, customer-safe entry titles, governance state or awareness reason, and one next-action statement where current review truth supports it.
- **FR-003**: Decision summary entries MUST derive from existing review/evidence/decision truth and MUST NOT create or persist a second decision source of truth.
- **FR-004**: Customer-safe summaries MUST distinguish "no decisions requiring awareness" from "decision data unavailable or incomplete" where current evidence completeness supports that distinction.
- **FR-005**: Review-derived Review Packs MUST include the decision summary in `summary.json`.
- **FR-006**: Review-derived Review Packs MUST include readable decision-awareness content in `executive-summary.md`.
- **FR-007**: Review Pack export MUST preserve the existing `auditor_ready_executive_export.v1` contract unless the implementation records a bounded additive version note in the existing delivery metadata.
- **FR-008**: Customer-safe default surfaces and exported executive summaries MUST NOT expose raw JSON, source fingerprints, internal reason ownership, platform reason families, raw OperationRun IDs, OperationRun URLs, debug payloads, or provider payload dumps.
- **FR-009**: `include_pii=false` MUST redact or omit PII-bearing decision summary fields using existing Review Pack redaction behavior.
- **FR-010**: Customer-safe summary generation MUST be scoped to the released review's workspace and managed environment before counts and copy are derived.
- **FR-011**: The feature MUST NOT introduce customer-facing approval, rejection, renewal, revocation, closure, or escalation actions.
- **FR-012**: Existing Review Pack generation, download, expire, regenerate, entitlement, and commercial-lifecycle behavior MUST remain unchanged except for included summary content.
### Non-Functional Requirements
- **NFR-001**: The implementation MUST reuse existing review and review-pack composition paths instead of adding a new export subsystem.
- **NFR-002**: The implementation MUST remain deterministic for the same released review and options.
- **NFR-003**: Generated summary content MUST be concise enough for first-read review consumption and must not turn exported markdown into a raw appendix.
- **NFR-004**: No new frontend assets are registered. Deployment `filament:assets` requirements remain unchanged.
- **NFR-005**: No database migration is expected. If implementation discovers a migration is necessary, stop and update the spec before continuing.
### UX Requirements
- **UX-001**: Customer-safe decision content MUST be default-visible only at a summary level; diagnostics and raw evidence remain secondary or absent in customer paths.
- **UX-002**: The customer-safe summary MUST preserve one dominant next action per surface (`Open review` or `Download review pack`) and must not make operator-only links visually equal.
- **UX-003**: Any status-like display MUST use existing BADGE-001 badge rendering or plain supporting text; no ad-hoc color mapping or custom status UI.
- **UX-004**: Empty states MUST be truthful: no decisions, unavailable evidence, blocked pack, and expired pack are separate meanings.
### RBAC / Security Requirements
- **SEC-001**: Existing workspace membership, managed-environment entitlement, and review/review-pack capabilities remain server-side authorization boundaries.
- **SEC-002**: Non-member or not-entitled workspace/environment access remains `404`.
- **SEC-003**: Member-but-missing-capability behavior remains existing `403` where current policies define it.
- **SEC-004**: Customer-safe summaries MUST NOT leak hidden-environment existence through counts, empty-state wording, links, or exported content.
### Auditability / Observability Requirements
- **AUD-001**: Existing review-open, review-pack generation, download, and export audit/telemetry semantics remain unchanged.
- **AUD-002**: No new audit action ID is required unless implementation introduces a new user-triggered mutation, which is out of scope.
- **AUD-003**: Review Pack generated artifacts remain traceable through existing `ReviewPack`, `EnvironmentReview`, `EvidenceSnapshot`, and `OperationRun` links.
### Data / Truth-Source Requirements
- **DATA-001**: Decision truth remains `FindingException` and `FindingExceptionDecision`.
- **DATA-002**: Released-review summary truth remains `EnvironmentReview.summary`.
- **DATA-003**: Review-pack artifact truth remains `ReviewPack.summary` and the generated ZIP file.
- **DATA-004**: The customer-safe summary is a derived snapshot at review composition / pack generation time, not a live Decision Register view.
## Acceptance Criteria
- **AC-001**: A released review with governance decisions requiring awareness exposes customer-safe summary content on the in-scope review/customer surface.
- **AC-002**: A released review with no decisions requiring awareness exposes a calm no-decision state.
- **AC-003**: A review-derived Review Pack ZIP includes matching structured and readable customer-safe decision summary content.
- **AC-004**: `include_pii=false` prevents tenant names and actor/owner labels from leaking into exported decision-summary content.
- **AC-005**: Customer-safe summaries contain no raw JSON, fingerprints, internal reason-family labels, or OperationRun URLs.
- **AC-006**: Cross-workspace and cross-environment decision records do not influence summary counts or exported content.
- **AC-007**: Existing review-pack generation/download/regenerate/expire authorization and confirmation behavior is unchanged.
- **AC-008**: Focused Pest feature tests pass, and browser smoke is completed or explicitly documented if rendered UI changes.
## Success Criteria
- **SC-001**: In focused test scenarios, 100% of generated review-derived packs include decision-summary content when source review truth contains decisions requiring awareness.
- **SC-002**: In focused negative scenarios, 100% of customer-safe paths omit raw operation/debug/proof internals.
- **SC-003**: In cross-scope test scenarios, 100% of hidden decisions are excluded from rendered and exported content.
- **SC-004**: The implementation introduces no new persisted entity, status family, global search resource, asset bundle, or operation type.
## Filament v5 Blueprint Contract
- **Livewire v4.0+ compliance**: Required. Current app uses Livewire 4.1.4.
- **Provider registration location**: No panel provider changes expected. Laravel 12 panel providers remain in `bootstrap/providers.php`.
- **Global search**: No new globally searchable resource is introduced. Existing resources keep their current View/Edit/global-search posture.
- **Destructive actions**: No new destructive action is introduced. Existing Review Pack `Expire` / regenerate confirmation behavior remains unchanged and must keep `->requiresConfirmation()` and policy/capability enforcement.
- **Asset strategy**: No new assets. Deployment `cd apps/platform && php artisan filament:assets` remains the existing deploy step only when registered assets change; this spec expects none.
- **Testing plan**: Use Pest 4 focused Feature coverage for review, review-pack, and customer workspace paths; use existing bounded Browser smoke only if rendered customer-review UI changes.
## Open Questions
None blocking. The implementation may choose whether to extend the existing `governance_package.governance_decisions` shape directly or add a nested `decision_summary` key, but it must preserve the requirements above and avoid new persistence or a new framework.

View File

@ -0,0 +1,110 @@
# Tasks: Decision Register Customer-Safe Summary & Review-Pack Inclusion
**Input**: Design documents from `/specs/308-decision-register-summary-review-pack/`
**Prerequisites**: `spec.md`, `plan.md`, `checklists/requirements.md`
**Tests**: Required. Use Pest 4 focused Feature coverage and the existing bounded Browser smoke only if rendered customer-review UI changes.
## Test Governance Checklist
- [x] Lane assignment is named and is the narrowest sufficient proof for the changed behavior.
- [x] New or changed tests stay in the smallest honest family, and any browser addition is limited to the existing bounded customer-review smoke.
- [x] Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default.
- [x] Planned validation commands cover the change without pulling unrelated suite cost.
- [x] The declared `shared-detail-family` / `standard-native-filament` surface profile is explicit.
- [x] Any material budget, baseline, trend, or escalation note is recorded in the active feature close-out.
## Phase 1: Preparation and Current-Truth Verification
- [x] T001 Review `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/308-decision-register-summary-review-pack/spec.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/308-decision-register-summary-review-pack/plan.md`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/308-decision-register-summary-review-pack/checklists/requirements.md`.
- [x] T002 Inspect current `governance_package` summary generation in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/EnvironmentReviews/EnvironmentReviewComposer.php`.
- [x] T003 Inspect current review-derived Review Pack generation in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Jobs/GenerateReviewPackJob.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/ReviewPackService.php`.
- [x] T004 Inspect current customer-safe review/package presentation in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/EnvironmentReviewResource.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/ReviewPackResource.php`.
- [x] T005 Confirm no migration, new model, new capability, new OperationRun type, new global-search resource, or new asset registration is needed; stop and update the spec if this is false.
## Phase 2: Tests First - Customer-Safe Review Summary (US1)
- [x] T006 [US1] Add or extend a focused feature test in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewCreationTest.php` or `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewExecutivePackTest.php` proving a released review summary includes customer-safe decision-summary content when governance decisions require awareness.
- [x] T007 [US1] Add a no-decisions scenario proving the summary renders a calm none state rather than implying hidden risk.
- [x] T008 [US1] Add an incomplete/unavailable evidence scenario proving the summary distinguishes unavailable decision data from no decisions when current evidence completeness supports the distinction.
- [x] T009 [US1] Add assertions that customer-safe review summary content does not include raw JSON, fingerprints, internal reason ownership, platform reason families, raw OperationRun IDs, or OperationRun URLs.
## Phase 3: Tests First - Review Pack Inclusion (US2)
- [x] T010 [US2] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/ReviewPack/EnvironmentReviewDerivedReviewPackTest.php` to generate a review-derived Review Pack with governance decisions and assert `summary.json` includes structured customer-safe decision-summary content.
- [x] T011 [US2] Extend the same test to assert `executive-summary.md` includes readable `Governance decisions requiring awareness` content matching the structured summary.
- [x] T012 [US2] Add a redaction assertion for `include_pii=false` proving tenant names, actor/owner labels, and other PII-bearing decision fields are redacted or omitted.
- [x] T013 [US2] Add an assertion that existing review-derived pack metadata still carries `auditor_ready_executive_export.v1`, `metadata.json`, `summary.json`, `sections.json`, and `executive-summary.md` without introducing a new top-level export family.
## Phase 4: Tests First - Scope, RBAC, and Lifecycle Boundaries (US3)
- [x] T014 [US3] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php` or adjacent review workspace tests to prove hidden workspace/environment decisions do not affect visible customer-safe summary counts or copy.
- [x] T015 [US3] Add or extend review-pack access assertions proving existing non-member `404` and missing-capability `403` behavior remains unchanged for view/download paths.
- [x] T016 [US3] Add assertions that customer-safe surfaces do not introduce approve, reject, renew, revoke, closure, or escalation actions.
## Phase 5: Implement Customer-Safe Summary Derivation
- [x] T017 [US1] Extend existing summary composition in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/EnvironmentReviews/EnvironmentReviewComposer.php` with a bounded customer-safe decision summary derived from current review/evidence/decision truth.
- [x] T018 [US1] Keep the summary additive under existing `governance_package` payloads, preferring `decision_summary` and/or clarified `governance_decisions` keys over new persistence or public framework classes.
- [x] T019 [US1] Ensure summary entries include customer-safe title, awareness reason/governance state, summary, count, and next-action wording where available.
- [x] T020 [US1] Ensure no customer-safe summary field copies raw proof URLs, raw OperationRun URLs, source fingerprints, platform reason families, or provider payload dumps.
- [x] T021 [US1] Preserve existing non-certification / interpretation disclosure when decision-summary content is present.
## Phase 6: Implement Review Pack Inclusion
- [x] T022 [US2] Update `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Jobs/GenerateReviewPackJob.php` so review-derived `summary.json` includes the customer-safe decision summary from the released review.
- [x] T023 [US2] Update `executive-summary.md` generation in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Jobs/GenerateReviewPackJob.php` to render a concise decision-awareness section from the same structured summary.
- [x] T024 [US2] Ensure `include_pii=false` flows through the existing redaction path for any tenant, actor, owner, or customer-identifying decision summary field.
- [x] T025 [US2] Preserve existing ZIP file names and delivery metadata unless a bounded additive metadata note is required and tested.
## Phase 7: Implement Customer Surface Presentation If Needed
- [x] T026 [US1] If current customer workspace/review detail surfaces do not expose the summary clearly, add a native/shared Filament presentation in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` or `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/EnvironmentReviewResource.php`.
- [x] T027 [US1] Keep exactly one dominant next action on each changed surface (`Open review`, `Download governance package`, or existing `Download`), with decision summary as supporting content.
- [x] T028 [US1] Use existing Filament sections/infolists/table text and BADGE-001-backed badges where status-like display is needed; do not add ad-hoc CSS, custom cards, local color mappings, or new assets.
- [x] T029 [US3] Confirm `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Governance/DecisionRegister.php` remains an operator surface and is not redesigned for this feature.
## Phase 8: Filament, RBAC, and Asset Contract Review
- [x] T030 Confirm Filament v5 / Livewire v4 compliance for any changed Filament page/resource and do not introduce Livewire v3 references.
- [x] T031 Confirm panel provider registration remains unchanged in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/bootstrap/providers.php`.
- [x] T032 Confirm no globally searchable resource is added or changed; if a resource is unexpectedly changed, verify View/Edit/global-search posture per Filament v5 rules.
- [x] T033 Confirm no new destructive action is introduced and existing Review Pack destructive-like actions still use `->requiresConfirmation()` plus existing authorization.
- [x] T034 Confirm no frontend assets are registered; deployment `filament:assets` requirements are unchanged.
## Phase 9: Validation
- [x] T035 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/EnvironmentReview/EnvironmentReviewExecutivePackTest.php tests/Feature/EnvironmentReview/EnvironmentReviewCreationTest.php`.
- [x] T036 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/EnvironmentReviewDerivedReviewPackTest.php tests/Feature/ReviewPack/ReviewPackResourceTest.php`.
- [x] T037 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php`.
- [x] T038 If rendered customer workspace or review detail UI changed, run `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php`.
- [x] T039 Run `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`.
- [x] T040 Run `git diff --check`.
## Phase 10: Close-Out
- [x] T041 Record implementation close-out notes in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/308-decision-register-summary-review-pack/plan.md`, including changed files, no-migration status, no-asset status, test results, browser smoke result or reason not run, and remaining gaps.
- [x] T042 Confirm customer-safe summary and Review Pack inclusion remain in scope and follow-up candidates are not implemented here.
- [x] T043 Confirm no application implementation happened outside the selected review/review-pack/customer-safe summary surfaces and focused tests.
## Dependency Order
1. T001-T005 before implementation.
2. T006-T016 before or alongside implementation changes.
3. T017-T025 before UI presentation work.
4. T026-T029 only if rendered UI needs adjustment after summary/export changes.
5. T030-T040 before close-out.
## Parallel Work Guidance
- T006-T009 can run in parallel with T010-T013 after Phase 1.
- T014-T016 can run in parallel with summary/export implementation.
- T030-T034 can be reviewed after implementation before validation.
## Non-Goals Checklist
- [x] No new decision persistence.
- [x] No new review-pack status or operation type.
- [x] No customer approval/mutation workflow.
- [x] No raw OperationRun or proof links in customer-safe default content.
- [x] No new asset bundle or ad-hoc custom styling.

View File

@ -0,0 +1,48 @@
# Specification Quality Checklist: RBAC Role Matrix & Access Boundary Audit
**Purpose**: Validate specification completeness and quality before implementation planning/implementation.
**Created**: 2026-05-15
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No application implementation was performed during preparation.
- [x] Focus is on security, trust, auditability, and boundary correctness.
- [x] The spec is repo-based and names the current evidence anchors.
- [x] All mandatory repo-specific sections are completed or explicitly marked N/A.
- [x] The candidate check required by SPEC-GATE-001 is completed.
- [x] Candidate selection rationale and completed-spec guardrail result are recorded.
## Requirement Completeness
- [x] No `[NEEDS CLARIFICATION]` markers remain.
- [x] Functional requirements are testable and boundary-oriented.
- [x] Acceptance criteria cover role inventory, owner-only contradictions, panel boundaries, workspace isolation, environment isolation, sensitive actions, and no RBAC redesign.
- [x] Edge cases are identified.
- [x] Scope is clearly bounded to audit-first minimal hardening.
- [x] Dependencies and assumptions are identified.
## Constitution Alignment
- [x] Workspace isolation and managed-environment isolation are explicit.
- [x] RBAC-UX server-side source-of-truth rules are explicit.
- [x] 404 vs 403 semantics are explicit.
- [x] Capability registry usage is explicit.
- [x] Test governance and lane classification are explicit.
- [x] Proportionality review confirms no new persisted truth, role model, table, enum/status family, or broad framework is planned.
## Feature Readiness
- [x] `spec.md` exists.
- [x] `plan.md` exists.
- [x] `tasks.md` exists.
- [x] Tasks are ordered by read-only inventory, classification, tests first, minimal fixes, validation, and close-out.
- [x] Tasks include focused tests and validation commands.
- [x] Follow-up candidates are listed instead of hidden in scope.
- [x] Related completed specs are treated as context only and are not modified.
## Notes
- Preparation found a repo-real path correction: `WorkspaceRoleCapabilityMap.php` is under `apps/platform/app/Services/Auth/`, not `apps/platform/app/Support/Auth/`.
- Preparation found a high-risk static contradiction to verify during implementation: Manager currently receives `WORKSPACE_MEMBERSHIP_MANAGE` and `TENANT_MEMBERSHIP_MANAGE`, while the Constitution says Manager must not manage tenant memberships.
- Preparation did not modify application code, tests, migrations, resources, routes, policies, models, services, jobs, views, or assets.

View File

@ -0,0 +1,348 @@
# Implementation Plan: RBAC Role Matrix & Access Boundary Audit
**Branch**: `309-rbac-role-matrix-access-boundary-audit` | **Date**: 2026-05-15 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/309-rbac-role-matrix-access-boundary-audit/spec.md`
## Summary
Audit and harden TenantPilot's RBAC and panel-access boundaries across workspace, managed-environment, provider connection, review, review pack, evidence, finding exception, OperationRun, Admin panel, and System panel surfaces.
The implementation is audit-first:
1. Inventory repo-real role/capability grants and direct access boundaries.
2. Classify Constitution/runtime contradictions.
3. Write focused tests for confirmed contradictions and boundary bugs.
4. Apply only minimal runtime fixes.
5. Re-run focused regression lanes and document remaining product decisions.
## Technical Context
**Language/Version**: PHP 8.4.15, Laravel 12.52.0
**Primary Dependencies**: Filament 5.2.1, Livewire 4.1.4, Pest 4.3.1, Laravel Sail
**Storage**: PostgreSQL; no schema change expected
**Testing**: Pest 4 Unit and Feature tests; Browser only if Feature/Filament tests cannot prove a panel/action boundary
**Validation Lanes**: confidence; optional browser; formatting/diff-check
**Target Platform**: Laravel Sail locally; Dokploy container deployment for staging/production
**Project Type**: Laravel/Filament app under `apps/platform`
**Performance Goals**: No broad query redesign; authorization checks should reuse existing resolvers and avoid new global scans
**Constraints**: no new RBAC architecture, no migrations by default, no new roles, no new assets, no UI redesign, no broad route cleanup
**Scale/Scope**: focused security-boundary audit and minimal fixes over existing role maps/policies/panels/tests
## UI / Surface Guardrail Plan
- **Guardrail scope**: no new operator-facing surface; direct access/action authorization and possible minor visibility alignment only.
- **Native vs custom classification summary**: existing Filament v5 panels/resources/pages/actions.
- **Shared-family relevance**: panel access, workspace/environment context, resource authorization, direct action execution, global search safety.
- **State layers in scope**: panel guard, middleware, route model binding, policies, Filament page/action access, Livewire action execution.
- **Audience modes in scope**: workspace users, customer-safe readers, platform/system users.
- **Decision/diagnostic/raw hierarchy plan**: no presentation hierarchy change.
- **Raw/support gating plan**: verify raw/provider/support details remain gated by existing policies.
- **One-primary-action / duplicate-truth control**: no new actions.
- **Handling modes by drift class or surface**: security blocker and high-risk contradiction fix in 309; product-decision-needed stays deferred.
- **Repository-signal treatment**: review-mandatory for role-map, panel, policy, and sensitive action changes.
- **Special surface test profiles**: standard-native-filament plus direct Feature/Livewire action tests.
- **Required tests or manual smoke**: focused Unit/Feature tests; browser not required unless direct behavior cannot be proven otherwise.
- **Exception path and spread control**: no exceptions planned.
- **Active feature PR close-out entry**: RBAC Inventory / Boundary Proof / Remaining Decisions.
## Shared Pattern & System Fit
- **Cross-cutting feature marker**: yes.
- **Systems touched**: role maps, capability resolvers, policies, gates, panels, workspace context, Filament resources/pages/actions, existing RBAC tests.
- **Shared abstractions reused**: `Capabilities`, `PlatformCapabilities`, `WorkspaceRoleCapabilityMap`, `RoleCapabilityMap`, `WorkspaceCapabilityResolver`, `CapabilityResolver`, `ManagedEnvironmentAccessScopeResolver`, model policies, Gate definitions, Filament access methods.
- **New abstraction introduced? why?**: none planned.
- **Why the existing abstraction was sufficient or insufficient**: the repo already has canonical maps and resolvers; the issue is suspected incorrect grants or missing enforcement, not absent infrastructure.
- **Bounded deviation / spread control**: any fix must remain local to existing maps/policies/panel checks and must not add a public permission framework.
## OperationRun UX Impact
- **Touches OperationRun start/completion/link UX?**: authorization only.
- **Central contract reused**: `OperationRunPolicy`, `OperationRunCapabilityResolver`, `OperationRunLinks`.
- **Delegated UX behaviors**: all existing start/completion/link feedback remains unchanged.
- **Surface-owned behavior kept local**: no new operation UI or notifications.
- **Queued DB-notification policy**: N/A.
- **Terminal notification path**: existing central lifecycle.
- **Exception path**: none.
## Provider Boundary & Portability Fit
- **Shared provider/platform boundary touched?**: provider credential authorization is audited.
- **Provider-owned seams**: credential rotation/delete/dedicated credential management.
- **Platform-core seams**: ProviderConnection resource/policy and workspace/environment access boundaries.
- **Neutral platform terms / contracts preserved**: provider connection, provider credential, workspace, managed environment, operation.
- **Retained provider-specific semantics and why**: existing Microsoft/Intune details stay in provider-owned logic.
- **Bounded extraction or follow-up path**: no provider registry redesign; provider-specific follow-up only if audit finds structural drift.
## Constitution Check
- Inventory-first: PASS. No inventory or Graph sync behavior changes are planned.
- Read/write separation: PASS. Mutations remain behind existing action flows; this spec audits and fixes authorization.
- Graph contract path: PASS. No Graph endpoint or contract change.
- Deterministic capabilities: NEEDS IMPLEMENTATION PROOF. Role/capability grants must become deterministic and tested.
- RBAC-UX: NEEDS IMPLEMENTATION PROOF. Constitution says Manager must not manage tenant memberships; current maps appear to grant Manager membership-management authority.
- Workspace isolation: NEEDS IMPLEMENTATION PROOF. Cross-workspace direct URLs must be tested for representative sensitive resources.
- Tenant/managed-environment isolation: NEEDS IMPLEMENTATION PROOF. Same-workspace wrong-environment direct URLs must be tested.
- Cross-plane access: PARTIAL PASS. Existing `/system` tests prove several tenant-session denials; Spec 309 should keep/extend focused proof.
- Run observability: PASS. No new run behavior; OperationRun access must remain policy-gated.
- TEST-GOV-001: PASS if tests stay focused and no broad helper defaults are introduced.
- PROP-001 / BLOAT-001: PASS. No new tables, roles, capability framework, enum, or UI taxonomy planned.
- PERSIST-001: PASS. No new persisted truth.
- STATE-001: PASS. No new lifecycle/status family.
- UI-SEM-001: PASS. No presentation framework.
- XCUT-001: PASS if implementation reuses existing auth/policy/gate paths.
- PROV-001: PASS. Provider credential authorization is audited without expanding provider semantics.
- UI-FIL-001: PASS if any visibility alignment stays Filament-native and avoids assets/ad-hoc styling.
## Initial Read-Only Repo Findings
### RBAC Inventory
| Role | Current inspected sensitive capabilities | Initial match assessment | Action |
|---|---|---|---|
| Workspace Owner | `workspace_membership.manage`, `tenant_membership.manage`, provider manage/dedicated, review/evidence manage, finding exception approve | Mostly expected | Keep unless tests reveal bypass |
| Workspace Manager | `workspace_membership.manage`, appended `tenant_membership.manage`, provider manage, review/evidence manage, finding exception approve | High-risk contradiction for membership; other manage grants need classification | Add tests, fix membership if Owner-only confirmed |
| Workspace Operator | membership view, provider view/run, review/evidence view, findings triage, audit view | Mostly expected | Prove no owner-only mutation |
| Workspace Readonly | workspace/settings/alerts/baselines/audit view plus tenant/review/evidence/provider view via merged tenant role | Mostly expected but verify customer-safe download/mutation boundaries | Prove read-only cannot mutate |
| Platform/System | `PlatformUser` + `PlatformCapabilities`, separate `platform` guard | Expected separate plane | Prove no ordinary workspace access to `/system` and no implicit customer data access |
### Constitution vs Runtime
- `apps/platform/app/Services/Auth/WorkspaceRoleCapabilityMap.php` currently grants Manager `Capabilities::WORKSPACE_MEMBERSHIP_MANAGE`.
- `WorkspaceRoleCapabilityMap::getCapabilities()` appends `Capabilities::TENANT_MEMBERSHIP_MANAGE` for Manager.
- `apps/platform/app/Services/Auth/RoleCapabilityMap.php` does not grant tenant Manager `TENANT_MEMBERSHIP_MANAGE`, which makes the WorkspaceRoleCapabilityMap append the direct source of the tenant-membership Manager grant.
- `.specify/memory/constitution.md` states: "Manager ... MUST NOT manage tenant memberships (Owner-only)."
- Existing tests currently assert Manager can manage tenant membership:
- `apps/platform/tests/Unit/Auth/CapabilityResolverTest.php`
- `apps/platform/tests/Feature/Rbac/RoleMatrix/ManagerAccessTest.php`
### Panel Boundary Findings
- `apps/platform/app/Models/User.php::canAccessPanel()` currently returns `true`.
- Admin panel uses `web` guard plus `ensure-correct-guard:web`, `ensure-workspace-selected`, and `ensure-filament-tenant-selected`.
- System panel uses `authGuard('platform')`, `ensure-correct-guard:platform`, and `ensure-platform-capability:platform.access_system_panel`.
- Existing tests already cover several system-panel cross-plane denials:
- `apps/platform/tests/Feature/Auth/CrossScopeAccessTest.php`
- `apps/platform/tests/Feature/System/Spec113/AuthorizationSemanticsTest.php`
- `apps/platform/tests/Feature/System/Spec114/SystemConsoleAccessSemanticsTest.php`
- Spec 309 still needs a dedicated boundary test family tying those semantics to the RBAC matrix and current `canAccessPanel()` posture.
## Proposed Minimal Fix Strategy
1. Start with static tests for `WorkspaceRoleCapabilityMap` and `RoleCapabilityMap`.
2. If Constitution Owner-only semantics are confirmed, remove Manager's `WORKSPACE_MEMBERSHIP_MANAGE` grant and the Manager-specific appended `TENANT_MEMBERSHIP_MANAGE`.
3. Update existing tests that assert Manager membership management so they become denial tests.
4. Verify whether `User::canAccessPanel()` can be tightened without breaking admin login/workspace chooser flow. If not, document the middleware/policy proof and add direct route tests.
5. Add or adjust policy/action tests for confirmed direct-access bypasses only.
6. Align Filament visibility only after server-side policy behavior is correct.
7. Do not change provider/review/evidence/finding-operation grants unless the audit classifies them as confirmed security bugs rather than product decisions.
## Project Structure
### Documentation (this feature)
```text
specs/309-rbac-role-matrix-access-boundary-audit/
|-- spec.md
|-- plan.md
|-- tasks.md
`-- checklists/
`-- requirements.md
```
### Likely Source Code Surfaces For Later Implementation
```text
apps/platform/app/Support/Auth/
|-- Capabilities.php
|-- PlatformCapabilities.php
`-- WorkspaceRole.php
apps/platform/app/Support/
`-- TenantRole.php
apps/platform/app/Services/Auth/
|-- WorkspaceRoleCapabilityMap.php
`-- RoleCapabilityMap.php
apps/platform/app/Models/
|-- User.php
`-- PlatformUser.php
apps/platform/app/Providers/
|-- AuthServiceProvider.php
`-- Filament/
|-- AdminPanelProvider.php
`-- SystemPanelProvider.php
apps/platform/app/Policies/
|-- OperationRunPolicy.php
|-- ProviderConnectionPolicy.php
|-- EnvironmentReviewPolicy.php
|-- ReviewPackPolicy.php
|-- EvidenceSnapshotPolicy.php
`-- FindingExceptionPolicy.php
apps/platform/tests/Unit/Auth/
apps/platform/tests/Feature/Rbac/
apps/platform/tests/Feature/Auth/
apps/platform/tests/Feature/System/
apps/platform/tests/Feature/ProviderConnections/
apps/platform/tests/Feature/ReviewPack/
apps/platform/tests/Feature/Reviews/
apps/platform/tests/Feature/Findings/
apps/platform/tests/Feature/Operations/
```
**Structure Decision**: Use existing Laravel/Filament/Pest test families where equivalent coverage exists. Create new `tests/Feature/Rbac/*BoundaryTest.php` files only when no repo-real focused equivalent exists.
## Technical Approach
1. Inventory capabilities from `Capabilities::all()` and `PlatformCapabilities::all()`.
2. Inventory role grants from `WorkspaceRoleCapabilityMap::getCapabilities()` and `RoleCapabilityMap::getCapabilities()`.
3. Categorize capabilities into workspace administration, managed-environment administration, provider connections, findings/exceptions, reviews, review packs, evidence, OperationRuns, reports, support requests, and system panel.
4. Compare membership, credential, lifecycle, review/export, and operation actions against Constitution RBAC-UX statements.
5. Add focused tests for confirmed mismatches and direct route/action boundaries.
6. Apply only the smallest map/policy/panel/resource changes needed for confirmed bugs.
7. Run focused regressions for affected review, review-pack, provider, finding, and operation lanes.
8. Close out with role inventory, fixed contradictions, deferred product decisions, and validation results.
## Data / Model Implications
- No migration expected.
- No new model expected.
- No new capability persistence expected.
- Static role-to-capability maps may change.
- Existing `workspace_memberships` and `managed_environment_memberships` data is not migrated by default.
## Policy / Authorization Implications
- `WorkspaceMembershipPolicy` and `WorkspaceRoleCapabilityMap` are the primary owner-only membership-management proof targets.
- Provider credential actions should continue to rely on `ProviderConnectionPolicy`.
- Review and Review Pack actions should continue to rely on `EnvironmentReviewPolicy` and `ReviewPackPolicy`.
- FindingException lifecycle actions should continue to rely on `FindingExceptionPolicy`.
- OperationRun visibility/action access should continue to rely on `OperationRunPolicy` and `OperationRunCapabilityResolver`.
- Service-level mutations must call Gate/Policy where UI actions execute state changes.
## Filament v5 / Livewire v4 Compliance Notes
- Filament is v5.2.1 and Livewire is v4.1.4.
- Panel providers are registered in `apps/platform/bootstrap/providers.php`, not `bootstrap/app.php`.
- This spec does not add a new panel provider.
- Any changed globally searchable resource must either keep an Edit/View page or disable global search.
- Destructive actions must execute via `Action::make(...)->action(...)`, include `->requiresConfirmation()`, and still authorize server-side.
- No new assets are planned. Existing panel theme assets remain resolved through `PanelThemeAsset`; deployment should keep the existing `cd apps/platform && php artisan filament:assets` step where registered Filament assets are published.
## Test Strategy
### New Or Updated Focused Tests
- `apps/platform/tests/Unit/Auth/WorkspaceRoleCapabilityMapTest.php`
- `apps/platform/tests/Feature/Rbac/WorkspaceRoleCapabilityBoundaryTest.php`
- `apps/platform/tests/Feature/Rbac/AdminPanelAccessBoundaryTest.php`
- `apps/platform/tests/Feature/Rbac/SystemPanelAccessBoundaryTest.php`
- `apps/platform/tests/Feature/Rbac/ManagedEnvironmentAccessBoundaryTest.php`
- `apps/platform/tests/Feature/Rbac/ProviderConnectionAccessBoundaryTest.php`
- `apps/platform/tests/Feature/Rbac/ReviewPackAccessBoundaryTest.php`
- `apps/platform/tests/Feature/Rbac/OperationRunAccessBoundaryTest.php`
- `apps/platform/tests/Feature/Rbac/FindingExceptionLifecycleAccessBoundaryTest.php`
Use existing tests instead when they already provide the exact proof and update names only if a new focused file improves reviewability.
### Regression Lanes
- Review/customer workspace:
- `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php`
- `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php`
- ReviewPack:
- `apps/platform/tests/Feature/ReviewPack/ReviewPackRbacTest.php`
- `apps/platform/tests/Feature/ReviewPack/ReviewPackDownloadTest.php`
- ProviderConnection:
- existing `apps/platform/tests/Feature/ProviderConnections/*Authorization*` and credential security tests if provider boundaries change.
- OperationRun:
- `apps/platform/tests/Feature/Rbac/OperationRunWorkspaceFirstAuthorizationTest.php`
- `apps/platform/tests/Feature/Operations/TenantlessOperationRunViewerTest.php`
- existing operation link/legacy route guard tests if policy or links change.
### Validation Commands
```bash
cd apps/platform && ./vendor/bin/sail artisan test --compact \
tests/Unit/Auth/WorkspaceRoleCapabilityMapTest.php \
tests/Feature/Rbac/WorkspaceRoleCapabilityBoundaryTest.php \
tests/Feature/Rbac/AdminPanelAccessBoundaryTest.php \
tests/Feature/Rbac/SystemPanelAccessBoundaryTest.php \
tests/Feature/Rbac/ManagedEnvironmentAccessBoundaryTest.php \
tests/Feature/Rbac/ProviderConnectionAccessBoundaryTest.php \
tests/Feature/Rbac/ReviewPackAccessBoundaryTest.php \
tests/Feature/Rbac/OperationRunAccessBoundaryTest.php \
tests/Feature/Rbac/FindingExceptionLifecycleAccessBoundaryTest.php
```
```bash
cd apps/platform && ./vendor/bin/sail artisan test --compact \
tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php \
tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php \
tests/Feature/ReviewPack/ReviewPackRbacTest.php \
tests/Feature/ReviewPack/ReviewPackDownloadTest.php
```
If OperationRun policy or links are touched:
```bash
cd apps/platform && ./vendor/bin/sail artisan test --compact \
tests/Feature/Rbac/OperationRunWorkspaceFirstAuthorizationTest.php \
tests/Feature/Operations/TenantlessOperationRunViewerTest.php \
tests/Feature/Guards/OperationRunLinkContractGuardTest.php
```
Final:
```bash
cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent
git diff --check
```
## Implementation Phases
1. **Read-only inventory**: confirm role maps, panel guards, policies, routes, resources, and existing tests.
2. **Classification**: distinguish confirmed security bugs, product decisions, docs drift, UI-only mismatches, and out-of-scope findings.
3. **Tests first**: add failing tests for confirmed contradictions.
4. **Minimal fixes**: adjust maps/policies/panel/resource access only where confirmed.
5. **Regression validation**: run focused affected lanes.
6. **Close-out**: record inventory, fixed contradictions, deferred decisions, tests, and remaining risks.
## Risk Controls
- Do not remove Manager capabilities unrelated to membership unless classified as confirmed bugs.
- Do not tighten `canAccessPanel()` until direct route/middleware behavior and workspace chooser flow are understood.
- Do not implement support impersonation, break-glass redesign, or system-to-workspace implicit access.
- Do not add new capability aliases.
- Do not create migrations unless the existing model cannot represent the confirmed fix.
- Do not broaden test helpers or factories globally.
## Rollout / Deployment Considerations
- No migration expected.
- No env vars expected.
- No queue/cron changes expected.
- No storage volume changes expected.
- No frontend build expected.
- Any changed authorization behavior must be validated in Staging before Production promotion.
- Existing Dokploy deployment should keep current asset publication and cache-clearing steps; no new asset step is introduced by this spec.
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|---|---|---|
| None planned | N/A | N/A |
## Open Product Decisions To Preserve If Unclear
- Manager membership-management authority.
- Manager managed-environment access-scope authority.
- Manager provider credential rotation/delete authority.
- Manager accepted-risk approval authority.
- Readonly Review Pack download authority.
- System/platform support access to customer workspaces.
Unclear items must be recorded as product-decision-needed rather than fixed blindly.

View File

@ -0,0 +1,381 @@
# Feature Specification: RBAC Role Matrix & Access Boundary Audit
**Feature Branch**: `309-rbac-role-matrix-access-boundary-audit`
**Created**: 2026-05-15
**Status**: Ready for implementation
**Input**: User-provided Spec 309 draft: "RBAC Role Matrix & Access Boundary Audit"
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
- **Problem**: TenantPilot is becoming a governance-of-record platform, but repo analysis shows a possible security-boundary contradiction: the Constitution says Manager must not manage tenant memberships, while the current role capability map appears to grant Manager-level membership-management authority.
- **Today's failure**: The product may present deterministic workspace and managed-environment isolation while runtime capability maps, panel access, policies, and direct routes disagree. That can create privilege escalation, workspace/environment leakage, provider credential exposure, or customer-visible review artifact access outside intended scope.
- **User-visible improvement**: Operators and reviewers can trust that workspace isolation, managed-environment isolation, capability checks, and panel boundaries agree server-side. Sensitive membership, provider, review, review-pack, accepted-risk, and operation surfaces are proven by direct tests rather than navigation visibility.
- **Smallest enterprise-capable version**: Run an audit-first repo verification over the existing capability registry, role maps, panel providers, workspace context, policies, resources, and tests; add focused failing tests for confirmed contradictions; apply only minimal corrections to existing role maps, policies, panel access, or Filament visibility after server-side proof.
- **Explicit non-goals**: No new RBAC model, no new role family, no permission UI, no identity federation, no SCIM, no support impersonation redesign, no billing lifecycle work, no navigation cleanup, no route-family redesign, no migrations by default, no customer portal work, and no broad policy framework.
- **Permanent complexity imported**: Focused Unit/Feature tests and possibly small edits to existing capability maps, policies, panel access, or visibility helpers. No new table, persisted entity, enum/status family, capability registry architecture, frontend asset, or public abstraction.
- **Why now**: The roadmap prioritizes customer-safe review/productization, but RBAC ambiguity is a foundation risk. Spec 309 reduces security and trust risk before additional customer-facing governance surfaces are built on top.
- **Why not local**: The suspected contradiction spans `WorkspaceRoleCapabilityMap`, Constitution role semantics, `User::canAccessPanel()`, panel middleware, policies, and existing tests. A local patch to one resource would not prove the boundary.
- **Approval class**: Core Enterprise
- **Red flags triggered**: Cross-surface authorization audit and broad test matrix. Defense: this is a security-boundary verification pass, not a new framework; all work reuses existing capability/policy/gate infrastructure and fixes only confirmed contradictions.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 1 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 11/12**
- **Decision**: approve
## Spec Scope Fields *(mandatory)*
- **Scope**: workspace
- **Primary Routes**:
- `/admin`
- `/admin/login`
- workspace and managed-environment admin surfaces under the existing Admin panel
- existing environment-scoped resource routes for Provider Connections, Environment Reviews, Review Packs, Evidence, Finding Exceptions, and Operation Runs
- `/system`
- representative `/system/*` pages and operation-detail routes
- **Data Ownership**:
- No new persisted data.
- `workspace_memberships` remains the workspace role source.
- `managed_environment_memberships` remains an access-scope/membership surface as currently implemented; this spec audits whether any remaining role-bearing or owner-only authority contradicts the Constitution.
- Provider Connections, Environment Reviews, Review Packs, Evidence, Finding Exceptions, Stored Reports, and tenant-bound OperationRuns must remain workspace + managed-environment scoped.
- Workspace-bound OperationRuns may remain workspace-only, but tenant-bound runs must still enforce environment entitlement before capability checks.
- **RBAC**:
- Workspace membership is the first isolation boundary.
- Managed-environment entitlement is the second isolation boundary.
- Capability/policy authorization is third.
- UI visibility is last and never the security boundary.
- Non-member or out-of-scope access must deny as not found (`404`) where repo policy semantics already use boundary hiding.
- In-scope members missing a capability must receive `403` where policies define capability denial.
This is not a canonical-view spec. No new default filter behavior is introduced.
## Cross-Cutting / Shared Pattern Reuse *(mandatory)*
- **Cross-cutting feature?**: yes.
- **Interaction class(es)**: RBAC capability resolution, panel access, route authorization, Filament resource/page access, action authorization, global search safety, operation drilldown links, provider credential actions, review/export/download access, and accepted-risk lifecycle actions.
- **Systems touched**:
- `apps/platform/app/Support/Auth/Capabilities.php`
- `apps/platform/app/Services/Auth/WorkspaceRoleCapabilityMap.php`
- `apps/platform/app/Services/Auth/RoleCapabilityMap.php`
- `apps/platform/app/Providers/AuthServiceProvider.php`
- `apps/platform/app/Models/User.php`
- `apps/platform/app/Providers/Filament/AdminPanelProvider.php`
- `apps/platform/app/Providers/Filament/SystemPanelProvider.php`
- `apps/platform/app/Support/Workspaces/WorkspaceContext.php`
- `apps/platform/app/Filament/Concerns/WorkspaceScopedTenantRoutes.php`
- relevant policies/resources/pages for ProviderConnection, EnvironmentReview, ReviewPack, EvidenceSnapshot, StoredReport, FindingException, and OperationRun.
- **Existing pattern(s) to extend**: capability registry constants, `WorkspaceCapabilityResolver`, `CapabilityResolver`, `ManagedEnvironmentAccessScopeResolver`, model policies, Gate definitions, Filament `canAccess()` / `canViewAny()` / policy checks, existing cross-plane middleware, and existing 404/403 semantics.
- **Shared contract / presenter / builder / renderer to reuse**: Existing capability/policy/gate infrastructure only. Do not introduce a new RBAC abstraction.
- **Why the existing shared path is sufficient or insufficient**: The repo already has a central capability registry and role maps. The issue is whether current mappings, panels, policies, and tests agree with the Constitution, not absence of infrastructure.
- **Allowed deviation and why**: None planned. If implementation discovers that a minimal helper is required to remove duplicated direct checks, it must be private, narrow, and justified in close-out.
- **Consistency impact**: Role maps, policies, direct URLs, Filament action execution, global search, operation links, and panel guards must all derive the same outcome for the same actor/workspace/environment/capability tuple.
- **Review focus**: Block UI-only security, raw role-string checks, direct route bypasses, cross-plane access, Manager/Operator/Readonly owner-only grants, and speculative RBAC redesign.
## OperationRun UX Impact *(mandatory)*
- **Touches OperationRun start/completion/link UX?**: yes, authorization only.
- **Shared OperationRun UX contract/layer reused**: `OperationRunPolicy`, `OperationRunCapabilityResolver`, `OperationRunLinks`, and existing Monitoring/Operations route helpers.
- **Delegated start/completion UX behaviors**: Existing operation start/link/display UX remains unchanged; this spec verifies that run visibility and action capability checks remain scope-safe.
- **Local surface-owned behavior that remains**: No new operation UI. Any touched action keeps its existing initiation input and feedback path.
- **Queued DB-notification policy**: N/A.
- **Terminal notification path**: Existing central lifecycle mechanism.
- **Exception required?**: none.
## Provider Boundary / Platform Core Check *(mandatory)*
- **Shared provider/platform boundary touched?**: yes, provider credential access is audited.
- **Boundary classification**: mixed. ProviderConnection records are platform-core integration records bound to a workspace and managed environment; provider credential operations remain high-privilege provider-owned behavior.
- **Seams affected**: ProviderConnection policies/resources/actions, provider management capabilities, dedicated credential actions, verification/start surfaces.
- **Neutral platform terms preserved or introduced**: provider connection, provider credential, managed environment, workspace, operation.
- **Provider-specific semantics retained and why**: Existing Microsoft/Intune specifics remain inside provider connection and Graph-facing code; this spec does not introduce provider-specific platform terminology.
- **Why this does not deepen provider coupling accidentally**: The audit checks access boundaries only and does not add provider contracts, endpoints, or provider taxonomy.
- **Follow-up path**: Support Access Governance remains separate; provider capability registry redesign is out of scope.
## UI / Surface Guardrail Impact *(mandatory)*
No new operator-facing surface is introduced. UI changes are allowed only after server-side authorization is correct and only to align visibility/disabled state with existing policy outcomes.
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|---|---|---|---|---|---|---|
| Admin panel access boundary | no new surface | Existing Filament panel | panel access, workspace context | middleware, panel gate, route | no | direct URL behavior must be tested |
| System panel access boundary | no new surface | Existing Filament system panel | cross-plane access | guard, middleware, panel auth | no | ordinary workspace users must not access `/system` |
| Existing resource/actions visibility | possible minor alignment only | Native Filament resources/actions | action execution and visibility | action policy, page access | no | visibility follows server-side policy |
## Decision-First Surface Role *(mandatory when operator-facing surfaces are changed)*
N/A - this spec does not create or materially redesign an operator-facing surface. Existing surfaces remain decision-owned by their current resources/pages.
## Audience-Aware Disclosure *(mandatory when operator-facing surfaces are changed)*
N/A - no customer-facing content or disclosure hierarchy changes. Existing raw/support diagnostics must remain capability-gated or hidden according to current policies.
## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)*
N/A - no new or redesigned list/detail/workbench surface. Existing Filament v5 action rules still apply to any touched resource/action: destructive actions require `->requiresConfirmation()` and server-side authorization.
## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)*
N/A - no new operator surface contract. Direct access/action tests are the proof mechanism for this spec.
## Proportionality Review *(mandatory when structural complexity is introduced)*
- **New source of truth?**: no.
- **New persisted entity/table/artifact?**: no.
- **New abstraction?**: no planned abstraction.
- **New enum/state/reason family?**: no.
- **New cross-domain UI framework/taxonomy?**: no.
- **Current operator problem**: possible mismatch between role/capability truth, Constitution semantics, panel access, and direct route/action authorization.
- **Existing structure is insufficient because**: it may contain incorrect grants or missing policy checks; the structure itself is sufficient if corrected and tested.
- **Narrowest correct implementation**: inventory, classify, write focused tests, and adjust only existing maps/policies/panel checks where repo truth confirms a bug.
- **Ownership cost**: focused RBAC boundary tests and close-out inventory.
- **Alternative intentionally rejected**: new permission framework or role model. That would import complexity without being required for this security audit.
- **Release truth**: current-release trust and security boundary hardening.
### Compatibility posture
The product is pre-production. Compatibility shims, legacy capability aliases, dual-read logic, and data migrations are out of scope unless a repo-verified security blocker cannot be represented by the current role/capability model.
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
- **Test purpose / classification**: Unit and Feature. Browser is not required unless direct panel/action behavior cannot be proven by Feature/Filament tests.
- **Validation lane(s)**: confidence for RBAC/panel/policy tests; optional browser only for unprovable rendered panel interaction.
- **Why this classification and these lanes are sufficient**: Static role-map tests prove deterministic grants. Feature tests prove direct URLs, policies, action execution, and cross-scope denial. Browser smoke is unnecessary when Feature/Livewire tests prove direct route/action boundaries.
- **New or expanded test families**: `apps/platform/tests/Unit/Auth/WorkspaceRoleCapabilityMapTest.php` and focused `apps/platform/tests/Feature/Rbac/*BoundaryTest.php` files or repo-real equivalents.
- **Fixture / helper cost impact**: moderate and feature-local. Tests need users, workspace memberships, managed environments, environment-scope rows, ProviderConnections, ReviewPacks, EnvironmentReviews, FindingExceptions, OperationRuns, and platform users. Shared helper defaults must stay cheap.
- **Heavy-family visibility / justification**: none expected.
- **Special surface test profile**: standard-native-filament and access-boundary feature tests.
- **Standard-native relief or required special coverage**: feature tests are preferred over browser smoke for panel boundaries and policies.
- **Reviewer handoff**: Confirm 404 vs 403 semantics, direct route/action proof, no UI-only security, no new RBAC model, no assets, no migrations, and no broadened test defaults.
- **Budget / baseline / trend impact**: low to moderate focused confidence-lane growth; no heavy-governance family.
- **Escalation needed**: none unless implementation discovers a structural RBAC model gap.
- **Active feature PR close-out entry**: RBAC Inventory / Boundary Proof / Remaining Decisions.
- **Planned validation commands**:
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Auth/WorkspaceRoleCapabilityMapTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Rbac/WorkspaceRoleCapabilityBoundaryTest.php tests/Feature/Rbac/AdminPanelAccessBoundaryTest.php tests/Feature/Rbac/SystemPanelAccessBoundaryTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Rbac/ManagedEnvironmentAccessBoundaryTest.php tests/Feature/Rbac/ProviderConnectionAccessBoundaryTest.php tests/Feature/Rbac/ReviewPackAccessBoundaryTest.php tests/Feature/Rbac/OperationRunAccessBoundaryTest.php tests/Feature/Rbac/FindingExceptionLifecycleAccessBoundaryTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php tests/Feature/ReviewPack/ReviewPackRbacTest.php tests/Feature/ReviewPack/ReviewPackDownloadTest.php`
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- `git diff --check`
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Role Matrix Truth Is Audited (Priority: P1)
As a platform maintainer, I need a repo-derived inventory of roles and capabilities so owner-only contradictions can be identified before new customer-facing governance surfaces are built.
**Why this priority**: This is the trigger for the spec and the prerequisite for deciding whether runtime behavior should change.
**Independent Test**: A focused role-map test can prove whether Owner, Manager, Operator, Readonly, and Platform/System grants match the intended sensitive-boundary matrix.
**Acceptance Scenarios**:
1. **Given** the current capability registry and role maps, **When** the implementation inventories all role grants, **Then** the close-out lists each sensitive grant, match/mismatch classification, and action.
2. **Given** the Constitution says Manager must not manage tenant memberships, **When** the role map grants Manager membership-management capability, **Then** the mismatch is either fixed as a confirmed bug or explicitly recorded as product-decision-needed.
---
### User Story 2 - Panel And Scope Boundaries Are Proven Directly (Priority: P1)
As a security reviewer, I need direct URL proof for `/admin`, `/system`, workspace isolation, and managed-environment isolation so hidden navigation cannot be mistaken for security.
**Why this priority**: Panel and scope bypasses would be security blockers.
**Independent Test**: Feature tests can authenticate ordinary workspace users, platform users, non-members, wrong-workspace members, and wrong-environment members, then assert direct URLs/actions return the repo-standard denial.
**Acceptance Scenarios**:
1. **Given** an ordinary workspace user, **When** they request `/system`, **Then** the system returns deny-as-not-found and no system content is visible.
2. **Given** a user without valid workspace authority, **When** they request direct `/admin` workspace surfaces, **Then** access is denied by authentication/workspace middleware or policy, not only hidden navigation.
3. **Given** a member of workspace A, **When** they request workspace B review packs, findings, provider connections, or operation runs, **Then** the response denies as not found.
4. **Given** a member scoped to environment A, **When** they request environment B records in the same workspace, **Then** the response denies as not found before capability checks.
---
### User Story 3 - Sensitive Actions Are Server-Side Gated (Priority: P2)
As an enterprise operator, I need sensitive membership, provider credential, review/review-pack, accepted-risk, and operation actions to be impossible for unauthorized roles even through direct action execution.
**Why this priority**: Mutation boundaries are higher risk than read-only view boundaries, and UI visibility is explicitly not a security boundary.
**Independent Test**: Filament/Livewire Feature tests and policy tests can call representative actions directly and assert non-owner or missing-capability actors are denied.
**Acceptance Scenarios**:
1. **Given** a Manager/Operator/Readonly actor, **When** they attempt owner-only membership management, **Then** the server denies execution.
2. **Given** a Manager/Operator actor without high-privilege provider authority, **When** they attempt provider credential rotation/delete/dedicated credential management, **Then** the server denies execution.
3. **Given** a Readonly actor, **When** they attempt finding exception approval/rejection, review publication/archive/export mutation, review-pack mutation, or operation action, **Then** the server denies execution.
4. **Given** an Owner or allowed role, **When** they perform existing allowed flows covered by regression tests, **Then** the flow still works.
## Edge Cases
- A user has workspace membership but no current workspace context.
- A user has workspace membership but a stale remembered managed-environment context.
- A user has platform/system authority but no workspace membership.
- A user has both web and platform sessions; cross-plane session separation must still hide the other plane.
- A record has a workspace_id / managed_environment_id mismatch.
- An OperationRun is workspace-bound with no managed_environment_id.
- An OperationRun is tenant-bound but its environment is deleted or outside the current workspace.
- A Filament URL-only action is navigation-only; confirmation behavior must not be assumed unless the action executes via `->action(...)`.
- A resource is globally searchable but lacks an Edit/View page; global search must be disabled or the page must exist.
## Functional Requirements *(mandatory)*
- **FR-001 Role Map Inventory**: Implementation MUST produce a repo-derived inventory of all roles and assigned capabilities, including Owner, Manager, Operator, Readonly, Platform/System, and any repo-real special roles.
- **FR-002 Constitution Alignment**: Implementation MUST compare Constitution RBAC statements against runtime mappings and classify mismatches as confirmed bug, intentional product decision, docs/constitution drift, unclear/product-decision-needed, or out of scope.
- **FR-003 Owner-only Enforcement**: Confirmed owner-only capabilities MUST NOT be granted to Manager, Operator, Readonly, or customer-safe actors.
- **FR-004 Admin Panel Boundary Enforcement**: Direct access to `/admin` surfaces MUST be tested. `User::canAccessPanel()` MUST NOT be the only boundary when it is permissive.
- **FR-005 System Panel Boundary Enforcement**: Direct access to `/system` MUST be tested. Ordinary workspace users MUST NOT access the System panel through workspace roles.
- **FR-006 Workspace Isolation**: Cross-workspace direct access MUST be denied for at least Environment Review, Review Pack, FindingException or decision surface, OperationRun, and ProviderConnection or equivalent sensitive resource.
- **FR-007 Managed Environment Isolation**: Same-workspace wrong-environment direct access MUST be denied for at least Review/ReviewPack, Evidence or StoredReport, FindingException, and OperationRun.
- **FR-008 Provider Connection Boundary**: ProviderConnection view/manage/verify/update/delete/dedicated-credential capabilities MUST match the target role model and be proven server-side.
- **FR-009 Review / Review Pack Boundary**: Customer-safe review and review-pack surfaces MUST preserve existing access rules, redaction, and no hidden environment leakage in summaries, counts, exports, or download URLs.
- **FR-010 FindingException / Decision Boundary**: Approval, rejection, renewal, revocation, closure, and accepted-risk lifecycle actions MUST be capability-gated server-side.
- **FR-011 OperationRun Boundary**: OperationRun visibility and action permissions MUST be workspace/environment scope-safe; run links MUST NOT grant access to users who cannot view the underlying run.
- **FR-012 Service-level Authorization**: Critical mutation paths MUST be checked for policy/gate enforcement beyond hidden UI actions.
- **FR-013 No New RBAC System**: Implementation MUST reuse existing capabilities, policies, gates, middleware, and resource checks.
- **FR-014 Close-out Inventory Format**: Close-out MUST include `Role -> Capabilities -> Sensitive? -> Matches target? -> Action`.
## Non-Functional Requirements
- **NFR-001 Minimality**: Fix only confirmed contradictions and direct boundary bugs.
- **NFR-002 Auditability**: Sensitive access decisions must be explainable by role, capability, workspace, environment, and policy.
- **NFR-003 Determinism**: Same user, workspace, environment, and capability set must always produce the same authorization outcome.
- **NFR-004 Testability**: Each corrected boundary must have focused Unit or Feature coverage.
- **NFR-005 No UI-only Security**: Navigation visibility, hidden actions, and disabled actions are insufficient proof.
- **NFR-006 No New Assets**: No CSS, JS, Vite, or design-system changes.
- **NFR-007 No Migration By Default**: Stop and document if a migration appears necessary.
## Security Requirements
- **SEC-001**: Workspace isolation is mandatory for all workspace-owned records.
- **SEC-002**: Managed-environment isolation is mandatory for all environment-owned records.
- **SEC-003**: Panel access must be enforced server-side.
- **SEC-004**: Membership management must be Owner-only unless a repo/product decision explicitly says otherwise.
- **SEC-005**: Provider credential-level operations must be high privilege only.
- **SEC-006**: Readonly/customer-safe users must not mutate anything.
- **SEC-007**: Platform/system users must not implicitly access customer workspace data through system authority.
- **SEC-008**: Direct URLs to denied objects must follow repo-standard denial semantics: `404` for non-member/out-of-scope and `403` for in-scope missing capability where existing policies define it.
- **SEC-009**: No test may rely only on navigation invisibility.
## Repo Evidence Anchors
Initial preparation found these repo-real paths:
| Area | Repo-real path | Preparation note |
|---|---|---|
| Capability registry | `apps/platform/app/Support/Auth/Capabilities.php` | Canonical tenant/workspace capability names |
| Workspace role map | `apps/platform/app/Services/Auth/WorkspaceRoleCapabilityMap.php` | User draft said `Support/Auth`; repo path is `Services/Auth` |
| Managed-environment role map | `apps/platform/app/Services/Auth/RoleCapabilityMap.php` | Current tenant-role capabilities |
| Workspace roles | `apps/platform/app/Support/Auth/WorkspaceRole.php` | Owner, Manager, Operator, Readonly |
| Tenant roles | `apps/platform/app/Support/TenantRole.php` | Owner, Manager, Operator, Readonly |
| Platform capabilities | `apps/platform/app/Support/Auth/PlatformCapabilities.php` | System panel capabilities |
| Auth/provider wiring | `apps/platform/app/Providers/AuthServiceProvider.php` | Gate and policy registration |
| User panel access | `apps/platform/app/Models/User.php` | `canAccessPanel()` currently returns true |
| Admin panel | `apps/platform/app/Providers/Filament/AdminPanelProvider.php` | `/admin`, web guard, workspace/environment middleware |
| System panel | `apps/platform/app/Providers/Filament/SystemPanelProvider.php` | `/system`, platform guard, platform capability middleware |
| Provider registration | `apps/platform/bootstrap/providers.php` | Laravel 12 provider registration location |
| Workspace context | `apps/platform/app/Support/Workspaces/WorkspaceContext.php` | Workspace session/context enforcement |
| Environment routes | `apps/platform/app/Filament/Concerns/WorkspaceScopedTenantRoutes.php` | Managed-environment scoped URL model |
| OperationRun policy | `apps/platform/app/Policies/OperationRunPolicy.php` | Workspace/environment/run access |
| ProviderConnection policy | `apps/platform/app/Policies/ProviderConnectionPolicy.php` | Provider credential/scope boundary |
| Review policy | `apps/platform/app/Policies/EnvironmentReviewPolicy.php` | Review access/mutation |
| ReviewPack policy | `apps/platform/app/Policies/ReviewPackPolicy.php` | Review pack access/mutation |
| Evidence policy | `apps/platform/app/Policies/EvidenceSnapshotPolicy.php` | Evidence access/mutation |
| FindingException policy | `apps/platform/app/Policies/FindingExceptionPolicy.php` | Accepted-risk/decision lifecycle |
| Constitution | `.specify/memory/constitution.md` | RBAC-UX owner-only and cross-plane rules |
## Initial Repo-Derived Inventory Snapshot
This preparation did not implement changes, but read-only inspection found:
- `WorkspaceRoleCapabilityMap` grants Owner `WORKSPACE_MEMBERSHIP_MANAGE` and `TENANT_MEMBERSHIP_MANAGE` through merged tenant-role capabilities.
- `WorkspaceRoleCapabilityMap` grants Manager `WORKSPACE_MEMBERSHIP_MANAGE` directly and appends `TENANT_MEMBERSHIP_MANAGE` in `getCapabilities()`.
- `RoleCapabilityMap` grants tenant Manager provider manage, review pack manage, environment review manage, evidence manage, and many governance mutation capabilities, while it does not grant `TENANT_ROLE_MAPPING_MANAGE`.
- Operator has operational and read scopes but no `TENANT_MEMBERSHIP_MANAGE`, `TENANT_MANAGE`, `PROVIDER_MANAGE`, or `TENANT_BACKUP_SCHEDULES_MANAGE`.
- Readonly has view-only provider/review/evidence/audit-style capabilities and no mutation capabilities from the inspected map.
- Platform users are separate `PlatformUser` records using the `platform` guard and `PlatformCapabilities`.
- Existing system-panel tests already prove several tenant-session-to-`/system` denials, but Spec 309 should add/extend focused boundary tests for the current risk matrix.
## Suspected Contradictions To Verify
- Manager membership-management grant appears to contradict the Constitution line: "Manager ... MUST NOT manage tenant memberships (Owner-only)."
- `User::canAccessPanel()` returning true may be acceptable only if admin/system middleware, guards, workspace context, and policies definitively enforce access. Implementation must prove this before changing it.
- Existing tests currently assert Manager can manage tenant membership in `apps/platform/tests/Unit/Auth/CapabilityResolverTest.php` and `apps/platform/tests/Feature/Rbac/RoleMatrix/ManagerAccessTest.php`; if Owner-only is confirmed, those tests must be updated as failing proof before runtime fix.
- Provider manage and credential-level capabilities for Manager may be intentional or too broad; classify before changing.
- Review/ReviewPack/Evidence manage capabilities for Manager may be intentional governance workflow authority; classify before changing.
- FindingException approval via workspace capability currently grants Owner and Manager from the inspected map; classify whether Manager approval is intended.
## Acceptance Criteria
- **AC-001 Role Inventory Produced**: Close-out includes a role/capability inventory for Owner, Manager, Operator, Readonly, Platform/System, and repo-real special roles.
- **AC-002 Owner-only Contradictions Resolved Or Classified**: Manager/Operator grants of owner-only membership-management capability are fixed or explicitly classified.
- **AC-003 Admin Panel Boundary Tested**: Tests prove users without valid admin/workspace authority cannot access `/admin` surfaces through direct URLs.
- **AC-004 System Panel Boundary Tested**: Tests prove ordinary workspace users cannot access `/system`.
- **AC-005 Workspace Isolation Tested**: Cross-workspace direct access is denied for EnvironmentReview, ReviewPack, FindingException or decision surface, OperationRun, and ProviderConnection/equivalent.
- **AC-006 Managed Environment Isolation Tested**: Same-workspace wrong-environment direct access is denied for Review/ReviewPack, Evidence or StoredReport, FindingException, and OperationRun.
- **AC-007 Sensitive Actions Tested**: At least one sensitive action from each group is tested against unauthorized roles: membership management, provider credential management, review/review-pack mutation, accepted-risk lifecycle mutation, and operation action/view.
- **AC-008 UI Visibility Is Not The Only Guard**: Every fixed sensitive action has a direct server-side access/action test.
- **AC-009 No Broad RBAC Redesign**: No new role model, table, public permission framework, or capability registry redesign is introduced.
- **AC-010 Focused Tests Pass**: Focused RBAC/panel/access-boundary tests pass.
- **AC-011 Existing Product Flows Still Work**: Existing Review, Review Pack, Findings, and OperationRun scenarios covered by regression tests still pass for Owner and allowed Manager/Operator roles.
- **AC-012 Filament v5 Contract Preserved**: Filament remains v5 with Livewire v4; provider registration remains in `apps/platform/bootstrap/providers.php`; no new assets are introduced; deploy continues to include the existing Filament asset publication step where registered assets are used.
## Assumptions
- Spec 308 is merged or cleanly separated before implementation.
- The product remains pre-production, so removing incorrect capability grants does not require data migration shims.
- Existing capability names remain canonical; no new capability strings are introduced unless implementation proves an existing owner-only capability cannot express the boundary.
- Browser tests are optional and only needed if Feature/Filament tests cannot prove a panel or action boundary.
## Risks
- Removing Manager membership authority may break existing tests and workflows that assumed Manager could manage memberships.
- Constitution may be newer than current product intent; unclear cases must be product-decision-needed instead of silently fixed.
- Tightening `canAccessPanel()` without understanding workspace-selection flow could block legitimate login/chooser flows.
- Provider/manage and review/manage authority may be too broad for this spec if the repo treats them as product decisions.
- Adding broad fixtures could worsen test-suite cost; keep RBAC tests focused and helper setup opt-in.
## Open Questions
- Can Manager manage workspace membership? Recommended default: no.
- Can Manager manage managed-environment membership/access scope? Recommended default: no unless product explicitly approves partial scope management.
- Can Manager rotate or delete provider credentials? Recommended default: no.
- Can Manager approve accepted risks? Recommended default: existing policy unless unsafe, but classify.
- Can Readonly download Review Packs? Recommended default: capability-gated according to existing review-pack policy.
- Should `User::canAccessPanel()` be restrictive? Recommended default: restrictive where it does not break workspace selection, with middleware/policies still enforcing fine-grained boundaries.
- Can platform/system users access workspace data? Recommended default: no implicit access; support access governance stays separate.
## Candidate Selection Rationale
- **Selected candidate**: `309 - RBAC Role Matrix & Access Boundary Audit`.
- **Source locations**:
- explicit user-provided Spec 309 draft on 2026-05-15
- `.specify/memory/constitution.md` RBAC-UX rules for workspace/tenant isolation, owner-only tenant membership management, cross-plane denial, and server-side authorization
- `docs/product/roadmap.md` enterprise access boundary/security hardening context
- `docs/product/spec-candidates.md` enterprise access boundary/support access governance context, with support-access implementation kept out of scope
- **Why selected**: The repo-read found a high-risk static mismatch between Constitution owner-only membership semantics and the current Manager grants in `WorkspaceRoleCapabilityMap`. This security-boundary warning should be verified before further customer-facing productization.
- **Why close alternatives were deferred**:
- Customer Review Workspace v1 Completion is deferred until RBAC/access boundaries are verified.
- Product Truth / Docs Drift Reconciliation is deferred until Spec 309 distinguishes runtime truth from docs drift.
- Support Access Governance v1 is deferred because it is a new support/impersonation product slice, not the minimal role-matrix audit.
- **Roadmap relationship**: Foundation hardening before customer-safe governance productization.
- **Smallest viable implementation slice**: inventory, classify, focused tests, minimal fixes for confirmed contradictions, and close-out decisions.
## Completed-Spec Guardrail Result
Related existing specs are context only and must not be rewritten:
- `specs/285-workspace-rbac-environment-access/` has implementation-completed/validated task markers and browser-smoke proof; use only as workspace-first RBAC context.
- `specs/276-support-access-governance/` has completed task markers and review outcome; support access remains a follow-up, not part of 309.
- `specs/301-admin-inventory-navigation-cutover/`, `specs/302-tenant-owned-surface-route-audit/`, `specs/303-admin-directory-groups-cutover/`, and `specs/304-tenant-panel-dead-code-retirement/` are completed/reviewed context for panel and route behavior.
- `specs/307-decision-register-evidence-operationrun-link-polish/` and `specs/308-decision-register-summary-review-pack/` are Decision Register / Review Pack context only.
- No existing `specs/309-*` package existed before this preparation.
## Follow-up Candidates
- Support Access Governance v1: audited support/impersonation, TTL, reason, approval, banner, and exportable access logs.
- Product Truth / Docs Drift Reconciliation: update roadmap/ledger/candidates after repo truth is confirmed.
- Commercial Entitlements / Billing-State Enforcement: plan lifecycle and workspace entitlement gates.
- Customer Review Workspace v1 Completion: continue customer-facing productization after security boundary verification.
- Route / Panel Access Contract Audit: broader route duplication and canonical-route cleanup if 309 uncovers structural route drift.
## Implementation Done Definition
Spec 309 is done when the role/capability inventory is documented, owner-only contradictions are fixed or classified, `/admin` and `/system` boundaries are tested, cross-workspace and cross-environment boundaries are tested, sensitive actions are server-side denied for unauthorized roles, no new RBAC architecture is introduced, focused tests pass, Pint dirty passes, `git diff --check` passes, and remaining product decisions are listed as follow-ups.

View File

@ -0,0 +1,204 @@
# Tasks: RBAC Role Matrix & Access Boundary Audit
**Input**: Design documents from `/specs/309-rbac-role-matrix-access-boundary-audit/`
**Prerequisites**: `spec.md`, `plan.md`, `checklists/requirements.md`
**Tests**: Required. Use Pest 4 Unit/Feature tests; Browser only if Feature/Filament tests cannot prove the access boundary.
## Test Governance Checklist
- [x] Lane assignment is named and is the narrowest sufficient proof for the changed behavior.
- [x] New or changed tests stay in Unit/Feature unless a panel interaction cannot be proven otherwise.
- [x] Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default.
- [x] Planned validation commands cover the changed boundaries without pulling unrelated lane cost.
- [x] Browser coverage, if any, is explicit and justified.
- [x] Close-out records fixed contradictions, deferred decisions, and validation results.
## Phase 1: Read-Only Inventory
**Purpose**: Verify repo truth before any runtime or test changes.
- [x] T001 Confirm current branch is `309-rbac-role-matrix-access-boundary-audit` and working tree state is understood.
- [x] T002 Read `.specify/memory/constitution.md` RBAC, workspace isolation, tenant isolation, cross-plane, and testing sections.
- [x] T003 [P] Inspect `apps/platform/app/Support/Auth/Capabilities.php` and `apps/platform/app/Support/Auth/PlatformCapabilities.php`.
- [x] T004 [P] Inspect `apps/platform/app/Services/Auth/WorkspaceRoleCapabilityMap.php` and `apps/platform/app/Services/Auth/RoleCapabilityMap.php`.
- [x] T005 [P] Inspect `apps/platform/app/Support/Auth/WorkspaceRole.php` and `apps/platform/app/Support/TenantRole.php`.
- [x] T006 [P] Inspect `apps/platform/app/Models/User.php` and `apps/platform/app/Models/PlatformUser.php`.
- [x] T007 [P] Inspect `apps/platform/app/Providers/AuthServiceProvider.php` and `apps/platform/bootstrap/providers.php`.
- [x] T008 [P] Inspect `apps/platform/app/Providers/Filament/AdminPanelProvider.php` and `apps/platform/app/Providers/Filament/SystemPanelProvider.php`.
- [x] T009 [P] Inspect `apps/platform/app/Support/Workspaces/WorkspaceContext.php` and `apps/platform/app/Filament/Concerns/WorkspaceScopedTenantRoutes.php`.
- [x] T010 [P] Inspect `apps/platform/app/Policies/WorkspaceMembershipPolicy.php`, `apps/platform/app/Policies/WorkspacePolicy.php`, and workspace membership Filament relation-manager surfaces.
- [x] T011 [P] Inspect `apps/platform/app/Policies/ProviderConnectionPolicy.php` and provider connection resources/actions.
- [x] T012 [P] Inspect `apps/platform/app/Policies/EnvironmentReviewPolicy.php`, `apps/platform/app/Policies/ReviewPackPolicy.php`, `apps/platform/app/Policies/EvidenceSnapshotPolicy.php`, and related resources/pages.
- [x] T013 [P] Inspect `apps/platform/app/Policies/FindingExceptionPolicy.php` and finding exception lifecycle services/actions.
- [x] T014 [P] Inspect `apps/platform/app/Policies/OperationRunPolicy.php`, `apps/platform/app/Support/Operations/OperationRunCapabilityResolver.php`, and operation link/view helpers.
- [x] T015 [P] Inspect existing RBAC, panel, provider, review-pack, customer-review, finding-exception, and OperationRun authorization tests under `apps/platform/tests/`.
- [x] T016 Produce a close-out-ready initial inventory table: `Role -> Capabilities -> Sensitive? -> Matches target? -> Action`.
## Phase 2: Classification
**Purpose**: Decide what is a confirmed bug versus intentional product behavior or docs drift.
- [x] T017 Classify Manager `WORKSPACE_MEMBERSHIP_MANAGE` grant in `apps/platform/app/Services/Auth/WorkspaceRoleCapabilityMap.php`.
- [x] T018 Classify Manager `TENANT_MEMBERSHIP_MANAGE` grant appended in `WorkspaceRoleCapabilityMap::getCapabilities()`.
- [x] T019 Classify Manager provider manage and dedicated credential boundaries in `apps/platform/app/Policies/ProviderConnectionPolicy.php`.
- [x] T020 Classify Manager review, review-pack, evidence, and finding-exception approval/manage capabilities.
- [x] T021 Classify `apps/platform/app/Models/User.php::canAccessPanel()` as safe-by-middleware or a confirmed defense-in-depth gap.
- [x] T022 Classify `/system` cross-plane behavior using existing system-panel tests and direct route behavior.
- [x] T023 Identify confirmed security blockers that must be fixed in Spec 309.
- [x] T024 Identify product-decision-needed items that must not be fixed blindly.
- [x] T025 Identify docs-only drift and follow-up candidates outside Spec 309.
- [x] T026 Record the minimal runtime fix list before editing application code.
## Phase 3: Tests First - Role And Panel Boundaries
**Purpose**: Add failing or tightening proof before runtime fixes.
- [x] T027 [P] Add or update `apps/platform/tests/Unit/Auth/WorkspaceRoleCapabilityMapTest.php` to assert known capabilities, unknown capability rejection, and Owner-only membership-management expectations once confirmed.
- [x] T028 [P] Update `apps/platform/tests/Unit/Auth/CapabilityResolverTest.php` if Manager tenant-membership management is confirmed incorrect.
- [x] T029 [P] Update `apps/platform/tests/Feature/Rbac/RoleMatrix/ManagerAccessTest.php` if Manager tenant-membership management is confirmed incorrect.
- [x] T030 [P] Add or update `apps/platform/tests/Feature/Rbac/WorkspaceRoleCapabilityBoundaryTest.php` for Manager/Operator/Readonly owner-only membership denial and Owner positive coverage.
- [x] T031 [P] Add or update `apps/platform/tests/Feature/Rbac/AdminPanelAccessBoundaryTest.php` to prove direct `/admin` workspace surfaces deny unauthenticated users, users without valid workspace authority, and wrong-plane actors.
- [x] T032 [P] Add or update `apps/platform/tests/Feature/Rbac/SystemPanelAccessBoundaryTest.php` or extend existing system tests to prove ordinary workspace users cannot access `/system` or representative `/system/*` routes.
## Phase 4: Tests First - Workspace And Environment Isolation
**Purpose**: Prove direct object access boundaries for representative high-risk resources.
- [x] T033 [P] Add or update `apps/platform/tests/Feature/Rbac/ManagedEnvironmentAccessBoundaryTest.php` for same-workspace wrong-environment denial.
- [x] T034 [P] Add or update EnvironmentReview direct access coverage in `apps/platform/tests/Feature/Rbac/ManagedEnvironmentAccessBoundaryTest.php` or an existing EnvironmentReview authorization test.
- [x] T035 [P] Add or update ReviewPack cross-workspace and wrong-environment coverage in `apps/platform/tests/Feature/Rbac/ReviewPackAccessBoundaryTest.php` or existing ReviewPack authorization tests.
- [x] T036 [P] Add or update EvidenceSnapshot or StoredReport wrong-environment coverage using repo-real policies/resources.
- [x] T037 [P] Add or update FindingException cross-workspace and wrong-environment coverage in `apps/platform/tests/Feature/Rbac/FindingExceptionLifecycleAccessBoundaryTest.php` or existing finding exception authorization tests.
- [x] T038 [P] Add or update OperationRun workspace-bound and environment-bound access coverage in `apps/platform/tests/Feature/Rbac/OperationRunAccessBoundaryTest.php` or existing OperationRun authorization tests.
- [x] T039 [P] Add or update ProviderConnection cross-workspace and wrong-environment coverage in `apps/platform/tests/Feature/Rbac/ProviderConnectionAccessBoundaryTest.php` or existing ProviderConnection authorization tests.
## Phase 5: Tests First - Sensitive Actions
**Purpose**: Prove server-side authorization for mutation/action execution, not only navigation visibility.
- [x] T040 Add membership-management direct action or policy tests proving non-Owner denial and last-owner guard preservation.
- [x] T041 Add ProviderConnection credential-level denial tests for Manager/Operator/Readonly where high-privilege-only classification is confirmed.
- [x] T042 Add Review/ReviewPack mutation denial tests for Readonly and any non-owner role classified as unauthorized.
- [x] T043 Add FindingException approval/rejection/renew/revoke lifecycle denial tests for unauthorized roles.
- [x] T044 Add OperationRun view/start/cancel/retry/action denial tests for unauthorized or out-of-scope roles using existing operation action surfaces.
- [x] T045 Ensure each destructive action changed by this spec is tested for server-side authorization and existing `->requiresConfirmation()` posture.
## Phase 6: Minimal Runtime Fixes
**Purpose**: Fix only confirmed contradictions and direct boundary bugs.
- [x] T046 Remove or adjust Manager `WORKSPACE_MEMBERSHIP_MANAGE` in `apps/platform/app/Services/Auth/WorkspaceRoleCapabilityMap.php` if Owner-only is confirmed.
- [x] T047 Remove or adjust Manager-specific `TENANT_MEMBERSHIP_MANAGE` append in `WorkspaceRoleCapabilityMap::getCapabilities()` if Owner-only is confirmed.
- [x] T048 Update `apps/platform/app/Models/User.php::canAccessPanel()` only if direct tests prove current permissive behavior is unsafe or can be tightened without breaking login/workspace selection.
- [x] T049 Add missing `Gate::authorize(...)` or policy checks in confirmed service/action mutation paths only. No missing mutation path was confirmed beyond the fixed role-map grants.
- [x] T050 Fix missing policy logic in ProviderConnection, ReviewPack, EnvironmentReview, EvidenceSnapshot, FindingException, or OperationRun policies only for confirmed direct-access bypasses. No direct-access policy bypass was confirmed.
- [x] T051 Align Filament action visibility/disabled state with server-side policy only after the policy/gate behavior is correct.
- [x] T052 Ensure any changed destructive Filament execution action still uses `->action(...)`, `->requiresConfirmation()`, and server-side authorization. No destructive action implementation changed.
- [x] T053 Do not introduce new roles, tables, migrations, capability aliases, broad resolvers, or UI redesigns.
## Phase 7: Focused Validation
**Purpose**: Run the smallest honest lanes for changed behavior.
- [x] T054 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Auth/WorkspaceRoleCapabilityMapTest.php`.
- [x] T055 Run focused RBAC boundary tests under `apps/platform/tests/Feature/Rbac/` that were added or updated for Spec 309.
- [x] T056 Run existing panel cross-plane tests if panel access changed: `apps/platform/tests/Feature/Auth/CrossScopeAccessTest.php`, `apps/platform/tests/Feature/System/Spec113/AuthorizationSemanticsTest.php`, and `apps/platform/tests/Feature/System/Spec114/SystemConsoleAccessSemanticsTest.php`.
- [x] T057 Run review/customer workspace regressions: `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php`, `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php`, `apps/platform/tests/Feature/ReviewPack/ReviewPackRbacTest.php`, and `apps/platform/tests/Feature/ReviewPack/ReviewPackDownloadTest.php`.
- [x] T058 Run provider connection authorization regressions if provider boundaries changed.
- [x] T059 Run OperationRun authorization/link regressions if OperationRun policy or links changed. No OperationRun policy/link code changed; existing OperationRun RBAC regression was still run.
- [x] T060 Run `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`.
- [x] T061 Run `git diff --check`.
- [x] T062 Run browser smoke only if Feature/Filament tests cannot prove a changed panel/action boundary; otherwise document why browser was not needed.
## Phase 8: Close-Out
**Purpose**: Leave implementation reviewers with exact security and product-truth decisions.
- [x] T063 Record final role/capability inventory in close-out notes.
- [x] T064 Record confirmed contradictions fixed.
- [x] T065 Record product decisions deferred.
- [x] T066 Record docs-only drift and follow-up candidates.
- [x] T067 Record tests run, tests not run, and why.
- [x] T068 Confirm no new RBAC model/table/capability framework/migration was introduced.
- [x] T069 Confirm Filament v5 / Livewire v4 compliance, provider registration location, global-search resource posture, destructive-action confirmation/authorization, no new asset strategy, and testing plan coverage.
- [x] T070 Confirm remaining risks and recommended next spec after 309.
## Dependencies & Execution Order
- Phase 1 blocks all later phases.
- Phase 2 blocks test writing and runtime edits.
- Phases 3, 4, and 5 can run partly in parallel once classification is complete, but each worker must own disjoint test files.
- Phase 6 starts only after tests for confirmed contradictions exist.
- Phase 7 starts after runtime fixes.
- Phase 8 closes after validation.
## Parallel Execution Examples
- T003-T015 can run in parallel as read-only inspection.
- T027-T032 can run in parallel with disjoint test files.
- T033-T039 can run in parallel with disjoint test files.
- T041-T044 can run in parallel when each action group owns separate policies/tests.
- T054-T059 can run in parallel if the Sail environment supports concurrent lanes; otherwise run serially.
## Implementation Strategy
1. MVP = role-map contradiction proof + `/admin` and `/system` direct boundary tests.
2. Add workspace/environment isolation proof for the representative resource set.
3. Add sensitive-action tests for the confirmed bug set.
4. Apply minimal fixes.
5. Validate focused lanes and close out with inventory and decisions.
## Deferred Follow-Ups / Non-Goals
- Support Access Governance v1.
- Product Truth / Docs Drift Reconciliation.
- Customer Review Workspace v1 Completion.
- Route / Panel Access Contract Audit beyond confirmed 309 bugs.
- Commercial entitlements or billing state.
- New RBAC management UI or permission matrix.
## Implementation Close-Out Notes
### RBAC Inventory
| Role | Capabilities | Sensitive? | Matches target? | Action |
|---|---|---:|---|---|
| Workspace Owner | Workspace membership manage, tenant membership manage, provider manage + dedicated credential manage, review/review-pack/evidence manage, finding exception approve, OperationRun view by capability | Yes | Yes | Kept. Owner positive tests added. |
| Workspace Manager | Workspace membership view, provider manage/run, review/review-pack/evidence manage, finding exception approve, workspace settings/manage, alerts/baselines/audit | Yes | Partially | Fixed confirmed membership-management contradiction. Provider/review/evidence/finding manage remains product-decision-needed because existing runtime and tests treat it as intended. |
| Workspace Operator | Workspace membership view, provider view/run, review/evidence view, findings triage, audit, allowed operation visibility | Medium/High | Yes | Kept and covered by boundary tests. |
| Workspace Readonly | Workspace/settings/alerts/baselines/audit view plus customer-safe tenant/review/evidence/provider/review-pack view where scoped | Medium | Yes | Kept and mutation denials covered. |
| Platform/System | Separate `platform` guard + `PlatformCapabilities`; no implicit admin/workspace access | Critical | Yes | Kept; `/system` direct tests added and cross-plane regressions passed. |
### Confirmed Contradictions Fixed
- Manager no longer receives `Capabilities::WORKSPACE_MEMBERSHIP_MANAGE` from `WorkspaceRoleCapabilityMap`.
- Manager no longer receives the Manager-only appended `Capabilities::TENANT_MEMBERSHIP_MANAGE` from `WorkspaceRoleCapabilityMap::getCapabilities()`.
- `User::canAccessPanel()` is now restricted to the `admin` panel; ordinary web users no longer advertise access to the `system` panel.
### Product Decisions Deferred
- Manager `PROVIDER_MANAGE`, `REVIEW_PACK_MANAGE`, `ENVIRONMENT_REVIEW_MANAGE`, `EVIDENCE_MANAGE`, and `FINDING_EXCEPTION_APPROVE` remain unchanged because repo-real policies and existing regressions treat them as current product behavior.
- Provider dedicated credential management remains high-privilege only through `PROVIDER_MANAGE_DEDICATED`; tests prove Manager/Operator/Readonly denial and Owner allowance.
- Support access governance, commercial lifecycle enforcement, and broader route/panel contract cleanup remain follow-up candidates.
### Validation Results
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Auth/WorkspaceRoleCapabilityMapTest.php tests/Unit/Auth/CapabilityResolverTest.php tests/Feature/Rbac/RoleMatrix/ManagerAccessTest.php tests/Feature/Rbac/WorkspaceRoleCapabilityBoundaryTest.php tests/Feature/Rbac/AdminPanelAccessBoundaryTest.php tests/Feature/Rbac/SystemPanelAccessBoundaryTest.php` — 17 passed.
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Rbac/ManagedEnvironmentAccessBoundaryTest.php tests/Feature/Rbac/ProviderConnectionAccessBoundaryTest.php tests/Feature/Rbac/ReviewPackAccessBoundaryTest.php tests/Feature/Rbac/OperationRunAccessBoundaryTest.php tests/Feature/Rbac/FindingExceptionLifecycleAccessBoundaryTest.php` — 12 passed.
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Auth/WorkspaceFirstManagedEnvironmentAccessTest.php tests/Feature/Rbac/WorkspaceMembershipsRelationManagerUiEnforcementTest.php tests/Feature/Auth/CrossScopeAccessTest.php tests/Feature/Auth/SystemPanelAuthTest.php` — 14 passed.
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/System/Spec113/AuthorizationSemanticsTest.php tests/Feature/System/Spec114/SystemConsoleAccessSemanticsTest.php` — 22 passed.
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php tests/Feature/ReviewPack/ReviewPackRbacTest.php tests/Feature/ReviewPack/ReviewPackDownloadTest.php` — 30 passed.
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Rbac/ProviderConnectionWorkspaceFirstPolicyTest.php tests/Feature/Rbac/OperationRunWorkspaceFirstAuthorizationTest.php tests/Feature/ProviderConnections/ManageCapabilityEnforcementTest.php tests/Feature/ProviderConnections/ProviderConnectionAuthorizationTest.php` — 13 passed.
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` — passed.
- `git diff --check` — passed.
Browser smoke was not run because the changed behavior is backend authorization, role maps, panel contract checks, and Filament/Livewire action enforcement proven by Feature/Livewire tests. No frontend assets, layouts, forms, or navigation redesign were introduced.
### Filament / Runtime Compliance
- Filament v5 remains on Livewire v4.1.4.
- Panel providers remain registered through `apps/platform/bootstrap/providers.php`.
- New/changed resources are not introduced; existing touched resources keep their current global-search posture.
- No destructive action implementation changed. Existing workspace membership destructive removal still uses an execution action with confirmation and the server-side workspace capability guard.
- No assets were added or changed; no `filament:assets` deploy impact.
- No new RBAC model, role, table, migration, capability alias, public permission framework, or UI redesign was introduced.