88 lines
3.3 KiB
Markdown
88 lines
3.3 KiB
Markdown
# 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 hasn’t 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
|