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
183 lines
16 KiB
Markdown
183 lines
16 KiB
Markdown
# Feature Specification: Provider Connections (Tenantless UI + Tenant Transparency)
|
||
|
||
**Feature Branch**: `089-provider-connections-tenantless-ui`
|
||
**Created**: 2026-02-12
|
||
**Status**: Draft
|
||
**Input**: User description: "Provider Connections als workspace-weites Integrations-Asset (tenantless UI) + Tenant-Transparenz"
|
||
|
||
## Clarifications
|
||
|
||
### Session 2026-02-12
|
||
|
||
- Q: Welche Provider sind im MVP-Scope dieser Spec? → A: Nur Microsoft (Graph) im MVP; UI/IA bleibt generisch „Provider Connections“.
|
||
- Q: Wie wird „aktiver Tenant-Kontext“ für den Default-Filter bestimmt? → A: TenantContext kommt aus Session/Context-Switcher; `tenant_id` Querystring kann den Default überschreiben.
|
||
- Q: Soll „Provider Connections“ in Filament Global Search erscheinen? → A: Nein, Global Search ist für diese Resource deaktiviert.
|
||
- Q: Wie soll die Auditability konkret umgesetzt werden? → A: AuditLog + OperationRun.
|
||
|
||
## User Scenarios & Testing *(mandatory)*
|
||
|
||
### User Story 1 - Workspace-weite Übersicht (Priority: P1)
|
||
|
||
Als Operator/Admin im Workspace möchte ich Provider Connections zentral unter Settings → Integrations finden und über eine canonical Route ohne Tenant-Parameter aufrufen, damit Integrationen enterprise-typisch auffindbar sind und die UI nicht von tenant-scoped Routes abhängt.
|
||
|
||
**Why this priority**: Stellt Informationsarchitektur wieder her, reduziert Kontextbrüche und eliminiert fragiles tenant-scoped Routing als Voraussetzung.
|
||
|
||
**Independent Test**: Aufruf der canonical Liste und Verifikation von Scoping + Default Filter + Tenant-Spalte.
|
||
|
||
**Acceptance Scenarios**:
|
||
|
||
1. **Given** ein User ist Mitglied im Workspace und Mitglied in Tenant A, aber nicht in Tenant B, **When** er die Provider-Connections-Liste öffnet, **Then** sieht er ausschließlich Connections aus Tenant A.
|
||
2. **Given** ein aktiver Tenant-Kontext ist gesetzt (Tenant A), **When** die Liste geöffnet wird, **Then** ist initial ein Tenant-A Filter aktiv und kann vom User entfernt werden.
|
||
3. **Given** ein aktiver Tenant-Kontext ist gesetzt (Tenant A) und der User ruft die Liste mit `?tenant_id=<tenant_external_id_von_TenantB>` auf, **When** die Liste lädt, **Then** ist Tenant B als Filter aktiv (Querystring überschreibt den Context-Default), ohne dass nicht-berechtigte Tenants Metadaten leaken.
|
||
4. **Given** ein User ist kein Workspace-Mitglied, **When** er die canonical Provider-Connections-Route aufruft, **Then** erhält er 404 (deny-as-not-found).
|
||
|
||
---
|
||
|
||
### User Story 2 - Sicherer Detail-/Edit-Zugriff ohne Secrets (Priority: P2)
|
||
|
||
Als Tenant-Mitglied möchte ich eine Provider Connection ansehen und (mit entsprechender Berechtigung) verwalten, damit ich Integrationsprobleme diagnostizieren und beheben kann, ohne dass Secrets im Klartext sichtbar werden.
|
||
|
||
**Why this priority**: Detail-/Edit-Surfaces sind die riskantesten Stellen für Metadaten-Leaks und Secret-Exfiltration.
|
||
|
||
**Independent Test**: Direkte View/Edit Zugriffe mit unterschiedlichen Membership-/Capability-Kombinationen; UI zeigt keine Klartext-Secrets.
|
||
|
||
**Acceptance Scenarios**:
|
||
|
||
1. **Given** ein User ist Workspace-Mitglied aber nicht Mitglied im owning Tenant einer Connection, **When** er die Detailseite direkt aufruft, **Then** erhält er 404.
|
||
2. **Given** ein User ist Tenant-Mitglied, aber ihm fehlt die View-Capability, **When** er Liste oder Detailseite aufruft, **Then** erhält er 403.
|
||
3. **Given** ein User hat View-, aber nicht Manage-Capability, **When** er die Detailseite öffnet, **Then** sind Manage-Aktionen sichtbar aber deaktiviert (mit Tooltip), und ein server-seitiger Mutationsversuch wird mit 403 abgewiesen.
|
||
4. **Given** irgendein berechtigter User sieht Liste/Detail, **When** Credentials/Secrets dargestellt werden, **Then** werden niemals Klartext-Secrets angezeigt und es gibt keine Copy-Aktionen für Secrets.
|
||
|
||
---
|
||
|
||
### User Story 3 - Tenant-Detailseite zeigt effektiven Provider-State + Deep Link (Priority: P3)
|
||
|
||
Als Operator im Tenant-Kontext möchte ich auf der Tenant-Detailseite den effektiven Default-Provider-Connection-Status sehen und über eine CTA direkt zur vorgefilterten Provider-Connections-Liste springen, damit ich Kontext behalte und trotzdem zentral arbeiten kann.
|
||
|
||
**Why this priority**: Reduziert Kontextbruch und macht den „effective state“ dort sichtbar, wo Operators ihn erwarten.
|
||
|
||
**Independent Test**: Tenant-Detailseite zeigt Card + CTA; CTA führt zur gefilterten canonical Liste.
|
||
|
||
**Acceptance Scenarios**:
|
||
|
||
1. **Given** Tenant A hat eine aufgelöste Default-Connection, **When** der User die Tenant-Detailseite öffnet, **Then** sieht er Display Name + Status/Health + Last Check.
|
||
2. **Given** Tenant A hat keine gültige Default-Connection, **When** der User die Tenant-Detailseite öffnet, **Then** sieht er einen klaren „Needs action“-State und eine CTA zur gefilterten Liste.
|
||
|
||
### Edge Cases
|
||
|
||
- User ist Workspace-Mitglied, aber in keinem Tenant Mitglied → Liste zeigt 0 Rows (ohne Tenant-Hinweise).
|
||
- `tenant_id` Filter verweist auf einen nicht-mitgliedschaftlich berechtigten Tenant → Liste zeigt 0 Rows; direkte Record-Zugriffe bleiben 404.
|
||
- Mitgliedschaft wird entzogen → direkte Zugriffe auf vormals sichtbare Records liefern 404.
|
||
- Sehr viele Tenants/Connections (MSP) → Scoping bleibt performant (server-seitige, membership-basierte Filterung ohne große In-Memory-ID-Listen).
|
||
- Health/Last Error enthält sensitive Inhalte → UI zeigt nur Reason Codes und gekürzte, nicht-sensitive Messages; keine Secrets.
|
||
|
||
## Requirements *(mandatory)*
|
||
|
||
### Functional Requirements
|
||
|
||
- **FR-001 (Canonical route)**: The system MUST expose Provider Connections at a canonical admin route without requiring a tenant route parameter (e.g., `/admin/provider-connections`).
|
||
- **FR-002 (Tenant filter)**: The canonical list MUST support optional filtering by owning tenant (e.g., `?tenant_id=<tenant_external_id>`).
|
||
- **FR-003 (Tenant transparency)**: List and detail MUST clearly show the owning tenant (name + environment indicator where applicable) and provide a deep link to tenant detail.
|
||
- **FR-004 (Context-respecting default filter)**: If a tenant context is active, the list MUST default to filtering by the current tenant. The user MUST be able to remove the filter.
|
||
- **FR-004a (Default resolution precedence)**: The default tenant filter MUST be resolved from the active TenantContext (session/context switcher). If a `tenant_id` query parameter is present, it MUST take precedence over the TenantContext-derived default.
|
||
- **FR-005 (Zero-leak scoping)**: Index/List MUST only return connections for tenants where the user is a member. The UI MUST not reveal metadata for non-member tenants.
|
||
- **FR-006 (Direct access semantics)**: Direct record access (view/edit) MUST be deny-as-not-found (404) when the user is not a member of the owning tenant.
|
||
- **FR-007 (Workspace gating)**: If the user is not a workspace member, all Provider Connections routes MUST return 404.
|
||
- **FR-008 (Capability-first RBAC)**: Capabilities MUST gate: view (list/detail), manage (create/edit/enable/disable/set-default/credential updates), run (health checks / operation triggers). Missing capability MUST return 403.
|
||
- **FR-009 (UI enforcement behavior)**: When missing capability but otherwise eligible (member), the UI SHOULD render actions disabled with a tooltip describing the missing capability (not silently removed), while server-side enforcement stays authoritative.
|
||
- **FR-010 (No secrets in UI)**: The UI MUST never display plaintext secrets and MUST NOT provide copy actions for secrets. Non-secret identifiers (e.g., Entra tenant ID) MAY be copyable.
|
||
- **FR-011 (List columns minimum)**: The list MUST include at least: Tenant, Provider, Display name, Entra tenant ID, Default indicator, Status badge, Health badge, Last check timestamp, Last error (reason code + truncated message).
|
||
- **FR-012 (List filters minimum)**: The list MUST provide filters for: Tenant, Provider, Status, Health, Default-only.
|
||
- **FR-013 (Tenant detail card)**: Tenant detail MUST show an “effective provider connection state” for the tenant and provide a CTA to open the canonical Provider Connections list pre-filtered for that tenant.
|
||
- **FR-014 (Legacy redirect)**: Legacy tenant-scoped URLs for provider connections MUST behave according to the “Legacy URL Redirect Matrix” (302 redirects only for entitled members; no-leak 404 otherwise) and remain for at least two releases.
|
||
- **FR-015 (Scalability requirement)**: Tenant-visibility scoping MUST be performed at query time based on membership relationships and MUST NOT depend on loading large tenant-id lists into memory.
|
||
- **FR-016 (MVP provider scope)**: The MVP MUST support Microsoft (Graph) as the only provider. The navigation label and IA remain generic (“Provider Connections”).
|
||
- **FR-017 (Global Search)**: Provider Connections MUST NOT appear in global search.
|
||
|
||
- **FR-018 (Create tenant resolution)**: Create MUST resolve the target tenant to an entitled tenant via `tenant_id` query parameter or the active TenantContext default. If no entitled tenant can be resolved, the create surface MUST behave as not found (404).
|
||
|
||
### Information Architecture
|
||
|
||
- Provider Connections MUST be placed under Settings → Integrations → Provider Connections.
|
||
- Provider Connections MUST feel “workspace-level” even when a tenant context is active (context affects default filtering, not canonical addressability).
|
||
|
||
### Scope Boundaries
|
||
|
||
- In scope: tenantless canonical navigation and routing; tenant transparency in list/detail; context-respecting default filter; deny-as-not-found membership rules; capability-first action gating; legacy redirect behavior.
|
||
- Out of scope: shared connections across multiple tenants; redesign of default/override semantics; changing the meaning of “default”; large wizard refactors.
|
||
|
||
### Assumptions & Dependencies
|
||
|
||
- A "workspace" membership model exists and can be evaluated for every request.
|
||
- A "tenant membership" model exists and is the source of truth for which tenants a user may access.
|
||
- Provider Connections already belong to exactly one owning tenant and one workspace.
|
||
- A tenant detail surface exists where the “Provider connection” card can be shown.
|
||
- The system has, or can represent, a provider identifier for connections (even if only Microsoft exists in the MVP).
|
||
- The system has an active TenantContext concept (e.g., chosen via a context switcher) that can be read when rendering admin pages.
|
||
|
||
### Query Parameter Contract
|
||
|
||
- **QP-001**: The canonical tenant filter query parameter name is `tenant_id`.
|
||
- **QP-002**: The `tenant_id` value is the managed tenant’s external identifier (the same identifier used in `/admin/tenants/{tenant}` routes), not a database primary key.
|
||
|
||
### Tenant Transparency Conventions
|
||
|
||
- **TT-001**: “Environment indicator” means a human-readable label that distinguishes tenant environments when such labeling exists in the workspace (e.g., Production vs Staging). If no such label exists for a tenant, the UI MUST omit the environment indicator (not substitute guesses).
|
||
|
||
### Default Filter Removal
|
||
|
||
- **DF-001**: The default tenant filter is a usability default only. Users MAY remove it at any time.
|
||
- **DF-002**: Removing the default filter MUST NOT change authorization boundaries: list and detail remain scoped to tenants the user is a member of.
|
||
|
||
### Authorization Semantics (404 vs 403)
|
||
|
||
- **AS-001**: Not a workspace member → 404 for list/detail/edit and any action endpoints.
|
||
- **AS-002**: Workspace member but not a member of the owning tenant → list returns no rows for that tenant; direct record access → 404.
|
||
- **AS-003**: Tenant member but missing required capability → 403 for the protected surface/action.
|
||
|
||
### Audit & Observability
|
||
|
||
- **AO-001**: User-initiated actions that change state (set default, enable/disable, credentials update/rotate) MUST be auditable.
|
||
- **AO-002**: User-initiated run/health actions MUST be auditable, either via an audit event or a run record that is reachable via a canonical “view run” link.
|
||
- **AO-003 (Decision)**: Manage/state-change actions MUST write an AuditLog entry. Run/health actions MUST create an OperationRun (or equivalent run record) that is reachable via a canonical “view run” link.
|
||
|
||
### Backward Compatibility
|
||
|
||
- **BC-001**: Redirect MUST preserve intent (tenant filter) and MUST NOT leak tenant names/metadata for non-members.
|
||
- **BC-002**: Deprecation window is at least two releases.
|
||
|
||
### Legacy URL Redirect Matrix
|
||
|
||
- **LR-001 (Redirect, tenant-managed scope)**: Requests to legacy workspace-managed tenant routes MUST redirect (302) to the canonical route and preserve intent via the tenant filter:
|
||
- `/admin/tenants/{tenant_external_id}/provider-connections` → `/admin/provider-connections?tenant_id={tenant_external_id}`
|
||
- `/admin/tenants/{tenant_external_id}/provider-connections/create` → `/admin/provider-connections/create?tenant_id={tenant_external_id}`
|
||
- `/admin/tenants/{tenant_external_id}/provider-connections/{record}/edit` → `/admin/provider-connections/{record}/edit?tenant_id={tenant_external_id}`
|
||
- **LR-002 (No-leak)**: For non-workspace members or users who are not members of the target tenant, legacy URLs MUST behave as not found (404) and MUST NOT redirect.
|
||
- **LR-003 (Explicit exclusion)**: The previously removed tenant-panel management route shape `/admin/t/{tenant_external_id}/provider-connections` remains not found (404) and is not part of the redirect surface.
|
||
|
||
## UI Action Matrix *(mandatory when Filament is changed)*
|
||
|
||
| Surface | Location | Header Actions | Inspect Affordance (List/Table) | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions |
|
||
|---|---|---|---|---|---|---|---|---|---|---|
|
||
| Provider Connections (List) | Admin → Settings → Integrations → Provider Connections | Create (manage) | Tenant deep link + View action | View (view), Edit (manage) | None (not required) | Create (manage) | n/a | n/a | Yes | Default tenant filter applies when tenant context active |
|
||
| Provider Connection (View) | Provider Connections → View | Enable/Disable (manage), Set default (manage), Health check (run), Credential update (manage) | n/a | Edit (manage) | None | n/a | Same as header actions | n/a | Yes | Non-membership is 404; missing capability is 403; destructive-like actions require confirmation |
|
||
| Provider Connection (Create/Edit) | Provider Connections → Create/Edit | None | n/a | None | None | n/a | n/a | Save (manage), Cancel | Yes | Secrets never displayed; credential updates require confirmation |
|
||
| Tenant detail “Provider connection” card | Tenant detail page | Open Provider Connections (view) | n/a | None | None | Optional: Create connection (manage) | n/a | n/a | No | CTA links to canonical list filtered by tenant |
|
||
|
||
### Key Entities *(include if feature involves data)*
|
||
|
||
- **Workspace**: A membership-gated scope that owns managed tenants and integrations.
|
||
- **Managed Tenant**: A tenant within a workspace; users are members of specific tenants.
|
||
- **Provider Connection**: An integration record owned by exactly one managed tenant and one workspace; includes status/health metadata and non-secret identifiers.
|
||
- **Capabilities**: Named permissions that gate view/manage/run behavior; UI enforcement reflects missing capabilities.
|
||
- **Audit Event / Run Record**: Captures sensitive user-initiated actions for later review.
|
||
|
||
## Success Criteria *(mandatory)*
|
||
|
||
### Measurable Outcomes
|
||
|
||
- **SC-001 (IA discoverability)**: A workspace admin can reach Provider Connections from the admin sidebar in ≤ 2 clicks (Settings → Integrations → Provider Connections).
|
||
- **SC-002 (No metadata leaks)**: For non-members of a tenant, 100% of direct access attempts to that tenant’s Provider Connection detail/edit routes return 404 and do not reveal tenant/provider/status/health metadata.
|
||
- **SC-003 (RBAC correctness)**: For tenant members lacking a required capability, 100% of protected endpoints return 403; the UI consistently shows disabled actions with an explanatory tooltip.
|
||
- **SC-004 (Context-respecting UX)**: From a tenant detail page, the “Open Provider Connections” CTA lands on a pre-filtered list for that tenant with a first-attempt success rate ≥ 95% in acceptance testing.
|