# Feature Specification: Action Surface Contract Compliance & RBAC Hardening

**Feature Branch**: `090-action-surface-contract-compliance`  
**Created**: 2026-02-12  
**Status**: Draft  
**Input**: User description: "Action Surface Contract Compliance & RBAC Hardening"

## Clarifications

### Session 2026-02-12

- Q: Should BackupSchedule retention (soft delete / restore / force delete) be included in Spec 090? → A: Defer it to a separate follow-up spec (Spec 090 covers only P0/P1 + empty-state CTAs).
- Q: Should the system record audit trail entries for denied/cancelled attempts, or only for successful executions? → A: Audit successful executions only.
- Q: Should row-click navigation be removed, or kept while adding an inspection affordance? → A: Keep row-click navigation where it exists; the inspection affordance may be clickable rows, a primary linked column, or a dedicated “View” action (avoid rendering a lone “View” row action button on read-only lists).

## User Scenarios & Testing *(mandatory)*

<!--
  IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance.
  Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them,
  you should still have a viable MVP (Minimum Viable Product) that delivers value.
  
  Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical.
  Think of each story as a standalone slice of functionality that can be:
  - Developed independently
  - Tested independently
  - Deployed independently
  - Demonstrated to users independently
-->

### User Story 1 - Safe admin actions (Priority: P1)

As an admin, I can only execute actions that create changes or side-effects when I am entitled to the scope (tenant/workspace) and have the required capability.

**Why this priority**: Prevents unauthorized changes and accidental destructive operations; closes RBAC gaps.

**Independent Test**: Can be fully tested by attempting a small set of in-scope actions as (a) non-member, (b) member without capability, (c) member with capability.

**Acceptance Scenarios**:

1. **Given** I am not a member of the tenant/workspace scope, **When** I try to access pages or execute actions in that scope, **Then** I receive a not found result (deny-as-not-found).
2. **Given** I am a member of the tenant/workspace scope but I lack the capability for an action with side-effects, **When** I attempt the action, **Then** the UI communicates it is unavailable and direct execution is denied.
3. **Given** I am a member of the tenant/workspace scope and I have the capability, **When** I execute the action, **Then** the action completes and an audit trail entry is recorded.

---

### User Story 2 - Consistent action surfaces (Priority: P2)

As an admin, I can reliably discover how to inspect and manage records across the admin panel because each list/table provides a visible inspection affordance and consistent action grouping and ordering.

**Why this priority**: Improves enterprise UX consistency, accessibility, and reduces operator error.

**Independent Test**: Can be tested by reviewing targeted list/table screens for a clear inspection affordance, consistent ordering, and consistent “More” grouping.

**Acceptance Scenarios**:

1. **Given** a record list/table for an in-scope resource, **When** I view the table’s inspection affordance, **Then** I can navigate to a record detail page using at least one of: clickable row navigation, a primary linked column, or a dedicated “View” action.
2. **Given** a record list/table has row-click navigation, **When** I cannot (or prefer not to) use row-click navigation, **Then** I can still navigate to record details using a visible alternative inspection affordance (a primary linked column and/or a dedicated “View” action).
3. **Given** a list/table is read-only and “View” would be the only row action, **When** the UI is rendered, **Then** it SHOULD avoid rendering a lone “View” row action button and instead use clickable rows or a primary linked column.

---

### User Story 3 - Productive empty states (Priority: P3)

As an admin, when a resource has no records yet, the empty state clearly indicates what to do next and offers a “New …” call-to-action when I am allowed to create records.

**Why this priority**: Reduces onboarding friction and “dead-end” screens.

**Independent Test**: Can be tested by visiting empty list/table screens for user-created resources and verifying the create call-to-action is present only when permitted.

**Acceptance Scenarios**:

1. **Given** there are no workspaces yet, **When** I open the workspaces list, **Then** I see a create call-to-action if I am allowed to create.
2. **Given** there are no backup schedules yet, **When** I open the backup schedules list, **Then** I see a create call-to-action if I am allowed to create.

---

### Edge Cases

