- Canonical /admin/onboarding entry point; legacy routes 404\n- Tenantless run viewer at /admin/operations/{run} with membership-based 404\n- RBAC UX (disabled controls + tooltips) and server-side 403\n- DB-only rendering/refresh; contract registry enforced\n- Adds migrations + tests + spec artifacts
4.2 KiB
4.2 KiB
Data Model — Managed Tenant Onboarding Wizard V1 (Enterprise) (073)
Entities
Workspace
Existing entity: App\Models\Workspace
- Onboarding is always initiated within a selected workspace context.
- Workspace membership is the primary isolation boundary for wizard + tenantless operations viewing.
Tenant (Managed Tenant)
Existing model: App\Models\Tenant
Key fields (existing or to extend):
id(PK)workspace_id(FK → workspaces)tenant_id(string; Entra Tenant ID) — spec’sentra_tenant_id(globally unique)external_id(string; Filament tenant route key; currently used in/admin/t/{tenant})name(string)primary_domain(string|null)notes(text|null)environment(string)status(string) — v1 lifecycle:draftonboardingactivearchived
Indexes / constraints (design intent):
- Unique:
tenant_id(global uniqueness; binds the tenant to exactly one workspace) external_idmust remain globally unique for Filament tenancy routing
State transitions:
draft→onboardingafter identification is recordedonboarding→activeon owner activationactive→archivedvia archive/deactivate workflow
Provider Connection
Existing model today: App\Models\ProviderConnection (currently tenant-owned)
Spec-aligned ownership model (design intent):
- Provider connections are workspace-owned.
- Default binding: provider connection bound to exactly one managed tenant.
- Reuse across managed tenants is disabled by default and policy-gated.
Proposed key fields (target):
id(PK)workspace_id(FK → workspaces)managed_tenant_id(FK → tenants.id; required in v1 default binding)provider(string)entra_tenant_id(string)is_default(bool)metadata(json)
Tenant Onboarding Session (new)
New model/table to persist resumable onboarding state for a workspace + Entra Tenant ID. Must never persist secrets and must render DB-only.
Proposed fields:
id(PK)workspace_id(FK)managed_tenant_id(FK → tenants.id; nullable until tenant is created)entra_tenant_id(string; denormalized identity key; globally unique across the system but still stored for idempotency)current_step(string;identify,connection,verify,bootstrap,complete)state(jsonb) — safe fields only (no secrets)tenant_nameenvironmentprimary_domainnotesselected_provider_connection_idverification_run_id(OperationRun id)bootstrap_run_ids(array)
started_by_user_id(FK users)updated_by_user_id(FK users)completed_at(timestamp|null)- timestamps
Constraints:
- Unique:
entra_tenant_id(global uniqueness) OR (if sessions are separate from tenants) unique(workspace_id, entra_tenant_id)with an additional global “tenant exists elsewhere” guard to enforce deny-as-not-found.
Operation Run
Existing model: App\Models\OperationRun
Spec-aligned visibility model (design intent):
- Runs are viewable tenantlessly at
/admin/operations/{run}. - Access is granted only to members of the run’s workspace; non-member → deny-as-not-found (404).
Proposed schema changes:
- Add
workspace_id(FK → workspaces), required. - Allow
tenant_idto be nullable for pre-activation runs. - Maintain DB-level active-run idempotency:
UNIQUE (tenant_id, run_identity_hash) WHERE tenant_id IS NOT NULL AND status IN ('queued', 'running')UNIQUE (workspace_id, run_identity_hash) WHERE tenant_id IS NULL AND status IN ('queued', 'running')
Validation rules (high level)
entra_tenant_id: required, non-empty, validate GUID format.- Tenant identification requires:
name,environment,entra_tenant_id. - Provider connection selected/created must be in the same workspace.
- Onboarding session
statemust be strictly whitelisted fields (no secrets).
Authorization boundaries
- Workspace membership boundary: non-member → 404 (deny-as-not-found) for onboarding and tenantless operations run viewing.
- Capability boundary (within membership): action attempts without capability → 403.
- Owner-only boundary: activation and blocked override require workspace owner; override requires reason + audit.