Implements Spec 089: moves Provider Connections to canonical tenantless route under `/admin/provider-connections`, enforces 404/403 semantics (workspace/tenant membership vs capability), adds tenant transparency (tenant column + filter + deep links), adds legacy redirects for old tenant-scoped URLs without leaking Location for 404 cases, and adds regression test coverage (RBAC semantics, filters, UI enforcement tooltips, Microsoft-only MVP scope, navigation placement). Notes: - Filament v5 / Livewire v4 compatible. - Global search remains disabled for Provider Connections. - Destructive/manage actions require confirmation and are policy-gated. Tests: - `vendor/bin/sail artisan test --compact tests/Feature/ProviderConnections` Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #107
118 lines
3.6 KiB
Markdown
118 lines
3.6 KiB
Markdown
# Phase 1 Design — Data Model (Provider Connections)
|
|
|
|
**Feature**: Provider Connections (tenantless UI + tenant transparency)
|
|
|
|
This feature primarily re-frames existing data under a canonical tenantless UI while enforcing workspace/tenant isolation.
|
|
|
|
## Entities
|
|
|
|
### Workspace
|
|
|
|
Represents the top-level isolation boundary. Users must be workspace members to access any Provider Connections surface.
|
|
|
|
**Key relationships**
|
|
- Workspace has many Tenants.
|
|
- Workspace has many ProviderConnections.
|
|
|
|
### Tenant
|
|
|
|
A managed tenant inside a Workspace.
|
|
|
|
**Key relationships**
|
|
- Tenant belongs to Workspace (`tenants.workspace_id`).
|
|
- Tenant has many ProviderConnections (`provider_connections.tenant_id`).
|
|
|
|
### ProviderConnection
|
|
|
|
An integration record owned by exactly one Tenant (and by extension, one Workspace).
|
|
|
|
**Storage**: `provider_connections` (PostgreSQL)
|
|
|
|
**Key columns (existing schema)**
|
|
- `workspace_id` (FK-ish, scoped to Workspace)
|
|
- `tenant_id` (FK to Tenants)
|
|
- `provider` (string; MVP: `microsoft`)
|
|
- `entra_tenant_id` (string)
|
|
- `display_name` (string)
|
|
- `is_default` (bool)
|
|
- `status` (string)
|
|
- `health_status` (string)
|
|
- `scopes_granted` (jsonb)
|
|
- `last_health_check_at` (timestamp)
|
|
- `last_error_reason_code` (string|null)
|
|
- `last_error_message` (string|null; sanitized/truncated)
|
|
- `metadata` (jsonb)
|
|
|
|
**Constraints / indexes (existing)**
|
|
- Unique: `(tenant_id, provider, entra_tenant_id)`
|
|
- Partial unique: one default per `(tenant_id, provider)` where `is_default = true`
|
|
|
|
**Relationships**
|
|
- ProviderConnection belongs to Tenant.
|
|
- ProviderConnection belongs to Workspace.
|
|
|
|
**State / badges**
|
|
- `status`: expected to map through BadgeCatalog (domain: ProviderConnectionStatus)
|
|
- `health_status`: expected to map through BadgeCatalog (domain: ProviderConnectionHealth)
|
|
|
|
### TenantMembership (isolation boundary)
|
|
|
|
A user-to-tenant entitlement table (exact schema is implementation-defined in the app; referenced by existing auth services).
|
|
|
|
**Purpose**
|
|
- Drives query-time scoping (JOIN-based) for list views.
|
|
- Drives deny-as-not-found semantics for direct record access.
|
|
|
|
### WorkspaceMembership (isolation boundary)
|
|
|
|
A user-to-workspace entitlement table.
|
|
|
|
**Purpose**
|
|
- Gates the entire feature with 404 semantics for non-members.
|
|
|
|
### AuditLog (governance)
|
|
|
|
Used for state-changing actions (manage actions).
|
|
|
|
**Requirements**
|
|
- Stable action IDs.
|
|
- Redacted/sanitized payloads (no secrets).
|
|
|
|
### OperationRun (observability)
|
|
|
|
Used for run/health actions.
|
|
|
|
**Requirements**
|
|
- Canonical “view run” link.
|
|
- Reason codes + sanitized messages.
|
|
|
|
## Query Scoping (JOIN-based)
|
|
|
|
**Goal**: No metadata leaks and MSP-scale performance.
|
|
|
|
List and global data pulls MUST:
|
|
- Start from `provider_connections`.
|
|
- JOIN to tenants and membership tables to enforce entitlement.
|
|
- Optionally apply a tenant filter based on:
|
|
1) `tenant_id` querystring (takes precedence)
|
|
2) otherwise, active TenantContext-derived default
|
|
|
|
Direct record access MUST:
|
|
- Resolve the owning tenant from the record.
|
|
- If user is not entitled to that tenant (or not a workspace member), respond 404.
|
|
- If entitled but lacking capability, respond 403.
|
|
|
|
## Validation Rules (UI-level)
|
|
|
|
- `provider` must be one of allowed providers (MVP: `microsoft`).
|
|
- `entra_tenant_id` must be present and formatted as expected (string).
|
|
- `display_name` required.
|
|
- Actions that toggle `is_default` must preserve the partial unique invariant.
|
|
- Any stored error messages must be sanitized and truncated.
|
|
|
|
## Data Minimization / Secrets
|
|
|
|
- No plaintext secrets are ever rendered.
|
|
- No “copy secret” affordances.
|
|
- Non-secret identifiers (e.g., Entra tenant ID) may be copyable.
|