TenantAtlas/specs/069-managed-tenant-onboarding-wizard/data-model.md
2026-02-01 12:20:09 +01:00

88 lines
3.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Data Model — Managed Tenant Onboarding Wizard v1
This design is aligned to current repo reality where the “managed tenant” is the existing `Tenant` model.
## Entity: Tenant (`App\\Models\\Tenant`)
### Relevant existing fields
- `id` (PK)
- `name` (display name)
- `tenant_id` (Entra tenant GUID; used as canonical external id)
- `external_id` (route key; kept in sync with `tenant_id` when present)
- `domain` (optional)
- `environment` (`prod|dev|staging|other`)
- `app_client_id` (optional)
- `app_client_secret` (encrypted cast; must never be displayed back to the user)
- RBAC health / verification storage:
- `rbac_last_checked_at` (datetime)
- `rbac_last_setup_at` (datetime)
- `rbac_canary_results` (array)
- `rbac_last_warnings` (array)
### New fields (proposed)
If onboarding needs to be explicitly tracked on the tenant record:
- `onboarding_status` enum-like string: `not_started|in_progress|completed` (default: `not_started`)
- `onboarding_completed_at` nullable datetime
Rationale: makes it cheap to render “Resume wizard” / completion status without loading session records.
## Entity: TenantOnboardingSession (new)
### Table name (proposed)
- `tenant_onboarding_sessions`
### Columns
- `id` (PK)
- `tenant_id` nullable FK → `tenants.id`
- nullable at the very beginning if the user hasnt provided a valid tenant GUID yet
- `created_by_user_id` FK → `users.id`
- `status` string: `active|completed|abandoned`
- `current_step` string: `welcome|tenant_details|credentials|permissions|verification`
- `payload` jsonb
- contains non-secret form state only (e.g., name, tenant_id, domain, environment)
- MUST NOT contain secrets
- `last_error_code` nullable string
- `last_error_message` nullable string (sanitized; no tokens/secrets)
- `completed_at` nullable datetime
- `abandoned_at` nullable datetime
- `created_at`, `updated_at`
### Indexes and constraints
- Ensure at most one active session per tenant:
- PostgreSQL partial unique index: `(tenant_id)` where `status = 'active'`
- Dedupe/resume lookup:
- index `(created_by_user_id, status)`
- index `(tenant_id, status)`
### State transitions
- `active``completed` when:
- tenant record exists
- credentials requirement (if enabled) is satisfied
- last verification run indicates success
- `active``abandoned` when user explicitly cancels
## Entity: OperationRun (`App\\Models\\OperationRun`)
Wizard-triggered checks must be observable via `OperationRun`.
### Relevant fields
- `tenant_id` FK
- `type` string (examples already in repo: `provider.connection.check`, `inventory.sync`, `compliance.snapshot`)
- `status` / `outcome`
- `run_identity_hash` (dedupe identity)
- `context` (json)
### Idempotency
Use `OperationRunService::ensureRun()` / `ensureRunWithIdentity()` to get DB-level active-run dedupe.
## Capability / Authorization model
- Capabilities are strings from the canonical registry `App\\Support\\Auth\\Capabilities`.
- Capability checks:
- Membership: `CapabilityResolver::isMember()`
- Capability: `CapabilityResolver::can()`
- Tenant-scoped non-member access is denied-as-not-found (404) by `DenyNonMemberTenantAccess` middleware.
- Filament actions use `App\\Support\\Rbac\\UiEnforcement` to apply:
- hidden UI for non-members
- disabled UI + tooltip for members lacking the capability
- server-side 404/403 guardrails