- A user attempts to execute a side-effect action via a direct request while the UI action is disabled.
- Bulk actions: mixed selection where some records are outside scope or not authorized.
- A destructive action is triggered unintentionally; confirmation must prevent accidental execution.
- Accessibility: users who cannot use row-click navigation must still discover a usable inspection affordance.
- Two admins attempt the same side-effect action concurrently; the system must behave predictably (no partial execution without an audit trail).

## Requirements *(mandatory)*

**Constitution alignment (required):** If this feature introduces any Microsoft Graph calls, any write/change behavior,
or any long-running/queued/scheduled work, the spec MUST describe contract registry updates, safety gates
(preview/confirmation/audit), tenant isolation, run observability (`OperationRun` type/identity/visibility), and tests.
If security-relevant DB-only actions intentionally skip `OperationRun`, the spec MUST describe `AuditLog` entries.

**Constitution alignment (RBAC-UX):** If this feature introduces or changes authorization behavior, the spec MUST:
- state which authorization plane(s) are involved (tenant `/admin/t/{tenant}` vs platform `/system`),
- ensure any cross-plane access is deny-as-not-found (404),
- explicitly define 404 vs 403 semantics:
  - non-member / not entitled to workspace scope OR tenant scope → 404 (deny-as-not-found)
  - member but missing capability → 403
- describe how authorization is enforced server-side (Gates/Policies) for every mutation/operation-start/credential change,
- reference the canonical capability registry (no raw capability strings; no role-string checks in feature code),
- ensure global search is tenant-scoped and non-member-safe (no hints; inaccessible results treated as 404 semantics),
- ensure destructive-like actions require confirmation (`->requiresConfirmation()`),
- include at least one positive and one negative authorization test, and note any RBAC regression tests added/updated.

**Constitution alignment (OPS-EX-AUTH-001):** OIDC/SAML login handshakes may perform synchronous outbound HTTP (e.g., token exchange)
on `/auth/*` endpoints without an `OperationRun`. This MUST NOT be used for Monitoring/Operations pages.

**Constitution alignment (BADGE-001):** If this feature changes status-like badges (status/outcome/severity/risk/availability/boolean),
the spec MUST describe how badge semantics stay centralized (no ad-hoc mappings) and which tests cover any new/changed values.

**Constitution alignment (Filament Action Surfaces):** If this feature adds or modifies any Filament Resource / RelationManager / Page,
the spec MUST include a “UI Action Matrix” (see below) and explicitly state whether the Action Surface Contract is satisfied.
If the contract is not satisfied, the spec MUST include an explicit exemption with rationale.

### Functional Requirements

- **FR-001 (Scope boundaries)**: The system MUST preserve deny-as-not-found for tenant/workspace scope boundaries: non-members or not-entitled users MUST receive a not found result for pages and actions in that scope.
- **FR-002 (403 semantics)**: When a user is a member of the tenant/workspace scope but lacks a required capability, the system MUST deny execution of the action with a forbidden result.
- **FR-003 (UI communication)**: For members without required capability, the UI MUST communicate that the action is unavailable (disabled state + explanatory hint).
- **FR-004 (Side-effect protection)**: Any action that creates/updates/deletes data or triggers external side-effects MUST require capability-first gating and MUST be enforced server-side.
- **FR-005 (Destructive confirmations)**: Destructive actions (archive, deactivate, delete, force delete) MUST require an explicit confirmation step.
- **FR-006 (Inspect affordance)**: Each in-scope list/table MUST provide a record inspection affordance per record. Accepted forms: clickable row navigation, a primary linked column, or a dedicated “View” action. Where row-click navigation already exists, it SHOULD be preserved; any dedicated “View” action is additive for accessibility and discoverability.
- **FR-007 (Action order and grouping)**: Row actions MUST follow the baseline order “View → Edit → Secondary → Destructive (last)”, and secondary actions MUST be grouped under a consistently labeled “More” group.
- **FR-008 (Presentation semantics)**: Sync/run-style actions MUST NOT be presented as destructive.
- **FR-009 (Empty-state CTAs)**: User-created resources in scope MUST provide an empty-state create call-to-action when the user is allowed to create.
- **FR-010 (Auditability)**: For every executed side-effect action in scope, the system MUST record an audit trail entry including who performed it, what was attempted, the scope (tenant/workspace), and the outcome.
- **FR-011 (Audit scope)**: Denied or cancelled attempts MUST NOT be required to create audit trail entries for this feature.

