TenantAtlas/app/Support/Audit/AuditActionId.php
ahmido 98e2b5acd9 feat: managed tenant onboarding draft identity and resume semantics (#167)
## Summary
- add canonical managed-tenant onboarding draft routing with explicit draft identity and landing vs concrete draft behavior
- implement draft lifecycle, authorization, attribution, picker UX, resume-stage resolution, and auditable cancel or completion semantics
- add focused feature, unit, and browser coverage plus Spec 138 artifacts for the onboarding draft resume flow

## Validation
- `vendor/bin/sail artisan test --compact tests/Feature/ManagedTenantOnboardingWizardTest.php tests/Feature/Audit/OnboardingDraftAuditTest.php tests/Feature/Onboarding/OnboardingDraftAccessTest.php tests/Feature/Onboarding/OnboardingDraftAuthorizationTest.php tests/Feature/Onboarding/OnboardingDraftLifecycleTest.php tests/Feature/Onboarding/OnboardingDraftMultiTabTest.php tests/Feature/Onboarding/OnboardingDraftPickerTest.php tests/Feature/Onboarding/OnboardingDraftRoutingTest.php tests/Feature/Onboarding/OnboardingRbacSemanticsTest.php tests/Feature/Onboarding/OnboardingVerificationClustersTest.php tests/Feature/Onboarding/OnboardingVerificationTest.php tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php tests/Feature/Verification/VerificationReportViewerDbOnlyTest.php tests/Unit/Onboarding tests/Unit/VerificationReportSanitizerEvidenceKindsTest.php tests/Browser/OnboardingDraftRefreshTest.php tests/Browser/OnboardingDraftVerificationResumeTest.php`
- passed: 69 tests, 251 assertions

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #167
2026-03-13 23:45:23 +00:00

249 lines
14 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\Audit;
enum AuditActionId: string
{
case WorkspaceMembershipAdd = 'workspace_membership.add';
case WorkspaceMembershipRoleChange = 'workspace_membership.role_change';
case WorkspaceMembershipRemove = 'workspace_membership.remove';
case WorkspaceMembershipLastOwnerBlocked = 'workspace_membership.last_owner_blocked';
case WorkspaceMembershipBreakGlassAssignOwner = 'workspace_membership.break_glass.assign_owner';
case TenantMembershipAdd = 'tenant_membership.add';
case TenantMembershipRoleChange = 'tenant_membership.role_change';
case TenantMembershipRemove = 'tenant_membership.remove';
case TenantMembershipLastOwnerBlocked = 'tenant_membership.last_owner_blocked';
// Not part of the v1 contract, but used in codebase.
case TenantMembershipBootstrapRecover = 'tenant_membership.bootstrap_recover';
// Diagnostics / repair actions.
case TenantMembershipDuplicatesMerged = 'tenant_membership.duplicates_merged';
// Managed tenant onboarding wizard.
case ManagedTenantOnboardingStart = 'managed_tenant_onboarding.start';
case ManagedTenantOnboardingResume = 'managed_tenant_onboarding.resume';
case ManagedTenantOnboardingDraftSelected = 'managed_tenant_onboarding.draft_selected';
case ManagedTenantOnboardingDraftUpdated = 'managed_tenant_onboarding.draft_updated';
case ManagedTenantOnboardingProviderConnectionChanged = 'managed_tenant_onboarding.provider_connection_changed';
case ManagedTenantOnboardingVerificationStart = 'managed_tenant_onboarding.verification_start';
case ManagedTenantOnboardingVerificationPersisted = 'managed_tenant_onboarding.verification_persisted';
case ManagedTenantOnboardingBootstrapStarted = 'managed_tenant_onboarding.bootstrap_started';
case ManagedTenantOnboardingCancelled = 'managed_tenant_onboarding.cancelled';
case ManagedTenantOnboardingActivationOverrideUsed = 'managed_tenant_onboarding.activation_override_used';
case ManagedTenantOnboardingActivation = 'managed_tenant_onboarding.activation';
case VerificationCompleted = 'verification.completed';
case VerificationCheckAcknowledged = 'verification.check_acknowledged';
case AlertDestinationCreated = 'alert_destination.created';
case AlertDestinationUpdated = 'alert_destination.updated';
case AlertDestinationDeleted = 'alert_destination.deleted';
case AlertDestinationEnabled = 'alert_destination.enabled';
case AlertDestinationDisabled = 'alert_destination.disabled';
case AlertDestinationTestRequested = 'alert_destination.test_requested';
case AlertRuleCreated = 'alert_rule.created';
case AlertRuleUpdated = 'alert_rule.updated';
case AlertRuleDeleted = 'alert_rule.deleted';
case AlertRuleEnabled = 'alert_rule.enabled';
case AlertRuleDisabled = 'alert_rule.disabled';
case WorkspaceSettingUpdated = 'workspace_setting.updated';
case WorkspaceSettingReset = 'workspace_setting.reset';
case BaselineProfileCreated = 'baseline_profile.created';
case BaselineProfileUpdated = 'baseline_profile.updated';
case BaselineProfileArchived = 'baseline_profile.archived';
case BaselineCaptureStarted = 'baseline_capture.started';
case BaselineCaptureCompleted = 'baseline_capture.completed';
case BaselineCaptureFailed = 'baseline_capture.failed';
case BaselineCompareStarted = 'baseline_compare.started';
case BaselineCompareCompleted = 'baseline_compare.completed';
case BaselineCompareFailed = 'baseline_compare.failed';
case BaselineAssignmentCreated = 'baseline_assignment.created';
case BaselineAssignmentUpdated = 'baseline_assignment.updated';
case BaselineAssignmentDeleted = 'baseline_assignment.deleted';
// Workspace selection / switch events (Spec 107).
case WorkspaceAutoSelected = 'workspace.auto_selected';
case WorkspaceSelected = 'workspace.selected';
/**
* @return array<string>
*/
public static function knownValues(): array
{
return array_map(
static fn (self $case): string => $case->value,
self::cases(),
);
}
public static function labelFor(string|self $action): string
{
$value = $action instanceof self ? $action->value : trim($action);
return self::labels()[$value] ?? self::humanize($value);
}
/**
* @param array<string, mixed> $context
*/
public static function summaryFor(
string|self $action,
?string $targetLabel = null,
?string $targetType = null,
array $context = [],
): string {
$value = $action instanceof self ? $action->value : trim($action);
$summary = self::summaries()[$value] ?? self::labelFor($value);
if ($targetLabel !== null && $targetLabel !== '' && ! str_contains($summary, $targetLabel)) {
$summary .= ' for '.$targetLabel;
} elseif (($targetLabel === null || $targetLabel === '') && filled($targetType)) {
$summary .= ' for '.self::humanize((string) $targetType);
}
if (($context['after_status'] ?? null) !== null && ($context['before_status'] ?? null) !== null && ! str_contains($summary, 'status')) {
return sprintf(
'%s (%s -> %s)',
$summary,
(string) $context['before_status'],
(string) $context['after_status'],
);
}
return $summary;
}
/**
* @return array<string, string>
*/
private static function labels(): array
{
return [
self::WorkspaceMembershipAdd->value => 'Workspace member add',
self::WorkspaceMembershipRoleChange->value => 'Workspace member role change',
self::WorkspaceMembershipRemove->value => 'Workspace member removal',
self::WorkspaceMembershipLastOwnerBlocked->value => 'Workspace last-owner protection',
self::TenantMembershipAdd->value => 'Tenant member add',
self::TenantMembershipRoleChange->value => 'Tenant member role change',
self::TenantMembershipRemove->value => 'Tenant member removal',
self::TenantMembershipLastOwnerBlocked->value => 'Tenant last-owner protection',
self::ManagedTenantOnboardingStart->value => 'Managed tenant onboarding start',
self::ManagedTenantOnboardingResume->value => 'Managed tenant onboarding resume',
self::ManagedTenantOnboardingDraftSelected->value => 'Managed tenant onboarding draft selected',
self::ManagedTenantOnboardingDraftUpdated->value => 'Managed tenant onboarding draft updated',
self::ManagedTenantOnboardingProviderConnectionChanged->value => 'Managed tenant onboarding provider connection changed',
self::ManagedTenantOnboardingVerificationStart->value => 'Managed tenant onboarding verification start',
self::ManagedTenantOnboardingVerificationPersisted->value => 'Managed tenant onboarding verification persisted',
self::ManagedTenantOnboardingBootstrapStarted->value => 'Managed tenant onboarding bootstrap started',
self::ManagedTenantOnboardingCancelled->value => 'Managed tenant onboarding cancelled',
self::ManagedTenantOnboardingActivationOverrideUsed->value => 'Managed tenant onboarding activation override used',
self::ManagedTenantOnboardingActivation->value => 'Managed tenant onboarding activation',
self::VerificationCompleted->value => 'Verification completed',
self::VerificationCheckAcknowledged->value => 'Verification check acknowledged',
self::AlertDestinationCreated->value => 'Alert destination created',
self::AlertDestinationUpdated->value => 'Alert destination updated',
self::AlertDestinationDeleted->value => 'Alert destination deleted',
self::AlertDestinationEnabled->value => 'Alert destination enabled',
self::AlertDestinationDisabled->value => 'Alert destination disabled',
self::AlertDestinationTestRequested->value => 'Alert destination test requested',
self::AlertRuleCreated->value => 'Alert rule created',
self::AlertRuleUpdated->value => 'Alert rule updated',
self::AlertRuleDeleted->value => 'Alert rule deleted',
self::AlertRuleEnabled->value => 'Alert rule enabled',
self::AlertRuleDisabled->value => 'Alert rule disabled',
self::WorkspaceSettingUpdated->value => 'Workspace setting updated',
self::WorkspaceSettingReset->value => 'Workspace setting reset',
self::BaselineProfileCreated->value => 'Baseline profile created',
self::BaselineProfileUpdated->value => 'Baseline profile updated',
self::BaselineProfileArchived->value => 'Baseline profile archived',
self::BaselineCaptureStarted->value => 'Baseline capture started',
self::BaselineCaptureCompleted->value => 'Baseline capture completed',
self::BaselineCaptureFailed->value => 'Baseline capture failed',
self::BaselineCompareStarted->value => 'Baseline compare started',
self::BaselineCompareCompleted->value => 'Baseline compare completed',
self::BaselineCompareFailed->value => 'Baseline compare failed',
self::BaselineAssignmentCreated->value => 'Baseline assignment created',
self::BaselineAssignmentUpdated->value => 'Baseline assignment updated',
self::BaselineAssignmentDeleted->value => 'Baseline assignment deleted',
self::WorkspaceAutoSelected->value => 'Workspace auto-selected',
self::WorkspaceSelected->value => 'Workspace selected',
'finding.triaged' => 'Finding triaged',
'finding.in_progress' => 'Finding moved to in progress',
'finding.assigned' => 'Finding assignment updated',
'finding.resolved' => 'Finding resolved',
'finding.closed' => 'Finding closed',
'finding.risk_accepted' => 'Finding risk accepted',
'finding.reopened' => 'Finding reopened',
'baseline.capture.started' => 'Baseline capture started',
'baseline.capture.completed' => 'Baseline capture completed',
'baseline.capture.failed' => 'Baseline capture failed',
'baseline.compare.started' => 'Baseline compare started',
'baseline.compare.completed' => 'Baseline compare completed',
'baseline.compare.failed' => 'Baseline compare failed',
'baseline.evidence.resume.started' => 'Baseline evidence capture resumed',
'backup.created' => 'Backup set created',
'backup.updated' => 'Backup set updated',
'backup.archived' => 'Backup set archived',
'backup.items_added' => 'Backup set items added',
'backup.assignments.included' => 'Backup set assignments included',
'backup_schedule.run_started' => 'Backup schedule run started',
'backup_schedule.run_finished' => 'Backup schedule run finished',
'backup_schedule.run_failed' => 'Backup schedule run failed',
'backup_schedule.run_skipped' => 'Backup schedule run skipped',
'backup_schedule.retention_applied' => 'Backup schedule retention applied',
'restore.started' => 'Restore started',
'restore.previewed' => 'Restore preview completed',
'restore.executed' => 'Restore executed',
'restore.failed' => 'Restore failed',
'restore.assignments.summary' => 'Restore assignment summary recorded',
'restore.group_mapping.applied' => 'Restore group mapping applied',
'operation.completed' => 'Operation completed',
'operation.failed' => 'Operation failed',
'operation.partial' => 'Operation partially completed',
'operation.blocked' => 'Operation blocked',
];
}
/**
* @return array<string, string>
*/
private static function summaries(): array
{
return [
self::WorkspaceMembershipAdd->value => 'Workspace member added',
self::WorkspaceMembershipRoleChange->value => 'Workspace member role changed',
self::WorkspaceMembershipRemove->value => 'Workspace member removed',
self::WorkspaceMembershipLastOwnerBlocked->value => 'Workspace last-owner protection triggered',
self::TenantMembershipAdd->value => 'Tenant member added',
self::TenantMembershipRoleChange->value => 'Tenant member role changed',
self::TenantMembershipRemove->value => 'Tenant member removed',
self::TenantMembershipLastOwnerBlocked->value => 'Tenant last-owner protection triggered',
self::WorkspaceSettingUpdated->value => 'Workspace setting updated',
self::WorkspaceSettingReset->value => 'Workspace setting reset',
self::BaselineProfileCreated->value => 'Baseline profile created',
self::BaselineProfileUpdated->value => 'Baseline profile updated',
self::BaselineProfileArchived->value => 'Baseline profile archived',
self::AlertDestinationCreated->value => 'Alert destination created',
self::AlertDestinationUpdated->value => 'Alert destination updated',
self::AlertDestinationDeleted->value => 'Alert destination deleted',
self::AlertRuleCreated->value => 'Alert rule created',
self::AlertRuleUpdated->value => 'Alert rule updated',
self::AlertRuleDeleted->value => 'Alert rule deleted',
];
}
private static function humanize(string $value): string
{
$normalized = str_replace(['.', '_', '-'], ' ', trim($value));
$normalized = preg_replace('/\s+/', ' ', $normalized) ?? $normalized;
return ucfirst($normalized);
}
}