mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-24 00:39:35 +08:00
fix(authz): prefer project over workspace in _resolve_flow_domain
Casbin g2 inheritance is directional: a child resource inherits from its parent, not the other way round. Declaring `g2, project:xyz, workspace:abc` lets a workspace-scoped grant flow down to checks made against project:xyz, but a project-scoped grant does NOT flow up to checks made against workspace:abc. The original precedence picked workspace whenever it was set, which made project-scoped grants invisible to the enforcer the moment a workspace was also known. Flip the precedence so the more specific domain (project) wins — project-scoped grants now match directly, and workspace-scoped grants still match via g2 inheritance. Both ids stay in the enforce context for ABAC. - _resolve_flow_domain returns project first, then workspace, then "*". - Docstrings on both _resolve_flow_domain and its callers updated. - test_ensure_flow_permission_workspace_beats_project renamed to test_ensure_flow_permission_project_beats_workspace with inverted assertion and an explanation of g2 directionality. - test_resolve_flow_domain_precedence updated accordingly. Deployment helper uses the same function but only one deployment test passes both ids; deployment behavior is unchanged in tests that pass project_id only.
This commit is contained in:
@ -150,19 +150,25 @@ async def ensure_permission(
|
||||
def _resolve_flow_domain(workspace_id: UUID | None, folder_id: UUID | None) -> str:
|
||||
"""Pick the most specific Casbin domain for a flow check.
|
||||
|
||||
Precedence (outer → inner):
|
||||
1. ``workspace:{workspace_id}`` when a workspace is set,
|
||||
2. ``project:{folder_id}`` when a folder is set,
|
||||
Precedence (inner → outer):
|
||||
1. ``project:{folder_id}`` when a folder is set,
|
||||
2. ``workspace:{workspace_id}`` when only a workspace is set,
|
||||
3. ``"*"`` when neither is set.
|
||||
|
||||
Enterprise Casbin policies link the two via ``g2`` (e.g.
|
||||
``g2, project:xyz, workspace:abc``) so a workspace-scoped role
|
||||
grant automatically applies to every project inside it.
|
||||
``g2, project:xyz, workspace:abc``) so that when the enforcer checks
|
||||
against the **project** domain, both project-scoped and workspace-scoped
|
||||
role grants match (workspace grants flow down to projects via g2).
|
||||
Passing the workspace domain when a project is known would make
|
||||
project-scoped grants invisible because g2 is directional — children
|
||||
inherit from parents, not the other way round. ``workspace_id`` is also
|
||||
forwarded in the enforce context for plugins that prefer ABAC-style
|
||||
matchers.
|
||||
"""
|
||||
if workspace_id is not None:
|
||||
return f"workspace:{workspace_id}"
|
||||
if folder_id is not None:
|
||||
return f"project:{folder_id}"
|
||||
if workspace_id is not None:
|
||||
return f"workspace:{workspace_id}"
|
||||
return "*"
|
||||
|
||||
|
||||
|
||||
@ -255,8 +255,14 @@ async def test_ensure_flow_permission_falls_back_to_project_domain(monkeypatch,
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_ensure_flow_permission_workspace_beats_project(monkeypatch, fake_user):
|
||||
"""When both workspace_id and folder_id are set, workspace wins as the domain."""
|
||||
async def test_ensure_flow_permission_project_beats_workspace(monkeypatch, fake_user):
|
||||
"""When both workspace_id and folder_id are set, the project domain wins.
|
||||
|
||||
Casbin g2 is directional (children inherit from parents). Passing the
|
||||
project domain lets workspace-scoped grants flow down via g2 *and* keeps
|
||||
project-scoped grants visible. Passing workspace would make project
|
||||
grants invisible — that was the original (buggy) behavior.
|
||||
"""
|
||||
_install_settings(monkeypatch, authz_enabled=True)
|
||||
service = _StubAuthorizationService(allow=True)
|
||||
_install_authz(monkeypatch, service)
|
||||
@ -273,8 +279,8 @@ async def test_ensure_flow_permission_workspace_beats_project(monkeypatch, fake_
|
||||
folder_id=folder_id,
|
||||
)
|
||||
|
||||
assert service.calls[0]["domain"] == f"workspace:{workspace_id}"
|
||||
# folder_id is still passed in context so the plugin can use it for g2 hierarchy.
|
||||
assert service.calls[0]["domain"] == f"project:{folder_id}"
|
||||
# workspace_id is still passed in context so the plugin can use it for ABAC matchers.
|
||||
assert service.calls[0]["context"]["folder_id"] == folder_id
|
||||
assert service.calls[0]["context"]["workspace_id"] == workspace_id
|
||||
|
||||
@ -292,9 +298,14 @@ async def test_ensure_flow_permission_wildcard_domain_when_neither_set(monkeypat
|
||||
|
||||
|
||||
def test_resolve_flow_domain_precedence():
|
||||
"""Unit test the precedence rule directly (workspace > project > '*')."""
|
||||
"""Unit test the precedence rule directly (project > workspace > '*').
|
||||
|
||||
Project is more specific than workspace; passing it lets workspace grants
|
||||
flow down via Casbin g2 inheritance while keeping project-scoped grants
|
||||
visible to the enforcer.
|
||||
"""
|
||||
ws, folder = uuid4(), uuid4()
|
||||
assert authz_utils._resolve_flow_domain(workspace_id=ws, folder_id=folder) == f"workspace:{ws}"
|
||||
assert authz_utils._resolve_flow_domain(workspace_id=ws, folder_id=folder) == f"project:{folder}"
|
||||
assert authz_utils._resolve_flow_domain(workspace_id=ws, folder_id=None) == f"workspace:{ws}"
|
||||
assert authz_utils._resolve_flow_domain(workspace_id=None, folder_id=folder) == f"project:{folder}"
|
||||
assert authz_utils._resolve_flow_domain(workspace_id=None, folder_id=None) == "*"
|
||||
|
||||
Reference in New Issue
Block a user