3232 optional_current_active_user ,
3333)
3434from tracecat .authz .enums import OrgRole , WorkspaceRole
35- from tracecat .authz .scopes import ORG_ROLE_SCOPES , PRESET_ROLE_SCOPES
3635from tracecat .authz .service import MembershipService , MembershipWithOrg
3736from tracecat .contexts import ctx_role
3837from tracecat .db .dependencies import AsyncDBSession
3938from tracecat .db .engine import get_async_session_context_manager
40- from tracecat .db .models import Organization , OrganizationMembership , User , Workspace
39+ from tracecat .db .models import (
40+ GroupMember ,
41+ GroupRoleAssignment ,
42+ Organization ,
43+ OrganizationMembership ,
44+ RoleScope ,
45+ Scope ,
46+ User ,
47+ UserRoleAssignment ,
48+ Workspace ,
49+ )
50+ from tracecat .db .models import (
51+ Role as RoleModel ,
52+ )
4153from tracecat .identifiers import InternalServiceID
4254from tracecat .logger import logger
4355from tracecat .organization .management import get_default_organization_id
@@ -85,42 +97,66 @@ async def _get_workspace_org_id(workspace_id: uuid.UUID) -> uuid.UUID | None:
8597ORG_OVERRIDE_COOKIE = "tracecat-org-id"
8698
8799
88- def compute_effective_scopes (role : Role ) -> frozenset [str ]:
89- """Compute the effective scopes for a role.
100+ async def compute_effective_scopes (
101+ session : AsyncDBSession , role : Role
102+ ) -> frozenset [str ]:
103+ """Compute the effective scopes for a role from the database.
104+
105+ Queries UserRoleAssignment and GroupRoleAssignment tables to resolve
106+ scopes through Role → RoleScope → Scope.
90107
91108 Scope computation follows this hierarchy:
92109 1. Platform superusers get "*" (all scopes)
93- 2. Org OWNER/ADMIN get their org-level scopes (includes full workspace access)
94- 3. Org MEMBER gets base org scopes + workspace membership scopes (if in workspace)
95- 4. Service roles inherit scopes based on the user they're acting on behalf of
96-
97- For workspace-scoped requests:
98- - Org OWNER/ADMIN: org-level scopes (they can access all workspaces)
99- - Workspace members: workspace role scopes from PRESET_ROLE_SCOPES
100-
101- Note: Group-based scopes will be added in PR 4 (RBAC Service & APIs).
110+ 2. Direct user role assignments (org-wide and workspace-specific)
111+ 3. Group role assignments (org-wide and workspace-specific)
102112 """
103113 if role .is_platform_superuser :
104114 return frozenset ({"*" })
105115
106- scope_set : set [str ] = set ()
107-
108- # Add org-level scopes based on org role
109- if role .org_role is not None :
110- scope_set |= ORG_ROLE_SCOPES .get (role .org_role , set ())
111-
112- # For workspace-scoped requests, add workspace role scopes
113- # (only if not an org admin/owner, who already have full access via org scopes)
114- if role .workspace_id and role .workspace_role :
115- # Org admins/owners already have workspace scopes via their org role
116- # Regular members need their workspace role scopes
117- if not role .is_org_admin :
118- scope_set |= PRESET_ROLE_SCOPES .get (role .workspace_role , set ())
116+ if role .user_id is None or role .organization_id is None :
117+ return frozenset ()
118+
119+ # Direct user role assignments → Role → RoleScope → Scope
120+ user_scopes = (
121+ select (Scope .name )
122+ .join (RoleScope , RoleScope .scope_id == Scope .id )
123+ .join (RoleModel , RoleModel .id == RoleScope .role_id )
124+ .join (UserRoleAssignment , UserRoleAssignment .role_id == RoleModel .id )
125+ .where (
126+ UserRoleAssignment .user_id == role .user_id ,
127+ UserRoleAssignment .organization_id == role .organization_id ,
128+ (
129+ UserRoleAssignment .workspace_id .is_ (None )
130+ | (UserRoleAssignment .workspace_id == role .workspace_id )
131+ )
132+ if role .workspace_id
133+ else UserRoleAssignment .workspace_id .is_ (None ),
134+ )
135+ )
119136
120- # Note: Group-based scopes (from group_assignment table) will be added in PR 4
121- # via RBACService.get_group_scopes()
137+ # Group role assignments → GroupMember → GroupRoleAssignment → Role → RoleScope → Scope
138+ group_scopes = (
139+ select (Scope .name )
140+ .join (RoleScope , RoleScope .scope_id == Scope .id )
141+ .join (RoleModel , RoleModel .id == RoleScope .role_id )
142+ .join (GroupRoleAssignment , GroupRoleAssignment .role_id == RoleModel .id )
143+ .join (GroupMember , GroupMember .group_id == GroupRoleAssignment .group_id )
144+ .where (
145+ GroupMember .user_id == role .user_id ,
146+ GroupRoleAssignment .organization_id == role .organization_id ,
147+ (
148+ GroupRoleAssignment .workspace_id .is_ (None )
149+ | (GroupRoleAssignment .workspace_id == role .workspace_id )
150+ )
151+ if role .workspace_id
152+ else GroupRoleAssignment .workspace_id .is_ (None ),
153+ )
154+ )
122155
123- return frozenset (scope_set )
156+ # Single atomic query: union both assignment paths
157+ combined = user_scopes .union (group_scopes )
158+ result = await session .execute (combined )
159+ return frozenset (result .scalars ().all ())
124160
125161
126162def get_role_from_user (
@@ -599,7 +635,8 @@ async def _authenticate_executor(
599635 return role
600636
601637
602- def _validate_role (
638+ async def _validate_role (
639+ session : AsyncSession ,
603640 role : Role ,
604641 * ,
605642 require_workspace : Literal ["yes" , "no" , "optional" ],
@@ -631,7 +668,7 @@ def _validate_role(
631668 )
632669
633670 # Compute effective scopes and create new role with scopes included
634- scopes = compute_effective_scopes (role )
671+ scopes = await compute_effective_scopes (session , role )
635672 logger .debug (
636673 "Computed effective scopes" ,
637674 scope_count = len (scopes ),
@@ -701,7 +738,8 @@ async def _role_dependency(
701738 detail = "Unauthorized" ,
702739 )
703740 # Validate structural requirements and set context
704- role = _validate_role (
741+ role = await _validate_role (
742+ session ,
705743 role ,
706744 require_workspace = require_workspace ,
707745 min_access_level = min_access_level ,
@@ -911,6 +949,7 @@ async def _require_superuser(
911949
912950async def _authenticated_user_only (
913951 user : Annotated [User , Depends (current_active_user )],
952+ session : AsyncDBSession ,
914953) -> Role :
915954 """Dependency for endpoints requiring only an authenticated user.
916955
@@ -931,7 +970,7 @@ async def _authenticated_user_only(
931970 is_platform_superuser = user .is_superuser ,
932971 # organization_id intentionally None - user may not belong to any org
933972 )
934- scopes = compute_effective_scopes (role )
973+ scopes = await compute_effective_scopes (session , role )
935974 role = role .model_copy (update = {"scopes" : scopes })
936975 ctx_role .set (role )
937976 return role
0 commit comments