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