instance()->getTable()->getEmptyStateActions() as $action) { if ($action instanceof Action && $action->getName() === $name) { return $action; } } return null; } function createCustomerSafeReviewPackForRbac(ManagedEnvironment $tenant, \App\Models\User $user, string $filePath): ReviewPack { $snapshot = seedEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0); $review = composeEnvironmentReviewForTest($tenant, $user, $snapshot); $review->forceFill([ 'status' => 'published', 'published_at' => now(), 'published_by_user_id' => (int) $user->getKey(), ])->save(); $review = markEnvironmentReviewCustomerSafeReady($review); $pack = ReviewPack::factory()->ready()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'environment_review_id' => (int) $review->getKey(), 'evidence_snapshot_id' => (int) $snapshot->getKey(), 'initiated_by_user_id' => (int) $user->getKey(), 'options' => [ 'include_pii' => false, 'include_operations' => true, ], 'file_path' => $filePath, 'file_disk' => 'exports', 'file_size' => Storage::disk('exports')->size($filePath), 'sha256' => hash('sha256', Storage::disk('exports')->get($filePath)), 'expires_at' => now()->addDay(), ]); $review->forceFill([ 'current_export_review_pack_id' => (int) $pack->getKey(), ])->save(); return $pack->fresh(['tenant', 'environmentReview']); } // ─── Non-Member Access ─────────────────────────────────────── it('returns 404 for non-member on list page', function (): void { $targetTenant = ManagedEnvironment::factory()->create(); $otherTenant = ManagedEnvironment::factory()->create(); [$user] = createUserWithTenant($otherTenant, role: 'owner'); $this->actingAs($user) ->get(ReviewPackResource::getUrl('index', tenant: $targetTenant, panel: 'admin')) ->assertNotFound(); }); it('returns 404 for non-member on view page', function (): void { $targetTenant = ManagedEnvironment::factory()->create(); $otherTenant = ManagedEnvironment::factory()->create(); [$user] = createUserWithTenant($otherTenant, role: 'owner'); $pack = ReviewPack::factory()->ready()->create([ 'managed_environment_id' => (int) $targetTenant->getKey(), ]); $this->actingAs($user) ->get(ReviewPackResource::getUrl('view', ['record' => $pack], tenant: $targetTenant, panel: 'admin')) ->assertNotFound(); }); it('returns 404 for non-member on download route', function (): void { $targetTenant = ManagedEnvironment::factory()->create(); $otherTenant = ManagedEnvironment::factory()->create(); [$user] = createUserWithTenant($otherTenant, role: 'owner'); $filePath = 'review-packs/rbac-test.zip'; Storage::disk('exports')->put($filePath, 'PK-fake'); $pack = ReviewPack::factory()->ready()->create([ 'managed_environment_id' => (int) $targetTenant->getKey(), 'file_path' => $filePath, 'file_disk' => 'exports', ]); $signedUrl = app(ReviewPackService::class)->generateDownloadUrl($pack); $this->actingAs($user)->get($signedUrl)->assertNotFound(); }); // ─── REVIEW_PACK_VIEW Member ──────────────────────────────── it('allows readonly member to access list page', function (): void { $tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly'); $this->actingAs($user) ->get(ReviewPackResource::getUrl('index', tenant: $tenant, panel: 'admin')) ->assertOk(); }); it('allows readonly member to access view page', function (): void { $tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly'); $pack = ReviewPack::factory()->ready()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'initiated_by_user_id' => (int) $user->getKey(), ]); $this->actingAs($user) ->get(ReviewPackResource::getUrl('view', ['record' => $pack], tenant: $tenant, panel: 'admin')) ->assertOk(); }); it('allows readonly member to download via signed URL', function (): void { $tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly'); $filePath = 'review-packs/readonly-test.zip'; Storage::disk('exports')->put($filePath, 'PK-fake'); $pack = createCustomerSafeReviewPackForRbac($tenant, $user, $filePath); $signedUrl = app(ReviewPackService::class)->generateDownloadUrl($pack); $this->actingAs($user)->get($signedUrl)->assertOk(); }); it('disables generate action for readonly member', function (): void { $tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly'); $tenant->makeCurrent(); Filament::setTenant($tenant, true); $component = Livewire::actingAs($user) ->test(ListReviewPacks::class) ->assertTableEmptyStateActionsExistInOrder(['generate_first']); $emptyStateAction = getReviewPackRbacEmptyStateAction($component, 'generate_first'); expect($emptyStateAction)->not->toBeNull() ->and($emptyStateAction?->isDisabled())->toBeTrue() ->and($emptyStateAction?->getTooltip())->toBe(UiTooltips::insufficientPermission()); }); // ─── REVIEW_PACK_MANAGE Member ────────────────────────────── it('allows owner to generate a review pack', function (): void { $tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); ReviewPack::factory()->ready()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'initiated_by_user_id' => (int) $user->getKey(), ]); $tenant->makeCurrent(); Filament::setTenant($tenant, true); Livewire::actingAs($user) ->test(ListReviewPacks::class) ->assertActionVisible('generate_pack') ->assertActionEnabled('generate_pack'); }); it('allows owner to expire a ready pack', function (): void { $tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); $filePath = 'review-packs/expire-rbac.zip'; Storage::disk('exports')->put($filePath, 'PK-fake'); $pack = ReviewPack::factory()->ready()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'initiated_by_user_id' => (int) $user->getKey(), 'file_path' => $filePath, 'file_disk' => 'exports', ]); $tenant->makeCurrent(); Filament::setTenant($tenant, true); Livewire::actingAs($user) ->test(ListReviewPacks::class) ->assertTableActionExists('expire', fn (Action $action): bool => $action->isConfirmationRequired(), $pack) ->assertTableActionVisible('expire', $pack) ->callTableAction('expire', $pack); $pack->refresh(); $audit = AuditLog::query() ->where('action', AuditActionId::ReviewPackExpired->value) ->latest('id') ->first(); expect($pack->status)->toBe(ReviewPackStatus::Expired->value) ->and(Storage::disk('exports')->exists($filePath))->toBeFalse() ->and($audit)->not->toBeNull() ->and($audit?->resource_type)->toBe('review_pack') ->and(data_get($audit?->metadata, 'review_pack_id'))->toBe((int) $pack->getKey()) ->and(data_get($audit?->metadata, 'artifact_family'))->toBe('review_pack') ->and(data_get($audit?->metadata, 'before_status'))->toBe(ReviewPackStatus::Ready->value) ->and(data_get($audit?->metadata, 'after_status'))->toBe(ReviewPackStatus::Expired->value) ->and(data_get($audit?->metadata, 'file_deleted'))->toBeTrue(); }); it('Spec406 does not delete foreign-disk files when expiring a review pack', function (): void { $tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); Storage::fake('public'); $exportsPath = 'review-packs/exports-same-path.zip'; $publicPath = 'review-packs/foreign-disk.zip'; Storage::disk('exports')->put($exportsPath, 'PK-exports'); Storage::disk('public')->put($publicPath, 'PK-public'); $pack = ReviewPack::factory()->ready()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'initiated_by_user_id' => (int) $user->getKey(), 'file_path' => $publicPath, 'file_disk' => 'public', 'file_size' => Storage::disk('public')->size($publicPath), 'sha256' => hash('sha256', Storage::disk('public')->get($publicPath)), ]); $expiredPack = app(ReviewPackService::class)->expire($pack, $user, 'spec406_wrong_disk'); $audit = AuditLog::query() ->where('action', AuditActionId::ReviewPackExpired->value) ->latest('id') ->first(); expect($expiredPack->status)->toBe(ReviewPackStatus::Expired->value) ->and(Storage::disk('public')->exists($publicPath))->toBeTrue() ->and(Storage::disk('exports')->exists($exportsPath))->toBeTrue() ->and($audit)->not->toBeNull() ->and(data_get($audit?->metadata, 'file_disk'))->toBe('public') ->and(data_get($audit?->metadata, 'file_deleted'))->toBeFalse(); }); it('disables expire action for readonly member', function (): void { $tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly'); $filePath = 'review-packs/expire-readonly.zip'; Storage::disk('exports')->put($filePath, 'PK-fake'); $pack = ReviewPack::factory()->ready()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'file_path' => $filePath, 'file_disk' => 'exports', ]); $tenant->makeCurrent(); Filament::setTenant($tenant, true); Livewire::actingAs($user) ->test(ListReviewPacks::class) ->assertTableActionVisible('expire', $pack) ->assertTableActionDisabled('expire', $pack); }); it('denies direct review pack lifecycle expiration for readonly members', function (): void { $tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly'); $filePath = 'review-packs/expire-direct-readonly.zip'; Storage::disk('exports')->put($filePath, 'PK-fake'); $pack = ReviewPack::factory()->ready()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'file_path' => $filePath, 'file_disk' => 'exports', 'file_size' => Storage::disk('exports')->size($filePath), 'sha256' => hash('sha256', Storage::disk('exports')->get($filePath)), ]); expect(fn (): ReviewPack => app(ReviewPackService::class)->expire($pack, $user, 'spec406_direct')) ->toThrow(AuthorizationException::class); $pack->refresh(); expect($pack->status)->toBe(ReviewPackStatus::Ready->value) ->and(Storage::disk('exports')->exists($filePath))->toBeTrue() ->and(AuditLog::query()->where('action', AuditActionId::ReviewPackExpired->value)->count())->toBe(0); }); // ─── Signed URL Security ──────────────────────────────────── it('rejects unsigned download URL with 403', function (): void { $tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); $pack = ReviewPack::factory()->ready()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, ]); $response = $this->actingAs($user) ->get(route('admin.review-packs.download', ['reviewPack' => $pack->getKey()])); $response->assertForbidden(); });