#### In-scope hotspots

The following hotspots MUST be brought into compliance:

- Policy “Capture snapshot” action: requires Tenant Sync capability and must be treated as a side-effect action.
- Workspace CRUD actions: capability-first gating across create/edit and related header actions.
- Tenant archive/deactivate: destructive confirmation compliance.
- Provider connections: ensure a visible inspection affordance and consistent “More” grouping/order.
- Directory groups (Entra groups) and inventory items: verify they remain compliant with the inspection-affordance rule (clickable rows) and do not regress.
- Findings list: action ordering consistency (View first).

### Canonical Capability Mapping (Spec 090)

All capability checks in this feature MUST use constants from `App\Support\Auth\Capabilities`:

| Hotspot / Surface | Required capability constant(s) |
|---|---|
| Policy list + view: “Sync from Intune”, “Capture snapshot” | `Capabilities::TENANT_SYNC` |
| Findings acknowledge actions (row, bulk, list-header mass acknowledge) | `Capabilities::TENANT_FINDINGS_ACKNOWLEDGE` |
| Backup schedules: create/edit/delete | `Capabilities::TENANT_BACKUP_SCHEDULES_MANAGE` |
| Backup schedules: run/retry (row + bulk) | `Capabilities::TENANT_BACKUP_SCHEDULES_RUN` |
| Provider connections: create/edit/credentials/default/enable/disable | `Capabilities::PROVIDER_MANAGE` |
| Provider connections: check connection / inventory sync / compliance snapshot | `Capabilities::PROVIDER_RUN` |
| Tenants: edit/admin consent | `Capabilities::TENANT_MANAGE` |
| Tenants: deactivate/force delete/restore | `Capabilities::TENANT_DELETE` |
| Tenants: sync | `Capabilities::TENANT_SYNC` |
| Workspaces: create/edit | `Capabilities::WORKSPACE_MANAGE` |

## UI Action Matrix *(mandatory when Filament is changed)*

If this feature adds/modifies any Filament Resource / RelationManager / Page, fill out the matrix below.

For each surface, list the user-facing actions (intent) and note the label when it is explicitly customized.
Do not rely on framework-default labels being stable across versions.

Also capture whether actions are destructive (confirmation? typed confirmation?), RBAC gating (capability + enforcement helper),
and whether the mutation writes an audit log.

