- 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
68 lines
4.6 KiB
Markdown
68 lines
4.6 KiB
Markdown
# Research — Managed Tenant Onboarding Wizard V1 (Enterprise) (073)
|
||
|
||
This document resolves planning unknowns and records key implementation decisions aligned with the clarified spec.
|
||
|
||
## Decisions
|
||
|
||
### 1) Managed Tenant model = existing `Tenant`
|
||
|
||
- **Decision:** Treat `App\Models\Tenant` as the “Managed Tenant” record.
|
||
- **Rationale:** Filament tenancy, membership model, and tenant-scoped flows already depend on `Tenant`; duplicating a second tenant-like table would multiply authorization and routing complexity.
|
||
- **Alternatives considered:** Introduce a new `ManagedTenant` model/table.
|
||
- **Why rejected:** Duplicates tenancy and membership boundaries; increases cross-plane leak risk.
|
||
|
||
### 2) Entra Tenant ID uniqueness = global, bound to one workspace
|
||
|
||
- **Decision:** Enforce global uniqueness for `tenants.tenant_id` (Entra Tenant ID) and bind it to exactly one workspace (the workspace_id on the tenant).
|
||
- **Rationale:** Matches FR-011 and the clarification decision (“global uniqueness bound to one workspace”).
|
||
- **Alternatives considered:** Allow the same Entra Tenant ID in multiple workspaces.
|
||
- **Why rejected:** Violates the clarified requirement and complicates deny-as-not-found behavior.
|
||
|
||
### 3) Canonical onboarding entry point = `/admin/onboarding` (only)
|
||
|
||
- **Decision:** Provide `/admin/onboarding` as the sole onboarding entry point.
|
||
- **Rationale:** Keeps a single user-facing URL for enterprise workflows; avoids fragmented legacy entry points.
|
||
- **Alternatives considered:** Workspace-scoped onboarding route (`/admin/w/{workspace}/...`).
|
||
- **Why rejected:** Conflicts with clarified spec (canonical `/admin/onboarding` only).
|
||
|
||
### 4) Tenantless operations viewer = existing `OperationRunResource` route `/admin/operations/{run}`
|
||
|
||
- **Decision:** Keep the route shape `/admin/operations/{run}` (already provided by `OperationRunResource` slug `operations`) and make it compliant by changing authorization + middleware behavior.
|
||
- **Rationale:** Minimizes routing surface area and leverages existing Monitoring → Operations UI.
|
||
- **Alternatives considered:** Create a separate “run viewer” page outside the resource.
|
||
- **Why rejected:** Duplicates infolist rendering and complicates observability conventions.
|
||
|
||
### 5) `/admin/operations/{run}` must not require selected workspace or auto-switch
|
||
|
||
- **Decision:** Exempt `/admin/operations/{run}` from forced workspace selection and from any “auto selection” side effects that would prevent tenantless viewing.
|
||
- **Rationale:** Spec requires (a) no workspace in the URL, (b) no pre-selected workspace required, (c) no auto-switching.
|
||
- **Alternatives considered:** Keep current `EnsureWorkspaceSelected` behavior (redirect to choose workspace).
|
||
- **Why rejected:** Violates FR-017a and can leak resource existence via redirects.
|
||
|
||
### 6) OperationRun authorization = workspace membership (non-member → 404)
|
||
|
||
- **Decision:** Authorize viewing a run by checking membership in the run’s workspace; non-member gets deny-as-not-found (404).
|
||
- **Rationale:** FR-017a defines access semantics; runs must be viewable tenantlessly before activation.
|
||
- **Alternatives considered:** Authorize by `Tenant::current()` + matching `run.tenant_id`.
|
||
- **Why rejected:** Requires tenant routing/selection and breaks tenantless viewing.
|
||
|
||
### 7) OperationRun schema = add `workspace_id`, allow tenantless runs, preserve idempotency
|
||
|
||
- **Decision:** Add `operation_runs.workspace_id` (FK) and allow `tenant_id` to be nullable for pre-activation operations. Preserve DB-level dedupe using two partial unique indexes:
|
||
- Tenant-bound runs: `UNIQUE (tenant_id, run_identity_hash) WHERE tenant_id IS NOT NULL AND status IN ('queued', 'running')`
|
||
- Tenantless runs: `UNIQUE (workspace_id, run_identity_hash) WHERE tenant_id IS NULL AND status IN ('queued', 'running')`
|
||
- **Rationale:** Enables tenantless operations while preserving race-safe idempotency guarantees.
|
||
- **Alternatives considered:** Keep `tenant_id` required and always derive workspace via join.
|
||
- **Why rejected:** Blocks tenantless flows and makes authorization join-dependent.
|
||
|
||
### 8) Provider connection ownership = workspace-owned, default 1:1 binding
|
||
|
||
- **Decision:** Align Provider Connections to be workspace-owned and (by default) bound to exactly one managed tenant; reuse is disabled by default and policy-gated.
|
||
- **Rationale:** Matches FR-022/022a/022b and reduces blast radius of credential reuse.
|
||
- **Alternatives considered:** Keep provider connections tenant-owned.
|
||
- **Why rejected:** Conflicts with clarified spec ownership model.
|
||
|
||
## Open Questions
|
||
|
||
- None for planning; implementation will need to reconcile existing DB schema and policies with the decisions above.
|