Implements Spec 081 provider-connection cutover. Highlights: - Adds provider connection resolution + gating for operations/verification. - Adds provider credential observer wiring. - Updates Filament tenant verify flow to block with next-steps when provider connection isn’t ready. - Adds spec docs under specs/081-provider-connection-cutover/ and extensive Spec081 test coverage. Tests: - vendor/bin/sail artisan test --compact tests/Feature/Filament/TenantSetupTest.php - Focused suites for ProviderConnections/Verification ran during implementation (see local logs). Co-authored-by: Ahmed Darrazi <ahmeddarrazi@MacBookPro.fritz.box> Reviewed-on: #98
140 lines
4.0 KiB
Markdown
140 lines
4.0 KiB
Markdown
# Data Model: Provider Connection Full Cutover
|
||
|
||
**Feature**: [specs/081-provider-connection-cutover/spec.md](spec.md)
|
||
**Date**: 2026-02-07
|
||
|
||
This document describes the entities involved in Spec 081 using the repo’s current schema.
|
||
|
||
## Entities
|
||
|
||
### Tenant
|
||
|
||
**Represents**: A managed tenant target (Entra/Intune tenant) within a workspace.
|
||
|
||
**Relevant attributes (existing)**
|
||
|
||
- `id` (PK)
|
||
- `workspace_id` (FK)
|
||
- `name`
|
||
- `tenant_id` (GUID-ish, used as Entra tenant ID)
|
||
- `external_id` (alternate tenant identifier)
|
||
- Legacy (deprecated by this spec):
|
||
- `app_client_id`
|
||
- `app_client_secret` (encrypted)
|
||
- `app_certificate_thumbprint`
|
||
- `app_notes`
|
||
|
||
**Derived helpers (existing)**
|
||
|
||
- `graphTenantId(): ?string` returns `tenant_id` or `external_id`.
|
||
- `graphOptions(): array{tenant:?string,client_id:?string,client_secret:?string}` (to be deprecated/unused at runtime).
|
||
|
||
**Relationships (existing)**
|
||
|
||
- `providerConnections(): HasMany` → ProviderConnection
|
||
- `providerCredentials(): HasManyThrough` → ProviderCredential (via ProviderConnection)
|
||
- `operationRuns(): HasMany` (implicit via OperationRun.tenant_id)
|
||
|
||
### ProviderConnection
|
||
|
||
**Represents**: A workspace-owned integration connection for a tenant + provider (e.g., Microsoft).
|
||
|
||
**Table**: `provider_connections`
|
||
|
||
**Fields (existing)**
|
||
|
||
- `id` (PK)
|
||
- `workspace_id` (FK, NOT NULL after migration)
|
||
- `tenant_id` (FK)
|
||
- `provider` (string, e.g. `microsoft`)
|
||
- `entra_tenant_id` (string; expected to match the tenant’s Entra GUID)
|
||
- `display_name` (string)
|
||
- `is_default` (bool)
|
||
- `status` (string; default `needs_consent`)
|
||
- `health_status` (string; default `unknown`)
|
||
- `scopes_granted` (jsonb array)
|
||
- `last_health_check_at` (timestamp)
|
||
- `last_error_reason_code` (string, nullable)
|
||
- `last_error_message` (string, nullable; must be sanitized)
|
||
- `metadata` (jsonb)
|
||
- `created_at`, `updated_at`
|
||
|
||
**Relationships (existing)**
|
||
|
||
- `tenant(): BelongsTo`
|
||
- `workspace(): BelongsTo`
|
||
- `credential(): HasOne` → ProviderCredential
|
||
|
||
**Invariants (existing + required)**
|
||
|
||
- Uniqueness: `unique (tenant_id, provider, entra_tenant_id)`
|
||
- Exactly one default per (tenant_id, provider): enforced by partial unique index `provider_connections_default_unique`.
|
||
|
||
**Behaviors (existing)**
|
||
|
||
- `makeDefault()` clears other defaults and sets this record default in a DB transaction.
|
||
|
||
### ProviderCredential
|
||
|
||
**Represents**: Encrypted credential material for a provider connection.
|
||
|
||
**Table**: `provider_credentials`
|
||
|
||
**Fields (existing)**
|
||
|
||
- `id` (PK)
|
||
- `provider_connection_id` (FK, unique)
|
||
- `type` (string; default `client_secret`)
|
||
- `payload` (encrypted array; hidden from serialization)
|
||
- `created_at`, `updated_at`
|
||
|
||
**Payload contract (current)**
|
||
|
||
- For `type=client_secret`:
|
||
- `client_id` (string)
|
||
- `client_secret` (string)
|
||
- optional `tenant_id` (string) validated against `ProviderConnection.entra_tenant_id`
|
||
|
||
### OperationRun
|
||
|
||
**Represents**: A canonical record for a long-running or operationally relevant action.
|
||
|
||
**Table**: `operation_runs`
|
||
|
||
**Key fields (existing)**
|
||
|
||
- `id` (PK)
|
||
- `workspace_id` (FK)
|
||
- `tenant_id` (FK nullable in some cases)
|
||
- `type` (string)
|
||
- `status` (`queued`|`running`|`completed`)
|
||
- `outcome` (`pending`|`succeeded`|`partially_succeeded`|`failed` + reserved `cancelled`)
|
||
- `context` (json)
|
||
- `failure_summary` (json)
|
||
- `summary_counts` (json)
|
||
- `started_at`, `completed_at`
|
||
|
||
**Context contract (provider-backed runs)**
|
||
|
||
- `provider` (string)
|
||
- `provider_connection_id` (int)
|
||
- `target_scope.entra_tenant_id` (string)
|
||
- `module` (string; from ProviderOperationRegistry definition)
|
||
|
||
**Spec 081 extension (planned)**
|
||
|
||
- Introduce `outcome=blocked` and store `reason_code` + link-only `next_steps` in safe context/failure summary.
|
||
|
||
## State Transitions
|
||
|
||
### ProviderConnection default selection
|
||
|
||
- `is_default: false -> true` via `makeDefault()`.
|
||
- Invariant: only one default per (tenant_id, provider).
|
||
|
||
### Provider-backed operation starts
|
||
|
||
- Start surface enqueues work and creates/dedupes `OperationRun`.
|
||
- If blocked (missing default connection/credential), an `OperationRun` is still created and finalized as blocked.
|
||
|