# Research: Workspace-first RBAC & Environment Access Scoping

## Scope of this research

This note records the repo-truth decisions behind Feature `285`. It stays prep-only and does not introduce runtime implementation.

## Sources reviewed

- `docs/product/roadmap.md`
- `docs/product/spec-candidates.md`
- `specs/280-workspace-tenancy-environment-routing/`
- `specs/281-provider-connection-scope/`
- `specs/283-provider-capability-registry/`
- `specs/062-tenant-rbac-v1/`
- `specs/065-tenant-rbac-v1/`
- `apps/platform/app/Models/WorkspaceMembership.php`
- `apps/platform/app/Models/ManagedEnvironmentMembership.php`
- `apps/platform/app/Models/User.php`
- `apps/platform/app/Services/Auth/WorkspaceCapabilityResolver.php`
- `apps/platform/app/Services/Auth/CapabilityResolver.php`
- `apps/platform/app/Support/Workspaces/WorkspaceContext.php`
- `apps/platform/app/Policies/ProviderConnectionPolicy.php`
- `apps/platform/app/Policies/OperationRunPolicy.php`
- `apps/platform/app/Policies/FindingPolicy.php`
- `apps/platform/app/Policies/EvidenceSnapshotPolicy.php`
- `apps/platform/app/Policies/ReviewPackPolicy.php`
- `apps/platform/app/Policies/TenantReviewPolicy.php`
- `apps/platform/app/Filament/Resources/TenantResource/Pages/ManageTenantMemberships.php`
- `apps/platform/app/Filament/Resources/TenantResource/RelationManagers/TenantMembershipsRelationManager.php`

## Decision 1: Workspace membership is already the right canonical role-bearing seam

### Findings

- `WorkspaceMembership` already exists as a workspace-owned role-bearing pivot.
- `WorkspaceCapabilityResolver` already resolves workspace membership, caches it per request, and exposes `getRole`, `can`, and `isMember`.
- Workspace-owned policies and management flows already rely on that seam.

### Decision

`WorkspaceMembership` remains the only role-bearing truth for Feature `285`.

### Why

The repo already contains the correct workspace-first substrate. Adding a third RBAC layer or preserving a second role-bearing environment path would increase drift instead of resolving it.

## Decision 2: The real repo problem is dual role-bearing truth, not missing RBAC from scratch

### Findings

- `ManagedEnvironmentMembership` is still treated as a role-bearing pivot.
- `CapabilityResolver` still resolves access from managed-environment membership and `RoleCapabilityMap`.
- `User::canAccessTenant()` still relies on `CapabilityResolver::isMember()`.
- `WorkspaceContext` still reaches managed-environment access through that tenant-first user path.
- Key policies for provider connections, runs, findings, evidence, review packs, and tenant reviews use different combinations of workspace membership and managed-environment membership.
- `ManageTenantMemberships` and `TenantMembershipsRelationManager` still expose operator-facing role editing for managed environments.

### Decision

Feature `285` is a consolidation cutover. It must replace the split role-bearing model rather than stack a new compatibility layer on top.

### Why

The raw candidate text talks about `TenantMembership`, but current repo truth uses `ManagedEnvironmentMembership`. The real cutover target is therefore the duplicated role-bearing access model itself.

## Decision 3: Managed-environment membership becomes a narrow access-scope overlay only

### Findings

- The roadmap still wants environment access scoping to remain possible after the workspace-first cutover.
- The repo has many environment-owned resources where full workspace inheritance may need narrowing.
- Replacing environment role authority does not require removing the concept of selective environment visibility.

### Decision

The current managed-environment membership persistence may survive only as a logical access-scope overlay or be replaced in place by an equivalent successor. It must not remain role-bearing.

### Why

This preserves the narrow product need for selective environment visibility without rebuilding a second ACL system.

## Decision 4: The default environment access rule is inheritance, not mandatory per-environment grants

### Findings

- Workspace-first tenancy is meant to reduce operator friction, not require duplicate allowlists for every member.
- The current product already anchors `ManagedEnvironment` to `workspace_id`.

### Decision

If a workspace member has no explicit scope rows, they inherit visibility to the managed environments in that workspace. If scope rows exist, they narrow visibility to an allowlist.

### Why

This is the smallest rule that preserves optional narrowing while keeping workspace membership authoritative.

## Decision 5: `CapabilityResolver` should be retargeted, not replaced by another resolver family

### Findings

- `CapabilityResolver` is already the shared entry point for many environment-owned capability checks.
- Policies, `User`, and other helpers already call into it.

### Decision

Retarget `CapabilityResolver` to derive the current role from workspace membership and use managed-environment scope only for visibility narrowing.

### Why

Replacing callers across the repo with a third resolver type would add avoidable migration spread.

## Decision 6: `User` and `WorkspaceContext` must share the same access contract

### Findings

- `User::getTenants()`, `User::getDefaultTenant()`, and `User::canAccessTenant()` currently shape Filament tenant access.
- `WorkspaceContext` tracks current workspace, remembered tenant context, and initial workspace resolution, but still delegates tenant access to the old user path.

### Decision

`User` and `WorkspaceContext` must consume the same workspace-first access contract so context selection, remembered tenant state, and page policy outcomes cannot drift.

### Why

If the shell and the policies resolve access differently, operators will continue to see allowed selection with denied pages or denied selection with allowed deep links.

## Decision 7: Membership-management UI must split role authority from visibility scope

### Findings

- `ManageTenantMemberships` currently titles the page around tenant memberships and says managed-environment access is managed there.
- `TenantMembershipsRelationManager` still offers add, change, and remove role actions using tenant capability checks.
- Workspace membership management already exists separately.

### Decision

The operator-facing role editor remains the workspace membership surface. The current managed-environment membership page is removed or retargeted into access-scope management with no second role selector.

### Why

Leaving the current UI intact would let operators recreate the duplicate role model even after backend changes land.

## Decision 8: OperationRun authorization stays shared but must follow the same workspace-first rule

### Findings

- `OperationRunPolicy` currently mixes workspace membership, managed-environment membership, and required capability logic.
- Some runs are workspace-bound; others are managed-environment-bound.

### Decision

Workspace-bound runs authorize from workspace membership only. Managed-environment-bound runs authorize from workspace membership, then environment scope, then required capability.

### Why

This preserves the existing distinction between workspace-wide and environment-bound operations without keeping role truth in two places.

## Rejected alternatives

### Keep both role-bearing membership models and synchronize them

Rejected because it adds more compatibility logic to a pre-production codebase and preserves the core ambiguity that `285` is meant to remove.

### Introduce a third access resolver beside the current two

Rejected because most callers already rely on `CapabilityResolver` or `WorkspaceCapabilityResolver`. A third resolver family would create a longer migration window and more drift.

### Remove all environment-level scoping entirely

Rejected because the roadmap explicitly keeps environment access scoping in scope after the workspace-first cutover.

### Turn environment scope into per-environment role overrides

Rejected because that would introduce a second ACL product and exceed the reserved scope of `285`.

## Resulting design constraints

- No compatibility shim or dual-write path.
- No new role family.
- No new persisted source of truth.
- No widening into provider capability, route-shell, source taxonomy, copy/localization, or cutover-guardrail work reserved for adjacent specs.
- The proof set stays bounded to unit, feature, and one browser smoke.
