# 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`.