| 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 |
|---|---|---|---|---|---|---|---|---|---|
| Policies (list) | Tenant panel (tenant scope) | “Sync from Intune” | Dedicated “View” row action | Visible: “View”; Group: “More” → “Ignore” / “Restore” / “Sync” / “Export to Backup” | Group: “More” → “Ignore Policies” / “Restore Policies” / “Sync Policies” / “Export to Backup” | “Sync from Intune” (same as header) | — | — | Yes | Sync/run-style actions must not be presented as destructive; side-effect actions are capability-gated and enforced server-side.
| Policy (view) | Tenant panel (tenant scope) | — | — (view page) | — | — | — | “Capture snapshot” (confirmation) | — | Yes | Capture snapshot is treated as a side-effect action.
| Workspaces (list + CRUD) | Admin panel (workspace scope) | Create workspace (Create action) | Dedicated “View” row action | Visible: “View” / “Edit” | — | (If table is empty) Create workspace | — | Save + Cancel | Yes | Ensure capability-first gating for create/edit and consistent action ordering.
| Tenants (list) | Admin panel (platform scope) | — | Dedicated “View” row action (label: “View”) | Action group (ellipsis) containing: “View” / “Sync” / “Open” / “Edit” / “Restore” / “Admin consent” / “Open in Entra” / “Verify configuration” / “Deactivate” / “Force delete” | “Sync selected” | — | — | Save + Cancel | Yes | Destructive actions (“Deactivate”, “Force delete”) require confirmation; side-effect actions (“Sync”, “Verify configuration”) require capability gating.
| Provider connections (list) | Admin panel (platform scope; tenant-filtered) | Create provider connection (Create action) | Clickable rows (record URL to view) | Action group (target label: “More”) containing: “Edit” / “Check connection” / “Inventory sync” / “Compliance snapshot” / “Set as default” / “Update credentials” / “Enable connection” / “Disable connection” | — | (If table is empty) Create provider connection | — | Save + Cancel | Yes | Current code uses a group label “Actions”; normalize to “More” to match FR-007.
| Directory groups (Entra groups list) | Admin panel (tenant scope) | None (intentionally) | Clickable rows (record URL) | None (read-only list) | None | None | — | — | No | Inspection affordance is clickable rows; no row actions.
| Inventory items (list) | Admin panel (tenant scope) | None (intentionally) | Clickable rows (record URL) | None (read-only list) | None | None | — | — | No | Inspection affordance is clickable rows; no row actions.
| Findings (list) | Tenant panel (tenant scope) | — | Dedicated “View” row action | Visible: “View”; Secondary: “Acknowledge” (confirmation) | Group: “Acknowledge selected” (confirmation) | — | — | — | Yes | Action ordering must match baseline (View first; secondary actions grouped).
| Backup schedules (list) | Tenant panel (tenant scope) | Create backup schedule (Create action) | “Edit” (opens record details/edit form) | Action group (ellipsis) containing: “Run now” / “Retry” / “Edit” / “Delete” | Group: “Run now” / “Retry” / “Delete” | (If table is empty) Create backup schedule | — | Save + Cancel | Yes | Retention/archival is deferred; destructive actions (“Delete”, bulk delete) require confirmation.

### Key Entities *(include if feature involves data)*

- **Capability**: A named permission that grants access to a class of actions (e.g., “Tenant Sync”, “Workspace Manage”).
- **Scope entitlement**: Whether a user is a member of / entitled to a tenant or workspace.
- **Admin action surface**: Where a user can discover and trigger an action (header, row actions, bulk actions, empty state).
- **Destructive action**: An action that archives, deactivates, deletes, or force deletes data.
- **Audit trail entry**: A record of an attempted or completed action, including actor, scope, and outcome.

## Success Criteria *(mandatory)*

### Measurable Outcomes

- **SC-001 (Contract compliance)**: 100% of the in-scope resources satisfy Action Surface Contract v1 (clear inspection affordance, consistent “More” group, correct action ordering).
- **SC-002 (RBAC hardening)**: 100% of in-scope side-effect actions are denied for members without capability and are deny-as-not-found for non-members.
- **SC-003 (Confirmation compliance)**: 100% of in-scope destructive actions require confirmation prior to execution.
- **SC-004 (Discoverability)**: For each in-scope list/table, at least one inspection affordance is clearly visible and usable to access record details (clickable rows, a primary linked column, and/or a dedicated “View” action).
- **SC-005 (Auditability)**: 100% of executed in-scope side-effect actions produce an audit trail entry with actor + scope + outcome.

## Assumptions

- Capability names used in this spec map to the canonical capability registry.
- “Capture snapshot” is considered a side-effect action and therefore requires both confirmation and capability gating.
- BackupSchedule retention (soft delete / restore / force delete) is explicitly deferred to a follow-up spec.
- Existing row-click navigation patterns should remain unchanged; this feature ensures a clear inspection affordance (clickable rows, primary linked columns, or a dedicated “View” action where appropriate).

## Out of Scope

- Navigation or information architecture redesign.
- Broad refactors of policies/capabilities outside the listed hotspots.
- Changing the established 404 vs 403 boundary semantics.
- BackupSchedule retention behavior changes (soft delete, restore, force delete).

## Rollout Notes

- This change primarily affects user experience and authorization enforcement; it should be safe to roll out behind existing access controls.
- Optional retention/archival behavior (if introduced later) requires a separate rollout decision.

## Testing Notes

- Add at least one positive and one negative authorization test per critical hotspot.
- Add regression assertions that each in-scope list/table has a clear inspection affordance and consistent ordering.
- Verify that denied attempts do not create audit trail entries for this feature.

<!-- End of specification -->
