[feat] Move RBAC enforcement from ee to oss#4801
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughRBAC enforcement now runs unconditionally in OSS routers and shared services, with the shared role and permission catalog moved into OSS and EE modules reduced to re-exports or EE-only overlays. Tests, docs, and workspace settings UI were updated to match the new edition split. ChangesRBAC in OSS
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
RBAC enforcement fromee to ossRBAC enforcement from ee to oss
There was a problem hiding this comment.
Pull request overview
This PR moves RBAC permission vocabulary, role catalog, and enforcement into the OSS API layer so permission checks run in both OSS and EE, while preserving the EE-only “RBAC not entitled ⇒ allow-all” bypass via a function-local is_ee()-guarded import.
Changes:
- Introduces OSS-owned RBAC modules (
permissions/{types,controls,service}.py) plus an OSS role-controls composition root (oss/src/core/access/controls.py) and EE re-export shims. - Removes
is_ee()gating from many OSS routers socheck_action_access(...)runs unconditionally, and updates the access router to stop short-circuiting OSS to allow-all. - Adds cross-edition tests and design docs to validate/describe the behavior flip and EE entitlement seam.
Reviewed changes
Copilot reviewed 47 out of 50 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/designs/rbac-in-oss/research.md | Research writeup capturing current RBAC state pre-move. |
| docs/designs/rbac-in-oss/proposal.md | Proposal for OSS-owned RBAC enforcement + EE entitlement seam. |
| docs/designs/rbac-in-oss/plan.md | Implementation plan and “as implemented” outcome notes. |
| docs/designs/rbac-in-oss/gap.md | Gap analysis covering entitlement, lockout, symbol resolution, and tests. |
| docs/designs/rbac-in-oss/checklist.md | Inventory/checklist used to ensure call-site coverage during un-gating. |
| api/oss/tests/pytest/unit/environments/test_commit_validation.py | Stubs RBAC check to keep environment commit-validation unit tests focused. |
| api/oss/tests/pytest/unit/access/test_role_controls_oss.py | New unit tests asserting OSS ignores custom-role env vars and uses defaults. |
| api/oss/tests/pytest/unit/access/init.py | Package init (no behavioral change). |
| api/oss/tests/pytest/acceptance/accounts/test_rbac_enforcement.py | OSS acceptance tests for invite/member-removal enforcement (owner allow / viewer deny). |
| api/oss/src/utils/env.py | Updates AccessConfig docstring to clarify OSS vs EE env-var behavior. |
| api/oss/src/services/db_manager.py | Adds RBAC-related lookups + fixes org membership role seeding + adds project-by-workspace helper. |
| api/oss/src/routers/workspace_router.py | Hoists enforcement checks above EE/OSS forks for workspace member removal and role listing. |
| api/oss/src/routers/user_profile.py | Makes reset-password permission checks run in OSS too. |
| api/oss/src/routers/organization_router.py | Makes invite/resend permission checks run in OSS too; adjusts role description resolution. |
| api/oss/src/routers/api_key_router.py | Makes API key CRUD permission checks run in OSS too. |
| api/oss/src/core/access/permissions/types.py | New OSS-owned Permission/Role enums used across editions. |
| api/oss/src/core/access/permissions/service.py | New OSS-owned RBAC enforcement service with EE-only entitlement bypass. |
| api/oss/src/core/access/permissions/controls.py | New OSS role catalog builder; EE-only overrides applied via function-local import. |
| api/oss/src/core/access/permissions/init.py | Package init (no behavioral change). |
| api/oss/src/core/access/controls.py | New OSS role-controls composition root + get_role* accessors and hash. |
| api/oss/src/core/access/init.py | Package init (no behavioral change). |
| api/oss/src/apis/fastapi/webhooks/router.py | Removes EE-gating so webhook RBAC checks always run. |
| api/oss/src/apis/fastapi/vault/router.py | Removes EE-gating so vault RBAC checks always run. |
| api/oss/src/apis/fastapi/tracing/router.py | Removes EE-gating for RBAC while keeping EE-only entitlement/meter logic gated. |
| api/oss/src/apis/fastapi/traces/router.py | Removes EE-gating so traces RBAC checks always run. |
| api/oss/src/apis/fastapi/tools/router.py | Removes EE-gating so tools RBAC checks always run. |
| api/oss/src/apis/fastapi/testcases/router.py | Removes EE-gating so testcases RBAC checks always run. |
| api/oss/src/apis/fastapi/queries/router.py | Removes EE-gating so queries RBAC checks always run. |
| api/oss/src/apis/fastapi/otlp/router.py | Removes EE-gating for RBAC while keeping EE-only entitlements gated. |
| api/oss/src/apis/fastapi/legacy_variants/router.py | Removes EE-gating so legacy variant RBAC checks always run. |
| api/oss/src/apis/fastapi/invocations/router.py | Removes EE-gating so invocations RBAC checks always run. |
| api/oss/src/apis/fastapi/folders/router.py | Removes EE-gating so folders RBAC checks always run. |
| api/oss/src/apis/fastapi/environments/utils.py | Moves RBAC imports to OSS so deploy checks can run in OSS. |
| api/oss/src/apis/fastapi/environments/router.py | Removes EE-gating so environments RBAC checks always run. |
| api/oss/src/apis/fastapi/annotations/router.py | Removes EE-gating so annotations RBAC checks always run. |
| api/oss/src/apis/fastapi/access/router.py | Removes OSS allow-all short-circuit; keeps EE-only credits meter gated. |
| api/ee/tests/pytest/unit/test_access_controls.py | Repoints EE unit tests to new plan/role override locations. |
| api/ee/tests/pytest/acceptance/accounts/test_rbac_enforcement_ee.py | EE acceptance tests mirroring OSS enforcement behavior (entitled plan). |
| api/ee/src/services/db_manager_ee.py | Re-exports moved RBAC lookups from OSS db_manager. |
| api/ee/src/core/access/permissions/types.py | EE re-export shim for OSS-owned Permission/Role enums. |
| api/ee/src/core/access/permissions/service.py | EE re-export shim for OSS-owned RBAC enforcement service. |
| api/ee/src/core/access/permissions/role_overrides.py | New EE-only role override parsing and application layer. |
| api/ee/src/core/access/permissions/controls.py | EE re-export shim for OSS role-controls builder/constants. |
| api/ee/src/core/access/controls.py | EE access-controls now focuses on plans; re-exports OSS role accessors. |
| api/AGENTS.md | Documents new RBAC enforcement ownership and OSS↔EE import constraints. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@CodeRabbit review |
✅ Action performedReview finished.
|
Railway Preview Environment
Updated at 2026-06-25T13:58:14.317Z |
There was a problem hiding this comment.
Actionable comments posted: 15
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
api/oss/src/apis/fastapi/environments/router.py (1)
1387-1406: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winRemove the leftover EE gate around guarded-environment checks.
Line 1388 and Line 1449 still skip
DEPLOY_ENVIRONMENTSchecks in OSS. Sinceguard_simple_environmentnow runs in OSS, a guarded environment can be edited or archived by a user with onlyEDIT_ENVIRONMENTS.🛡️ Proposed fix
- # If guarded, require DEPLOY_ENVIRONMENTS permission - if is_ee(): - environment = await self.environments_service.fetch_environment( - project_id=UUID(request.state.project_id), - environment_ref=Reference(id=environment_id), - ) - is_guarded = False - if environment and environment.flags: - flags = environment.flags - if isinstance(flags, EnvironmentFlags): - is_guarded = bool(getattr(flags, "is_guarded", False)) - elif isinstance(flags, dict): - is_guarded = bool(flags.get("is_guarded", False)) - if is_guarded: - if not await check_action_access( # type: ignore - user_uid=request.state.user_id, - project_id=request.state.project_id, - permission=Permission.DEPLOY_ENVIRONMENTS, # type: ignore - ): - raise FORBIDDEN_EXCEPTION # type: ignore + environment = await self.environments_service.fetch_environment( + project_id=UUID(request.state.project_id), + environment_ref=Reference(id=environment_id), + ) + is_guarded = False + if environment and environment.flags: + flags = environment.flags + if isinstance(flags, EnvironmentFlags): + is_guarded = bool(getattr(flags, "is_guarded", False)) + elif isinstance(flags, dict): + is_guarded = bool(flags.get("is_guarded", False)) + if is_guarded and not await check_action_access( # type: ignore + user_uid=request.state.user_id, + project_id=request.state.project_id, + permission=Permission.DEPLOY_ENVIRONMENTS, # type: ignore + ): + raise FORBIDDEN_EXCEPTION # type: ignore @@ - # If guarded, require DEPLOY_ENVIRONMENTS permission - if is_ee(): - is_guarded = False - if environment.flags: - flags = environment.flags - if isinstance(flags, EnvironmentFlags): - is_guarded = bool(getattr(flags, "is_guarded", False)) - elif isinstance(flags, dict): - is_guarded = bool(flags.get("is_guarded", False)) - if is_guarded: - if not await check_action_access( # type: ignore - user_uid=request.state.user_id, - project_id=request.state.project_id, - permission=Permission.DEPLOY_ENVIRONMENTS, # type: ignore - ): - raise FORBIDDEN_EXCEPTION # type: ignore + is_guarded = False + if environment.flags: + flags = environment.flags + if isinstance(flags, EnvironmentFlags): + is_guarded = bool(getattr(flags, "is_guarded", False)) + elif isinstance(flags, dict): + is_guarded = bool(flags.get("is_guarded", False)) + if is_guarded and not await check_action_access( # type: ignore + user_uid=request.state.user_id, + project_id=request.state.project_id, + permission=Permission.DEPLOY_ENVIRONMENTS, # type: ignore + ): + raise FORBIDDEN_EXCEPTION # type: ignoreAs per coding guidelines, RBAC enforcement is an OSS feature and handlers should not edition-gate
check_action_access(...). Based on PR objectives, the only edition difference should be the EE plan gate inside the enforcement seam.Also applies to: 1448-1463
Source: Coding guidelines
api/oss/src/apis/fastapi/environments/utils.py (1)
50-51: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winDo not short-circuit guarded deploy checks in OSS.
Line 50 returns before checking guarded environments whenever
not is_ee(). This lets OSS commit/deploy paths skipDEPLOY_ENVIRONMENTSeven though RBAC enforcement has moved to OSS.🛡️ Proposed fix
- if not is_ee() or not environment_id: + if not environment_id: returnAs per coding guidelines, RBAC enforcement is an OSS feature and
check_action_access(...)should not be edition-gated. Based on PR objectives, the EE-specific behavior should stay in the enforcement/entitlement seam, not in API helpers.Source: Coding guidelines
api/oss/src/apis/fastapi/applications/router.py (1)
597-616: 🔒 Security & Privacy | 🟠 MajorAdd
exclude=[HTTPException]to four@suppress_exceptions(...)decorators to preserve RBAC denial responses.These handlers raise
FORBIDDEN_EXCEPTION(anHTTPException) inside@suppress_exceptions(...)decorators without excluding HTTP exceptions. Whenexcludeis not provided, the decorator suppresses all exception types, includingHTTPException, returning200with default/empty responses instead of403Forbidden. This breaks RBAC enforcement.Confirm by checking the decorator signatures—they lack
exclude=[HTTPException]unlike adjacent protected endpoints in other routers (e.g.,folders/router.py,queries/router.py).Lines affected
- Line 597:
fetch_application- Line 736:
query_applications- Line 1574:
query_application_revisions- Line 1880:
fetch_simple_applicationSource: Coding guidelines
🧹 Nitpick comments (2)
api/oss/src/routers/organization_router.py (1)
35-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove the access-controls import to module scope.
_role_description()performs a local import for a catalog/config lookup on every call; this should be a module-level import unless there is a circular dependency.♻️ Proposed cleanup
from oss.src.core.access.permissions.service import check_action_access from oss.src.core.access.permissions.types import Permission +from oss.src.core.access.controls import get_role_description from oss.src.services.db_manager import get_user_org_and_workspace_id def _role_description(role: str) -> str: - """Resolve a workspace-role description from the effective access-controls - catalog (env-overridable via AGENTA_ACCESS_ROLES).""" - from oss.src.core.access.controls import get_role_description - return get_role_description("workspace", role) or ""As per coding guidelines, "
api/**/*.py: Avoid local imports inside helper functions for configuration lookup; prefer module-level imports unless there is a proven circular dependency."Source: Coding guidelines
api/oss/src/routers/workspace_router.py (1)
77-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDrop the env-override comment in the OSS router.
This comment narrates the list comprehension and can mislead OSS readers because custom-role env vars are intentionally EE-only. The code is clearer without it.
♻️ Proposed cleanup
- # Resolve via access-controls (env-overridable via AGENTA_ACCESS_ROLES). workspace_roles_with_description = [As per coding guidelines, "Keep AI-generated in-code comments minimal" and comment only the non-obvious why; the PR objective also states custom-role env vars are ignored in OSS.
Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 3342c318-f160-4e15-8216-12cda2ebd405
📒 Files selected for processing (50)
api/AGENTS.mdapi/ee/src/core/access/controls.pyapi/ee/src/core/access/permissions/controls.pyapi/ee/src/core/access/permissions/role_overrides.pyapi/ee/src/core/access/permissions/service.pyapi/ee/src/core/access/permissions/types.pyapi/ee/src/services/db_manager_ee.pyapi/ee/tests/pytest/acceptance/accounts/test_rbac_enforcement_ee.pyapi/ee/tests/pytest/unit/test_access_controls.pyapi/oss/src/apis/fastapi/access/router.pyapi/oss/src/apis/fastapi/annotations/router.pyapi/oss/src/apis/fastapi/applications/router.pyapi/oss/src/apis/fastapi/environments/router.pyapi/oss/src/apis/fastapi/environments/utils.pyapi/oss/src/apis/fastapi/evaluations/router.pyapi/oss/src/apis/fastapi/evaluators/router.pyapi/oss/src/apis/fastapi/folders/router.pyapi/oss/src/apis/fastapi/invocations/router.pyapi/oss/src/apis/fastapi/legacy_variants/router.pyapi/oss/src/apis/fastapi/otlp/router.pyapi/oss/src/apis/fastapi/queries/router.pyapi/oss/src/apis/fastapi/testcases/router.pyapi/oss/src/apis/fastapi/testsets/router.pyapi/oss/src/apis/fastapi/tools/router.pyapi/oss/src/apis/fastapi/traces/router.pyapi/oss/src/apis/fastapi/tracing/router.pyapi/oss/src/apis/fastapi/vault/router.pyapi/oss/src/apis/fastapi/webhooks/router.pyapi/oss/src/apis/fastapi/workflows/router.pyapi/oss/src/core/access/__init__.pyapi/oss/src/core/access/controls.pyapi/oss/src/core/access/permissions/__init__.pyapi/oss/src/core/access/permissions/controls.pyapi/oss/src/core/access/permissions/service.pyapi/oss/src/core/access/permissions/types.pyapi/oss/src/routers/api_key_router.pyapi/oss/src/routers/organization_router.pyapi/oss/src/routers/user_profile.pyapi/oss/src/routers/workspace_router.pyapi/oss/src/services/db_manager.pyapi/oss/src/utils/env.pyapi/oss/tests/pytest/acceptance/accounts/test_rbac_enforcement.pyapi/oss/tests/pytest/unit/access/__init__.pyapi/oss/tests/pytest/unit/access/test_role_controls_oss.pyapi/oss/tests/pytest/unit/environments/test_commit_validation.pydocs/designs/rbac-in-oss/checklist.mddocs/designs/rbac-in-oss/gap.mddocs/designs/rbac-in-oss/plan.mddocs/designs/rbac-in-oss/proposal.mddocs/designs/rbac-in-oss/research.md
mmabrouk
left a comment
There was a problem hiding this comment.
@jp-agenta thanks for the pr. Can you please include the docs update (very minor, just making sure it's in sync with reality) with this pr
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 8b40be54-ea26-44a7-b82a-a922c597794e
📒 Files selected for processing (3)
docs/blog/entries/open-sourcing-agenta.mdxdocs/docs/administration/access-control/01-organizations.mdxdocs/docs/administration/access-control/03-rbac.mdx
✅ Files skipped from review due to trivial changes (1)
- docs/blog/entries/open-sourcing-agenta.mdx
Fixes from the PR #4801 review pass: - Bind invite/resend authorization to the target org (validate the workspace's project belongs to organization_id; reuse that project for the service call) so the RBAC check cannot pass for the wrong org. - Cast workspace_id to UUID in get_workspace_members (was binding a raw string to a UUID column). - Gate evaluator variant/revision-log routes with evaluator permissions instead of workflow permissions. - Move FORBIDDEN_EXCEPTION out of the core RBAC service into apis/fastapi/shared/exceptions.py (no HTTPException in core); repoint importers and the EE shim. - Invalidate the check_action_access cache on every membership/role mutation and add the controls hash to its key, so revocation takes effect immediately instead of after the TTL. - Reuse the open session for the user lookup in get_user_org_and_workspace_id; include role permissions in the controls hash; enforce both fields for new overlay roles; tighten the tools slug regex to fullmatch. - Docs: RBAC is always-on in OSS (rbac/organizations mdx + blog); design findings ledger. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
api/oss/src/routers/organization_router.py (1)
363-370: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winUse the authorized project for the EE resend path.
The RBAC check is scoped to
project.idresolved fromworkspace_id, but the EE resend call still uses ambientrequest.state.project_id. If those differ, authorization and resend lookup/mutation target different projects. Passstr(project.id)here, matching the invite and OSS resend paths.🛡️ Proposed fix
invite_user = await workspace_manager.resend_user_workspace_invite( payload=payload, - project_id=request.state.project_id, + project_id=str(project.id), organization_id=organization_id, workspace_id=workspace_id, user_uid=request.state.user_id, )api/oss/src/apis/fastapi/evaluators/router.py (1)
1281-1286: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winRequire environment read access for environment-backed retrieval.
Line 1281 checks only evaluator access, but this handler also dereferences environment refs/key and can reveal which evaluator revision an environment pins. Add an environment read check inside the
environment_lookup_requestedbranch.🔒 Proposed conditional environment authorization
if environment_lookup_requested: if not environment_refs_requested: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Environment-backed evaluator retrieve requires environment refs.", ) + if not await check_action_access( # type: ignore + user_uid=request.state.user_id, + project_id=request.state.project_id, + permission=Permission.VIEW_ENVIRONMENTS, # type: ignore + ): + raise FORBIDDEN_EXCEPTION # type: ignore + if not key: if evaluator_ref and evaluator_ref.slug: key = f"{evaluator_ref.slug}.revision"Also applies to: 1329-1334
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 3df7c0d0-022c-4dab-bf87-490834334ace
📒 Files selected for processing (36)
api/ee/src/apis/fastapi/events/router.pyapi/ee/src/core/access/permissions/role_overrides.pyapi/ee/src/core/access/permissions/service.pyapi/ee/src/services/db_manager_ee.pyapi/ee/tests/pytest/unit/test_access_controls.pyapi/ee/tests/pytest/unit/test_controls_env_override.pyapi/oss/src/apis/fastapi/annotations/router.pyapi/oss/src/apis/fastapi/applications/router.pyapi/oss/src/apis/fastapi/environments/router.pyapi/oss/src/apis/fastapi/environments/utils.pyapi/oss/src/apis/fastapi/evaluations/router.pyapi/oss/src/apis/fastapi/evaluators/router.pyapi/oss/src/apis/fastapi/folders/router.pyapi/oss/src/apis/fastapi/invocations/router.pyapi/oss/src/apis/fastapi/legacy_variants/router.pyapi/oss/src/apis/fastapi/otlp/router.pyapi/oss/src/apis/fastapi/queries/router.pyapi/oss/src/apis/fastapi/shared/exceptions.pyapi/oss/src/apis/fastapi/testcases/router.pyapi/oss/src/apis/fastapi/testsets/router.pyapi/oss/src/apis/fastapi/tools/router.pyapi/oss/src/apis/fastapi/traces/router.pyapi/oss/src/apis/fastapi/tracing/router.pyapi/oss/src/apis/fastapi/webhooks/router.pyapi/oss/src/apis/fastapi/workflows/router.pyapi/oss/src/core/access/controls.pyapi/oss/src/core/access/permissions/service.pyapi/oss/src/core/access/permissions/types.pyapi/oss/src/routers/organization_router.pyapi/oss/src/services/db_manager.pyapi/oss/tests/pytest/unit/access/test_role_controls_oss.pydocs/designs/rbac-in-oss/checklist.mddocs/designs/rbac-in-oss/findings.mddocs/designs/rbac-in-oss/plan.mddocs/designs/rbac-in-oss/proposal.mddocs/designs/rbac-in-oss/research.md
💤 Files with no reviewable changes (1)
- api/oss/src/core/access/permissions/types.py
✅ Files skipped from review due to trivial changes (4)
- api/ee/src/apis/fastapi/events/router.py
- docs/designs/rbac-in-oss/checklist.md
- docs/designs/rbac-in-oss/research.md
- docs/designs/rbac-in-oss/plan.md
🚧 Files skipped from review as they are similar to previous changes (21)
- api/oss/src/apis/fastapi/environments/utils.py
- api/ee/src/core/access/permissions/service.py
- docs/designs/rbac-in-oss/proposal.md
- api/oss/src/apis/fastapi/legacy_variants/router.py
- api/oss/src/core/access/controls.py
- api/oss/src/apis/fastapi/webhooks/router.py
- api/oss/src/apis/fastapi/testcases/router.py
- api/oss/src/apis/fastapi/traces/router.py
- api/oss/src/apis/fastapi/folders/router.py
- api/oss/src/apis/fastapi/otlp/router.py
- api/oss/src/apis/fastapi/tools/router.py
- api/oss/src/apis/fastapi/invocations/router.py
- api/ee/src/core/access/permissions/role_overrides.py
- api/oss/src/apis/fastapi/annotations/router.py
- api/oss/src/apis/fastapi/tracing/router.py
- api/oss/src/apis/fastapi/queries/router.py
- api/oss/src/apis/fastapi/applications/router.py
- api/oss/src/apis/fastapi/evaluations/router.py
- api/oss/src/apis/fastapi/testsets/router.py
- api/oss/src/apis/fastapi/environments/router.py
- api/oss/src/apis/fastapi/workflows/router.py
RBAC enforcement is on in OSS, but roles were invisible in the OSS UI: - Move the GET /access/roles endpoint from the EE access router to OSS (it uses get_roles/SCOPES, which now live in OSS). EE keeps /plans and inherits /roles via its additive mount, so both editions serve it. - Web members UI gated role display/selection behind `isEE() && hasRBAC`, which is false in OSS. Switch the gate to `!isEE() || hasRBAC` so OSS always shows the role column, the populated invite-role selector, and the edit-role dropdown; EE keeps the plan gate (not-entitled EE still hides roles and shows the upsell). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
OSS always enforces RBAC; Cloud/Enterprise follow the plan (non-RBAC plans grant full access). Addresses review feedback that the prior wording implied EE always enforces roles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Context
RBAC enforcement only ran in EE. In OSS, every permission check was wrapped in
if is_ee():, so OSS was effectively allow-all: any member could invite users, edit environments, delete testsets, and so on, regardless of role. Memberships and roles already existed in OSS; only the enforcement was missing.This moves the permission vocabulary, role catalog, and enforcement service into OSS so RBAC is always-on in both editions. The only edition difference left is the EE plan gate: on a plan that is not RBAC-entitled, EE still grants allow-all. That gate now lives behind a function-local
is_ee()import inside the OSS service, so OSS never importsee.*at module top.Changes
The RBAC code moved from
ee/src/core/access/tooss/src/core/access/, and EE keeps thin re-export shims at the old paths so existing EE imports keep working.permissions/{types,controls,service}.py, theget_role*accessors, and four DB lookups (get_organization,get_workspace_members,get_project_members,get_user_org_and_workspace_id) plusget_project_by_workspace.check_project_has_role_or_permissionenforces in both editions. TheFlag.RBAC-not-entitled allow-all bypass runs only underis_ee()via a function-local import of the EE entitlements service. OSS falls through and enforces.Before, in an OSS router:
After:
check_action_accessunconditionally. Entitlement and meter calls (check_entitlements,Counter) stayis_ee()-gated.organization_routerandworkspace_routerhad separate EE and OSS flows. The enforcement check was hoisted above the edition fork so invite, resend, and member-removal enforce in both editions.AGENTA_ACCESS_ROLESandAGENTA_ACCESS_ROLES_OVERLAYare an EE feature. The override parsers moved toee/src/core/access/permissions/role_overrides.py. OSS returns the code-default role catalog and ignores those env vars.add_user_to_workspace_and_orgnow writes the explicit role on the org member row instead of silently defaulting toviewer, so an invited admin is actually an admin.Tests / notes
role_overridesmodule.ruff formatandruff checkclean acrossapi/. Every touched module imports under bothAGENTA_LICENSE=ossand=ee. Full suites green in both editions: EE api 1797 passed, OSS api 1527 passed, with SDK and services green.if is_ee():wrappers. Reviewing withgit diff -wshows the real change (~300 lines).What to QA
viewermember of a project and try to invite a user or edit an environment. You should get 403. Before this change you could do it.owneror anadmin, the same actions succeed.admin, accept, and confirm they land with theadminrole and can manage members (not silently downgraded toviewer).