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

3.3 KiB
Raw Blame History

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

  • activecompleted when:
    • tenant record exists
    • credentials requirement (if enabled) is satisfied
    • last verification run indicates success
  • activeabandoned 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