TenantAtlas/specs/285-workspace-rbac-environment-access/spec.md
Ahmed Darrazi ef02ff5a29
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 8m29s
feat: implement spec 285 workspace-first environment access
2026-05-09 14:36:12 +02:00

330 lines
40 KiB
Markdown

# Feature Specification: Workspace-first RBAC & Environment Access Scoping
**Feature Branch**: `285-workspace-rbac-environment-access`
**Created**: 2026-05-09
**Status**: Blocked by external prerequisites
**Input**: User description: "Follow instructions in #prompt:SKILL.md with these arguments: mit 285 weitermachen"
## Spec Candidate Check *(mandatory — SPEC-GATE-001)*
- **Problem**: Repo truth already has workspace memberships, managed-environment memberships, a workspace capability resolver, and a tenant capability resolver, but authorization still depends on two parallel role-bearing truths. Operators can be valid workspace members while environment visibility, Filament tenant selection, provider-connection access, run drilldowns, and governance surfaces still hinge on separate `ManagedEnvironmentMembership` rows and a second capability map.
- **Today's failure**: access semantics are not consistently workspace-first. `WorkspaceContext`, `User::getTenants()`, `ProviderConnectionPolicy`, `OperationRunPolicy`, `FindingPolicy`, `EvidenceSnapshotPolicy`, `ReviewPackPolicy`, and `TenantReviewPolicy` all mix workspace membership, environment membership, and capability checks differently. That makes deny-as-not-found behavior harder to reason about, duplicates role administration, and blocks a calm future role model such as Workspace Admin versus Customer Viewer because role truth is split before customer-safe roles even exist.
- **User-visible improvement**: operators manage role-bearing access once at workspace level, environment visibility becomes an explicit secondary scope instead of a second hidden RBAC core, and tenant-owned pages, provider connections, run drilldowns, evidence, findings, and governance review surfaces all follow the same 404 versus 403 rules.
- **Smallest enterprise-capable version**: keep `workspace_memberships` as the only role-bearing access truth, replace role-bearing `ManagedEnvironmentMembership` semantics with a narrow optional managed-environment access-scope overlay, retarget `CapabilityResolver`, `User::canAccessTenant()`, `WorkspaceContext`, and the key environment-owned policies to evaluate workspace role first and environment scope second, and migrate the current tenant-membership management surface so it no longer edits a second role family.
- **Explicit non-goals**: no `/system` plane RBAC redesign, no full customer-portal RBAC migration, no mandatory environment-level ACL product, no per-environment role matrix, no new role-productization surface, no provider-capability registry work from Spec `283`, no source-taxonomy work from Spec `284`, no copy/localization neutralization from Spec `286`, no no-legacy guardrail pack from Spec `287`, and no compatibility shim or dual-write path.
- **Permanent complexity imported**: one unified workspace-first access-resolution path, one narrow optional managed-environment scope overlay contract, targeted policy rewiring, a migration of the current tenant-membership UI into workspace-role management plus environment-scope management, and focused unit, feature, and browser proof. No new role family and no new independent persisted source of truth are added.
- **Why now**: Specs `279` through `283` reserve the workspace-first cutover pack specifically so workspace context, provider-neutral identity, and provider capability truth do not sit on top of tenant-first authorization. Without `285`, the route shell and provider boundary work still rest on a dual RBAC core that keeps the product Microsoft- and tenant-shaped in its access semantics.
- **Why not local**: a local policy patch or one-off page helper would leave `User`, `WorkspaceContext`, `CapabilityResolver`, `OperationRunPolicy`, relation managers, and onboarding or provider surfaces inconsistent. The drift is structural and already spans models, policies, Filament navigation, and tests.
- **Approval class**: Core Enterprise
- **Red flags triggered**: authorization source-of-truth change, cross-cutting policy retargeting, and semantic replacement of an existing role-bearing model. Defense: the repo already carries both workspace and managed-environment role truths. Canonical replacement is safer and narrower than keeping them synchronized through more compatibility logic.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 1 | Komplexität: 1 | Produktnähe: 2 | Wiederverwendung: 2 | **Gesamt: 10/12**
- **Decision**: approve
## Spec Scope Fields *(mandatory)*
- **Scope**: workspace
- **Primary Routes**:
- `/admin/workspaces`
- `/admin/workspaces/{record}`
- `/admin/provider-connections`
- named onboarding routes `admin.onboarding` and `admin.onboarding.draft`
- current admin-panel managed-environment selection and tenant-owned routes used by `Finding`, `EvidenceSnapshot`, `ReviewPack`, `TenantReview`, and `OperationRun` drilldowns, regardless of whether the shell is still the current tenant-context route family or the prepared workspace-first route family from Spec `280`
- `TenantResource` membership and related-access surfaces that currently expose managed-environment membership CRUD
- **Data Ownership**:
- `workspace_memberships` remain the workspace-owned, role-bearing source of truth
- the current `managed_environment_memberships` persistence becomes a narrow managed-environment access-scope overlay or is replaced in-place by its workspace-first successor; it must not remain a second role-bearing truth
- `ManagedEnvironment`, `ProviderConnection`, `Finding`, `EvidenceSnapshot`, `ReviewPack`, and `TenantReview` remain managed-environment-owned records anchored by `workspace_id` plus `managed_environment_id`
- `OperationRun` continues to support both workspace-bound records anchored by `workspace_id` alone and managed-environment-bound records anchored by `workspace_id` plus optional `managed_environment_id`
- **RBAC**:
- workspace membership is the first entitlement boundary
- managed-environment access scope is an optional narrowing boundary, not a second independent role authority
- capability checks for managed-environment-owned resources continue to use the existing capability registry, but role resolution must come from workspace membership rather than environment membership
- non-members or out-of-scope actors stay `404`; in-scope members missing a required capability stay `403`
## Cross-Cutting / Shared Pattern Reuse
- **Cross-cutting feature?**: yes
- **Interaction class(es)**: navigation entry points, context selection, relation-manager membership management, cross-resource authorization, run drilldowns, and shared policy enforcement
- **Systems touched**: `WorkspaceContext`, `User::managedEnvironments()`, `User::getTenants()`, `User::canAccessTenant()`, `WorkspaceCapabilityResolver`, `CapabilityResolver`, `TenantMembershipManager`, `WorkspaceMembershipManager`, `OperationRunCapabilityResolver`, key environment-owned policies, `ManageTenantMemberships`, `TenantMembershipsRelationManager`, workspace membership relation managers, and shared `UiEnforcement` paths that depend on capability outcomes
- **Existing pattern(s) to extend**: workspace membership resolution, current capability resolver caching, shared deny-as-not-found policy pattern, existing workspace membership management, and existing action-surface enforcement for membership CRUD
- **Shared contract / presenter / builder / renderer to reuse**: `WorkspaceCapabilityResolver`, `CapabilityResolver` as the current managed-environment capability entry point, `WorkspaceContext`, `OperationRunCapabilityResolver`, `UiEnforcement`, `WorkspaceMembershipManager`, and the existing Filament relation-manager contract family
- **Why the existing shared path is sufficient or insufficient**: the repo already has the right core seams, but they currently stop at workspace-only or environment-only access decisions. The feature should converge them into one shared workspace-first access contract instead of introducing a third resolver stack.
- **Allowed deviation and why**: none. Any temporary compatibility alias, duplicate manager, or page-local access resolver would extend the drift this slice is meant to remove.
- **Consistency impact**: workspace chooser, managed-environment selection, provider-connections access, environment-owned resource policies, and membership-management surfaces must all derive access from the same workspace-first contract before provider capability or operability checks add deeper gating.
- **Review focus**: reviewers must verify that no role-bearing `ManagedEnvironmentMembership` path survives in policies or Filament gates, that workspace membership becomes the only role authority, and that environment scope is modeled as narrowing rather than as a second full RBAC system.
## OperationRun UX Impact
- **Touches OperationRun start/completion/link UX?**: yes
- **Shared OperationRun UX contract/layer reused**: `OperationRunCapabilityResolver`, `OperationRunPolicy`, `ProviderOperationStartGate`, and the existing `OperationRunService` lifecycle path
- **Delegated start/completion UX behaviors**: run viewability, required capability lookups, workspace-versus-environment entitlement resolution, and provider-capability follow-through stay delegated to the shared run authorization seams
- **Local surface-owned behavior that remains**: start surfaces keep only initiation inputs and the existing `Open operation` or drilldown affordances; this slice changes the access contract they rely on
- **Queued DB-notification policy**: `N/A`
- **Terminal notification path**: existing central lifecycle mechanism
- **Exception required?**: none
## Provider Boundary / Platform Core Check
`N/A - no shared provider/platform boundary is redefined in this slice. The feature consumes provider-neutral capability outcomes from Spec 283 as downstream inputs only.`
## UI / Surface Guardrail Impact
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|---|---|---|---|---|---|---|
| Workspace membership management | yes | Native Filament resource relation manager | role-bearing membership CRUD, audit-safe owner guards | page, relation manager, modal | no | becomes the only role-management surface |
| Managed-environment access-scope management (retargeted from current tenant membership surface) | yes | Native Filament relation manager plus existing page shell | optional environment narrowing, selection visibility, drilldown continuity | page, relation manager, modal | no | must stop acting like a second role editor |
| Shared managed-environment selection and tenant-owned page access | yes | Mixed shared shell plus existing native pages | context selection, 404/403 semantics, route continuity, run drilldowns | shell, page, remembered context, URL-query | no | workflow guardrail change more than layout change |
## Decision-First Surface Role
| Surface | Decision Role | Human-in-the-loop Moment | Immediately Visible for First Decision | On-Demand Detail / Evidence | Why This Is Primary or Why Not | Workflow Alignment | Attention-load Reduction |
|---|---|---|---|---|---|---|---|
| Workspace membership management | Primary Decision Surface | Decide which workspace role a user should hold | member, role, and last-owner guard status | audit trail and historical change detail | Primary because this is the canonical role-bearing authority after the cutover | Matches workspace-first SaaS administration | removes duplicate role assignment across workspace and environment surfaces |
| Managed-environment access-scope management | Secondary Context Surface | Decide whether a workspace member should be narrowed to a subset of environments | whether access inherits workspace-wide or is explicitly narrowed | exact scoped environments and audit history | Secondary because it narrows visibility after the main workspace role decision | Keeps environment scope subordinate to workspace role truth | avoids forcing operators to reason about two different role systems |
| Shared managed-environment selection and tenant-owned page access | Primary Decision Surface | Decide whether the current environment is accessible and what next page can be opened safely | current workspace, selected environment, access-denied routing, one next valid destination | deeper diagnostics, provider capability blockers, and operability detail | Primary because operators feel the cutover first through context selection and page access | Follows the existing admin shell and drilldown workflow | reduces surprise 404/403 drift between pages and runs |
## Audience-Aware Disclosure
| Surface | Audience Modes In Scope | Decision-First Default-Visible Content | Operator Diagnostics | Support / Raw Evidence | One Dominant Next Action | Hidden / Gated By Default | Duplicate-Truth Prevention |
|---|---|---|---|---|---|---|---|
| Workspace membership management | operator-MSP, support-platform | member, role, allowed management actions, owner-guard status | audit detail, membership source, history | raw model identifiers | `Change role` or `Add member` | raw IDs and low-level audit context stay secondary | workspace role is shown once as the canonical authority |
| Managed-environment access-scope management | operator-MSP, support-platform | whether access inherits workspace-wide or is explicitly scoped, plus the scoped environments | who is scoped where, why, and audit history | raw pivot identifiers and debug metadata | `Manage access scope` | raw pivot data stays hidden | the page explains environment narrowing once and does not repeat workspace role meaning |
| Shared managed-environment selection and tenant-owned page access | operator-MSP, support-platform | selected workspace, selected environment, and whether access is allowed | scoped-access reasons, provider capability or operability follow-up | raw policy internals and debug payloads | `Open environment` or `Return to workspace` | debug semantics stay out of default-visible UI | access denial is explained through one shared contract before deeper diagnostics appear |
## UI/UX Surface Classification
| Surface | Action Surface Class | Surface Type | Likely Next Operator Action | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type / Justification |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Workspace membership management | List / Detail / Settings | CRUD / Relation-first Resource | add member or change role | inline relation-manager management | forbidden | grouped relation actions only | grouped destructive actions with confirmation | workspace detail memberships relation | same workspace detail page | workspace context | Workspace member | canonical role-bearing access | none |
| Managed-environment access-scope management | List / Detail / Settings | CRUD / Relation-first Resource | add or remove allowed environments for a workspace member | inline relation-manager or dedicated scoped page | forbidden | grouped relation actions only | grouped destructive actions with confirmation | managed-environment access scope page under current workspace or environment | same access-scope page | workspace and managed-environment context | Environment access scope | whether access is inherited or narrowed | none |
| Shared managed-environment selection and tenant-owned page access | Navigation / Drilldown / Context | Global-context Shell | open the current environment safely | explicit environment selection and deep-link entry points | required where the shell already uses row or identifier click | secondary navigation or diagnostics in helper placements | none introduced by this slice | existing admin shell and context chooser | existing tenant-owned page routes and run drilldowns | workspace, managed environment, lifecycle | Managed environment | access allowed or deny-as-not-found boundary | none |
## Operator Surface Contract
| Surface | Primary Persona | Decision / Operator Action Supported | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|---|---|---|---|---|---|---|---|---|---|---|
| Workspace membership management | Workspace owner or manager | decide which workspace role a member should hold | relation-manager settings surface | What should this member be allowed to do across the workspace? | member, workspace role, owner-guard state | audit history and membership source | role authority, owner-guard | TenantPilot only | Add member, Change role | Remove member |
| Managed-environment access-scope management | Workspace owner or manager | decide whether a member should be limited to specific environments | relation-manager or dedicated scoped page | Should this member inherit workspace-wide access or be narrowed to specific environments? | inheritance mode, selected environments | audit history, scope source | inherited versus narrowed | TenantPilot only | Add allowed environment, Remove allowed environment | Clear or narrow access scope |
| Shared managed-environment selection and tenant-owned page access | Workspace operator | decide whether the current environment or run can be opened safely | global-context shell and page access | Can I open this environment, run, or resource from the current workspace? | current workspace, selected environment, and access result | deeper operability or provider blockers | workspace entitlement, environment scope, capability, operability | none | Open environment, Return to workspace | none |
## Proportionality Review
- **New source of truth?**: yes, but it is a canonical replacement of the existing role-bearing source-of-truth split rather than a new persisted truth
- **New persisted entity/table/artifact?**: no
- **New abstraction?**: yes
- **New enum/state/reason family?**: no
- **New cross-domain UI framework/taxonomy?**: no
- **Current operator problem**: access control is duplicated across workspace roles and managed-environment memberships, which creates inconsistent page access, duplicated administration, and future-role drift.
- **Existing structure is insufficient because**: the current structure requires two role-bearing membership tables plus two resolver stacks to answer one authorization question. That contradicts the workspace-first cutover and makes deny-as-not-found rules inconsistent.
- **Narrowest correct implementation**: keep workspace memberships as the only role-bearing truth, reinterpret or replace the existing managed-environment membership model as a narrow access-scope overlay, and retarget the existing shared resolvers, policies, and relation managers instead of adding a new RBAC framework.
- **Ownership cost**: capability resolution, model semantics, membership management surfaces, policy tests, and browser smoke all need synchronized updates; that cost is bounded because the feature removes a duplicate model instead of adding a third one.
- **Alternative intentionally rejected**: keeping both role-bearing tables and adding synchronization or fallback logic. That would preserve the split truth and add more compatibility complexity in a pre-production codebase.
- **Release truth**: current-release truth
### Compatibility posture
This feature assumes a pre-production environment.
Backward compatibility, legacy aliases, dual-write logic, historical-fixture preservation, and compatibility-specific tests are out of scope unless an adjacent prerequisite spec explicitly requires them.
Canonical replacement is preferred over preservation.
### External implementation prerequisites
- Spec `280` must already provide the workspace-first route and panel-shell baseline on the implementation branch before `285` runtime work begins.
- Spec `281` must already provide the provider-neutral target-scope and provider-identity baseline so access scope does not hardcode Microsoft-specific scope contracts.
- Spec `283` must already provide the provider capability context consumed by provider-backed actions once workspace-first access passes.
- Spec `284` remains adjacent but is not a hard blocker for the RBAC cutover as long as `285` does not absorb source-taxonomy work.
## Testing / Lane / Runtime Impact
- **Test purpose / classification**: Unit, Feature, Browser
- **Validation lane(s)**: fast-feedback, confidence, browser
- **Why this classification and these lanes are sufficient**: the resolver and scope-overlay logic need unit proof; policy, Filament access, and environment-selection behavior need feature proof; one browser smoke is required to confirm that workspace role management plus environment-scope management drive real shell access consistently.
- **New or expanded test families**: one unit family for workspace-first access resolution and scoped-environment inheritance, one feature family for environment-owned policy retargeting, one feature family for membership-surface migration, one feature family for `OperationRun` access continuity, and one narrow browser smoke for workspace role plus environment-scope behavior
- **Fixture / helper cost impact**: moderate because proof needs workspace membership, optional environment-scope records, managed-environment context, and representative provider/run resources without widening global test defaults
- **Heavy-family visibility / justification**: one browser smoke only; no heavy-governance family is justified
- **Special surface test profile**: standard-native-filament, global-context-shell, shared-detail-family
- **Standard-native relief or required special coverage**: standard Filament feature coverage is sufficient for membership management surfaces; one global-context-shell smoke is required for environment selection and page access continuity
- **Reviewer handoff**: reviewers must verify that `workspace_memberships` become the sole role-bearing truth, that any surviving managed-environment overlay no longer carries capability authority, that `ProviderConnectionResource` stays non-globally-searchable with View and Edit pages intact, that touched destructive membership actions still use `->action(...)` plus `->requiresConfirmation()`, that Filament remains v5 on Livewire v4, that provider registration stays in `apps/platform/bootstrap/providers.php`, and that the planned tests cover both positive and negative access paths
- **Budget / baseline / trend impact**: moderate feature-local increase only
- **Escalation needed**: `document-in-feature` if implementation must stage a temporary scope-overlay alias; `reject-or-split` if it keeps dual role authority
- **Active feature PR close-out entry**: Guardrail
- **Planned validation commands**:
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && REPO_ROOT="$(git rev-parse --show-toplevel)" && (cd "$REPO_ROOT/apps/platform" && ./vendor/bin/sail artisan test --compact tests/Unit/Auth/WorkspaceFirstCapabilityResolverTest.php tests/Unit/Auth/ManagedEnvironmentAccessScopeResolverTest.php)`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && REPO_ROOT="$(git rev-parse --show-toplevel)" && (cd "$REPO_ROOT/apps/platform" && ./vendor/bin/sail artisan test --compact tests/Feature/Auth/WorkspaceFirstManagedEnvironmentAccessTest.php tests/Feature/Rbac/ProviderConnectionWorkspaceFirstPolicyTest.php tests/Feature/Rbac/OperationRunWorkspaceFirstAuthorizationTest.php tests/Feature/Rbac/GovernanceArtifactsWorkspaceFirstAuthorizationTest.php tests/Feature/Filament/WorkspaceMembershipRoleManagementTest.php tests/Feature/Filament/ManagedEnvironmentAccessScopeManagementTest.php)`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && REPO_ROOT="$(git rev-parse --show-toplevel)" && (cd "$REPO_ROOT/apps/platform" && ./vendor/bin/sail artisan test --compact tests/Browser/Spec285WorkspaceRbacEnvironmentAccessSmokeTest.php)`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && REPO_ROOT="$(git rev-parse --show-toplevel)" && (cd "$REPO_ROOT/apps/platform" && ./vendor/bin/sail bin pint --dirty --format agent)`
## Candidate Selection Gate Summary
- **Selected candidate**: `285 - Workspace-first RBAC & Environment Access Scoping`
- **Source locations**:
- `docs/product/spec-candidates.md` under the reserved workspace-first cutover pack
- `docs/product/roadmap.md` under the same cutover ordering
- **Why selected now**: the user explicitly requested the reserved slot `285`, and repo truth shows this is the next unspecced cutover gap after workspace context, provider-neutral target scope, and provider capability preparation. The remaining blocker is no longer route or provider vocabulary alone, but the dual authorization core.
- **Why close alternatives were deferred**:
- `284` remains source-taxonomy work and should not be folded into RBAC replacement
- `286` should follow once the canonical access model and environment-scope semantics are stable
- `287` should harden the cutover after `285` finishes replacing the dual access core instead of before
- **Smallest viable implementation slice**: replace role-bearing managed-environment membership truth with workspace-first role resolution plus an optional environment-scope overlay, then retarget the key policies and membership surfaces around that contract
- **Documented deviations from raw candidate wording**:
- the raw candidate says `TenantMembership` should be removed or replaced, but current repo truth uses `ManagedEnvironmentMembership` rather than `TenantMembership`
- repo truth already has `WorkspaceMembership`, `WorkspaceCapabilityResolver`, and workspace-level role management, so `285` is not greenfield RBAC; it is a consolidation cutover
- the optional environment-scope layer should be modeled as a narrow visibility overlay, not as another full role-bearing ACL system
## Completed-Spec Guardrail Result
- `specs/279-workspace-managed-environment-core/` already exists with implementation-close-out history and remains historical prerequisite context only
- `specs/280-workspace-tenancy-environment-routing/` already exists with `Status: Ready` and remains adjacent prepared context only
- `specs/281-provider-connection-scope/` already exists with `Status: Ready` and remains adjacent prepared context only
- `specs/282-governance-artifact-retargeting/` already exists with implementation-close-out history and remains historical adjacent context only
- `specs/283-provider-capability-registry/` already exists with `Status: Ready` and remains adjacent prepared context only
- `specs/062-tenant-rbac-v1/` and `specs/065-tenant-rbac-v1/` remain historical tenant-first RBAC context and are not modified by this package
- the target package `specs/285-workspace-rbac-environment-access/` did not exist before this prep run and is the sole new package created here
## Deferred Adjacent Candidates
- `284 - Provider-neutral Artifact Source Taxonomy v1`
- `286 - UI Copy, IA & Localization Neutralization`
- `287 - Cutover Quality Gates & No-Legacy Enforcement`
## User Scenarios & Testing
### User Story 1 - Workspace membership becomes the only role authority (Priority: P1)
As a workspace owner or manager, I want one canonical role-bearing membership record so every managed-environment page and run drilldown inside my workspace follows the same role decision.
**Why this priority**: this is the core cutover value. Without it, environment-owned pages still depend on a second RBAC truth.
**Independent Test**: create one workspace member without any explicit managed-environment scope rows, open an entitled managed environment, and confirm provider connections, findings, evidence, review packs, and run drilldowns all use the workspace role consistently.
**Acceptance Scenarios**:
1. **Given** a user has a workspace membership with the required role and no environment-scope narrowing, **When** they open a managed-environment-owned resource inside that workspace, **Then** access is decided from workspace membership plus capability and does not require a second role-bearing environment membership.
2. **Given** a user is not a workspace member, **When** they open any managed-environment-owned route or run drilldown for that workspace, **Then** the system responds as deny-as-not-found.
---
### User Story 2 - Environment scope can narrow visibility without becoming a second RBAC system (Priority: P1)
As a workspace owner or manager, I want to optionally narrow a member to specific managed environments without creating a second role matrix per environment.
**Why this priority**: the roadmap explicitly wants environment access scopes to stay feasible, but not at the cost of reintroducing another full ACL core.
**Independent Test**: grant one workspace member access to only one environment, confirm that environment is selectable and a sibling environment in the same workspace stays hidden or 404, while the same workspace role capabilities apply inside the allowed environment.
**Acceptance Scenarios**:
1. **Given** a workspace member has an explicit allowlist for one managed environment, **When** they try to open another managed environment in the same workspace, **Then** the system responds as deny-as-not-found.
2. **Given** a workspace member has an explicit allowlist for one managed environment, **When** they open an allowed environment, **Then** their role capabilities are derived from workspace membership and only visibility is narrowed by environment scope.
---
### User Story 3 - Membership management surfaces stop editing duplicate role truth (Priority: P2)
As a workspace owner or manager, I want workspace roles and environment scopes managed on purpose-built surfaces so I do not assign contradictory roles in two places.
**Why this priority**: the current tenant-membership UI encodes the drift directly; until it is retargeted, operators can keep rebuilding the dual model.
**Independent Test**: open the workspace membership surface and the retargeted environment-scope surface, confirm roles are managed only at workspace level, confirm environment surfaces only manage visibility scope, and confirm both mutation paths audit their changes.
**Acceptance Scenarios**:
1. **Given** a workspace owner opens membership management, **When** they change a member role, **Then** the role change is stored and audited at workspace scope only.
2. **Given** a workspace owner opens the managed-environment access-scope surface, **When** they add or remove one environment from scope, **Then** the mutation changes environment visibility only and does not expose a second role selector.
### Edge Cases
- What happens when a workspace member has no explicit environment-scope rows and the workspace contains archived or removed managed environments?
- How does the system handle a remembered managed-environment context after a scope row is removed or a workspace membership is deleted?
- What happens when an `OperationRun` belongs to a workspace with no `managed_environment_id` versus one that is environment-bound?
- How does the system handle last-owner protection when workspace role management replaces the current managed-environment owner semantics?
- What happens when local RBAC passes but provider capability from Spec `283` blocks the action afterward?
## Requirements
### Functional Requirements
- **FR-001 Workspace membership is the only role-bearing authorization truth.** Managed-environment-owned resource capabilities MUST resolve from workspace membership, not from a second environment role record.
- **FR-002 Workspace membership is the first entitlement boundary.** Non-members of the current workspace MUST receive deny-as-not-found for workspace-owned and managed-environment-owned routes, actions, and run drilldowns.
- **FR-003 Managed-environment scope is an optional narrowing layer.** The managed-environment overlay MUST only answer whether the current workspace member may access a specific managed environment; it MUST NOT become a second role or capability registry.
- **FR-004 Full workspace inheritance is the default.** If no explicit managed-environment scope narrowing exists for a workspace member, that member inherits environment visibility across the selectable managed environments in the workspace.
- **FR-005 Scoped access is explicit.** If explicit managed-environment scope rows exist for a workspace member, only those environments are visible or openable for that member.
- **FR-006 Capability resolution stays capability-first.** The existing capability registry remains the only capability vocabulary, and environment-owned policies MUST continue to check capabilities rather than raw role strings.
- **FR-007 `User::canAccessTenant()`, Filament tenant selection, remembered tenant context, and related-context navigation MUST use the same workspace-first access contract.**
- **FR-008 Environment-owned policies MUST be retargeted.** `ManagedEnvironment`, `ProviderConnection`, `OperationRun`, `Finding`, `EvidenceSnapshot`, `ReviewPack`, `TenantReview`, and other governance-review or evidence surfaces in scope MUST evaluate workspace membership first, managed-environment scope second, and capability third.
- **FR-009 `OperationRun` authorization MUST preserve mixed workspace and managed-environment behavior.** Workspace-wide runs continue to authorize against workspace membership; environment-bound runs additionally respect managed-environment scope before capability evaluation.
- **FR-010 Membership-management surfaces MUST split concerns.** Workspace membership management remains the only role-editing surface. The current tenant-membership surface MUST either be removed or transformed into environment access-scope management with no second role selector.
- **FR-011 Membership and scope mutations remain audited.** Workspace role changes, environment-scope changes, and last-owner blocks MUST write canonical audit records with no secrets.
- **FR-012 404 versus 403 semantics stay explicit.** Non-membership or out-of-scope access returns `404`; in-scope members missing the capability return `403`.
- **FR-013 Global search and shell context stay safe.** Non-members or out-of-scope actors MUST not receive managed-environment hints through global search or remembered tenant context.
- **FR-014 No compatibility path is introduced.** The runtime MUST not keep dual-write, fallback-role reads, or legacy role-based `ManagedEnvironmentMembership` checks once the cutover lands.
### Non-Functional Requirements
- **NFR-001 Resolver performance remains request-local and cacheable.** The new access-resolution path MUST avoid N+1 membership or scope queries in page tables, run lists, and bulk authorization preflight.
- **NFR-002 Test breadth stays bounded.** Proof focuses on unit resolver behavior, feature authorization behavior, and one browser smoke; no broad browser matrix is justified.
- **NFR-003 Workspace and managed-environment isolation remain explicit and diagnosable.** Audit logs and denied-access logs MUST make the failed boundary diagnosable without leaking raw provider detail.
- **NFR-004 Filament v5 and Livewire v4 remain unchanged.** The feature MUST not introduce view publishing, custom action surfaces outside existing patterns, or asset strategy changes.
## Scope Boundaries *(required for this slice)*
### In Scope
- replacing environment role authority with workspace-first role authority
- modeling explicit managed-environment scope as narrowing only
- retargeting `CapabilityResolver`, `User::canAccessTenant()`, `WorkspaceContext`, and the key environment-owned policies to that contract
- migrating the current tenant-membership management surface so it no longer edits a second role family
- preserving canonical 404 versus 403 semantics across workspace-owned and environment-owned surfaces
- preserving provider-capability follow-through as a downstream gate rather than re-encoding provider rules here
### Non-Goals
- designing a new customer-facing role catalog
- shipping per-environment role overrides or a full environment ACL matrix
- redefining `/system` platform RBAC
- absorbing provider capability, source taxonomy, copy/localization, or no-legacy guardrail work from Specs `283`, `284`, `286`, or `287`
- shipping compatibility aliases or legacy dual-write logic
## Assumptions
- Specs `280`, `281`, and `283` will land or already be available on the eventual implementation branch before runtime work on `285` starts.
- `WorkspaceMembership` and `WorkspaceCapabilityResolver` remain the correct extension points for canonical role-bearing access.
- The current `managed_environment_memberships` persistence can be repurposed or replaced in place because the product is still pre-production.
- The existing capability vocabulary in `App\Support\Auth\Capabilities` remains valid; `285` changes who resolves roles, not the capability names themselves.
- `ProviderConnectionResource` remains non-globally-searchable, while resources already carrying valid search destinations retain those destinations.
## Risks
- dual-role checks may survive in one or more policies or Filament helpers and silently preserve the old model
- environment-scope management could accidentally keep role selectors or owner semantics and reintroduce a second RBAC core under a new label
- `OperationRun` access and remembered tenant context may drift if they do not adopt the same workspace-first access contract as page policies
- enum or capability-map convergence could widen unexpectedly if implementation tries to solve adjacent role-productization concerns in the same slice
## UI Action Matrix *(mandatory when Filament is changed)*
**Action Surface Contract satisfied?**: yes
**UI-FIL-001 satisfied?**: yes. The slice keeps native Filament relation managers, native confirmation modals, and existing shared enforcement helpers. No local Blade replacement or asset exception is planned.
**UX-001 satisfied?**: yes for the touched surfaces in scope. The feature reuses existing resource and relation-manager shells rather than introducing custom Create/Edit/View layouts.
| 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 |
|---|---|---|---|---|---|---|---|---|---|---|
| Workspace membership management | `apps/platform/app/Filament/Resources/Workspaces/RelationManagers/WorkspaceMembershipsRelationManager.php` | `Add member` | Inline relation-manager row focus inside the workspace detail page; no redundant `View` action | `Change role`, `Remove member` | none | `Add member` | none | native modal submit and cancel | yes | `Remove member` stays destructive, confirmation-protected, server-authorized, and last-owner-safe |
| Managed-environment access-scope management | `apps/platform/app/Filament/Resources/TenantResource/Pages/ManageTenantMemberships.php` plus `TenantMembershipsRelationManager.php` | `Add allowed environment` | Inline relation-manager or page-local scoped list; no separate inspect route | `Edit scope`, `Remove access` | none | `Add allowed environment` | none | native modal submit and cancel | yes | The surface may not expose a role selector, owner semantics, or a second role-bearing mutation path |
## Key Entities
- **WorkspaceMembership**: existing workspace-owned, role-bearing membership pivot that remains the only source for role and capability derivation.
- **ManagedEnvironmentAccessScope**: logical successor to the current managed-environment membership semantics; stores optional visibility narrowing only and never grants capabilities by itself.
- **ManagedEnvironment authorization decision**: derived, non-persisted access payload used by `User`, `WorkspaceContext`, policies, and run drilldowns to evaluate membership, scope, capability, and denial outcome consistently.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: In the bounded proof suite, a workspace member with the required workspace role and no explicit scope rows can open at least one provider-connection surface and one governance-artifact surface in the workspace without any role-bearing managed-environment membership row.
- **SC-002**: In the bounded proof suite, a workspace member narrowed to one managed environment receives `404` for a sibling environment in the same workspace while still receiving the expected capability outcome inside the allowed environment.
- **SC-003**: The retargeted membership surfaces expose exactly one role-editing plane at workspace scope and zero role selectors on the managed-environment scope surface, while every destructive membership or scope mutation remains confirmation-protected.
- **SC-004**: The named unit, feature, browser, and dirty-file formatting commands in this package pass without widening the proof family beyond the files listed in `spec.md`, `plan.md`, `quickstart.md`, and `tasks.md`.