# 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’s `entra_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: - `draft` - `onboarding` - `active` - `archived` **Indexes / constraints (design intent):** - Unique: `tenant_id` (global uniqueness; binds the tenant to exactly one workspace) - `external_id` must remain globally unique for Filament tenancy routing **State transitions:** - `draft` → `onboarding` after identification is recorded - `onboarding` → `active` on owner activation - `active` → `archived` via 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_name` - `environment` - `primary_domain` - `notes` - `selected_provider_connection_id` - `verification_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_id` to 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 `state` must 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.