Skip to content

Commit 5dcdedf

Browse files
committed
refactor(rbac): unify scope presets
1 parent f022e5b commit 5dcdedf

File tree

3 files changed

+48
-87
lines changed

3 files changed

+48
-87
lines changed

tests/unit/test_rbac_scopes.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
ORG_ADMIN_SCOPES,
2121
ORG_MEMBER_SCOPES,
2222
ORG_OWNER_SCOPES,
23-
ORG_ROLE_SCOPES,
2423
PRESET_ROLE_SCOPES,
2524
VIEWER_SCOPES,
2625
)
@@ -200,10 +199,13 @@ def test_editor_includes_viewer(self):
200199
def test_admin_includes_editor(self):
201200
assert EDITOR_SCOPES.issubset(ADMIN_SCOPES)
202201

203-
def test_system_role_mapping(self):
202+
def test_preset_role_mapping(self):
204203
assert PRESET_ROLE_SCOPES["workspace-viewer"] == VIEWER_SCOPES
205204
assert PRESET_ROLE_SCOPES["workspace-editor"] == EDITOR_SCOPES
206205
assert PRESET_ROLE_SCOPES["workspace-admin"] == ADMIN_SCOPES
206+
assert PRESET_ROLE_SCOPES["organization-owner"] == ORG_OWNER_SCOPES
207+
assert PRESET_ROLE_SCOPES["organization-admin"] == ORG_ADMIN_SCOPES
208+
assert PRESET_ROLE_SCOPES["organization-member"] == ORG_MEMBER_SCOPES
207209

208210

209211
class TestOrgRoleScopes:
@@ -224,11 +226,6 @@ def test_admin_has_billing_read(self):
224226
def test_member_has_minimal_scopes(self):
225227
assert ORG_MEMBER_SCOPES == frozenset({"org:read", "org:member:read"})
226228

227-
def test_org_role_mapping(self):
228-
assert ORG_ROLE_SCOPES["organization-owner"] == ORG_OWNER_SCOPES
229-
assert ORG_ROLE_SCOPES["organization-admin"] == ORG_ADMIN_SCOPES
230-
assert ORG_ROLE_SCOPES["organization-member"] == ORG_MEMBER_SCOPES
231-
232229

233230
class TestRequireScopeDecorator:
234231
"""Tests for the @require_scope decorator."""

tracecat/authz/scopes.py

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
"""RBAC scope definitions for Tracecat.
22
3-
This module defines the default scope sets for:
4-
- System workspace roles (Viewer, Editor, Admin)
5-
- Organization roles (Owner, Admin, Member)
3+
This module defines the default scope sets for preset roles.
64
75
Scopes follow the OAuth 2.0 format: `{resource}:{action}`
86
@@ -17,7 +15,7 @@
1715
from __future__ import annotations
1816

1917
# =============================================================================
20-
# Viewer Role Scopes (read-only)
18+
# Workspace Role Scopes
2119
# =============================================================================
2220

2321
VIEWER_SCOPES: frozenset[str] = frozenset(
@@ -33,10 +31,6 @@
3331
}
3432
)
3533

36-
# =============================================================================
37-
# Editor Role Scopes (create/edit, no delete or admin)
38-
# =============================================================================
39-
4034
EDITOR_SCOPES: frozenset[str] = VIEWER_SCOPES | frozenset(
4135
{
4236
"workflow:create",
@@ -56,10 +50,6 @@
5650
}
5751
)
5852

59-
# =============================================================================
60-
# Admin Role Scopes (full workspace capabilities)
61-
# =============================================================================
62-
6353
ADMIN_SCOPES: frozenset[str] = EDITOR_SCOPES | frozenset(
6454
{
6555
"workflow:delete",
@@ -80,22 +70,12 @@
8070
}
8171
)
8272

83-
# =============================================================================
84-
# Preset Role -> Scope Set Mapping
85-
# =============================================================================
86-
87-
PRESET_ROLE_SCOPES: dict[str, frozenset[str]] = {
88-
"workspace-viewer": VIEWER_SCOPES,
89-
"workspace-editor": EDITOR_SCOPES,
90-
"workspace-admin": ADMIN_SCOPES,
91-
}
92-
9373
# =============================================================================
9474
# Organization Role Scopes
9575
# =============================================================================
9676

97-
# Note: Org OWNER/ADMIN roles grant implicit access to ALL workspaces in the org.
98-
# These scopes define WHAT they can do, not WHERE (tier determines container access).
77+
# Org OWNER/ADMIN roles grant implicit access to ALL workspaces in the org.
78+
# These scopes define WHAT they can do, not WHERE.
9979

10080
ORG_OWNER_SCOPES: frozenset[str] = frozenset(
10181
{
@@ -220,10 +200,15 @@
220200
)
221201

222202
# =============================================================================
223-
# Organization Role -> Scope Set Mapping
203+
# Preset Role -> Scope Set Mapping
224204
# =============================================================================
225205

226-
ORG_ROLE_SCOPES: dict[str, frozenset[str]] = {
206+
PRESET_ROLE_SCOPES: dict[str, frozenset[str]] = {
207+
# Workspace roles
208+
"workspace-viewer": VIEWER_SCOPES,
209+
"workspace-editor": EDITOR_SCOPES,
210+
"workspace-admin": ADMIN_SCOPES,
211+
# Organization roles
227212
"organization-owner": ORG_OWNER_SCOPES,
228213
"organization-admin": ORG_ADMIN_SCOPES,
229214
"organization-member": ORG_MEMBER_SCOPES,

tracecat/authz/seeding.py

Lines changed: 33 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,7 @@
1717
from sqlalchemy.dialects.postgresql import insert as pg_insert
1818

1919
from tracecat.authz.enums import ScopeSource
20-
from tracecat.authz.scopes import (
21-
ADMIN_SCOPES,
22-
EDITOR_SCOPES,
23-
ORG_ADMIN_SCOPES,
24-
ORG_MEMBER_SCOPES,
25-
ORG_OWNER_SCOPES,
26-
VIEWER_SCOPES,
27-
)
20+
from tracecat.authz.scopes import PRESET_ROLE_SCOPES
2821
from tracecat.db.models import Organization, Role, RoleScope, Scope
2922
from tracecat.logger import logger
3023

@@ -130,57 +123,43 @@
130123
# Preset Role Definitions
131124
# =============================================================================
132125

133-
# All preset role slugs
134-
PRESET_ROLE_SLUGS: frozenset[str] = frozenset(
135-
{"owner", "admin", "editor", "viewer", "member"}
136-
)
137-
138-
# Preset role definitions: (slug, name, description, scopes)
139-
#
140-
# Roles can be assigned at org level (workspace_id=NULL) or workspace level (workspace_id set).
141-
# The same role can be used at both levels; the assignment context determines what access applies.
142-
#
143-
# Role hierarchy:
144-
# - owner: Organization owner with full control (org-level only)
145-
# - admin: Full administrative access (can be org-level or workspace-level)
146-
# - editor: Create/edit access without admin capabilities (workspace-level)
147-
# - viewer: Read-only access (workspace-level)
148-
# - member: Basic org membership without workspace access (org-level only)
149-
#
150-
# Note: The "admin" role combines org admin and workspace admin scopes since
151-
# it may be assigned at either level.
152-
PRESET_ROLE_DEFINITIONS: list[tuple[str, str, str, frozenset[str]]] = [
153-
(
154-
"owner",
155-
"Owner",
156-
"Full organization control",
157-
ORG_OWNER_SCOPES,
158-
),
159-
(
160-
"admin",
161-
"Admin",
162-
"Full administrative access at organization or workspace level",
163-
ADMIN_SCOPES | ORG_ADMIN_SCOPES, # Combined for flexibility
126+
# Preset roles seeded per-organization.
127+
# Slugs match the keys in PRESET_ROLE_SCOPES from scopes.py.
128+
PRESET_ROLE_DEFINITIONS: dict[str, tuple[str, str, frozenset[str]]] = {
129+
# slug → (name, description, scopes)
130+
"workspace-viewer": (
131+
"Viewer",
132+
"Read-only access to workspace resources",
133+
PRESET_ROLE_SCOPES["workspace-viewer"],
164134
),
165-
(
166-
"editor",
135+
"workspace-editor": (
167136
"Editor",
168137
"Create and edit resources, no delete or admin access",
169-
EDITOR_SCOPES,
138+
PRESET_ROLE_SCOPES["workspace-editor"],
170139
),
171-
(
172-
"viewer",
173-
"Viewer",
174-
"Read-only access to workspace resources",
175-
VIEWER_SCOPES,
140+
"workspace-admin": (
141+
"Admin",
142+
"Full workspace capabilities",
143+
PRESET_ROLE_SCOPES["workspace-admin"],
176144
),
177-
(
178-
"member",
145+
"organization-owner": (
146+
"Owner",
147+
"Full organization control",
148+
PRESET_ROLE_SCOPES["organization-owner"],
149+
),
150+
"organization-admin": (
151+
"Admin",
152+
"Organization admin without delete or billing manage",
153+
PRESET_ROLE_SCOPES["organization-admin"],
154+
),
155+
"organization-member": (
179156
"Member",
180157
"Basic organization membership",
181-
ORG_MEMBER_SCOPES,
158+
PRESET_ROLE_SCOPES["organization-member"],
182159
),
183-
]
160+
}
161+
162+
PRESET_ROLE_SLUGS: frozenset[str] = frozenset(PRESET_ROLE_DEFINITIONS)
184163

185164

186165
# =============================================================================
@@ -376,7 +355,7 @@ async def seed_system_roles_for_org(
376355
# Prepare role values with pre-generated IDs
377356
role_values = []
378357
role_id_by_slug: dict[str, UUID] = {}
379-
for slug, name, description, _ in PRESET_ROLE_DEFINITIONS:
358+
for slug, (name, description, _) in PRESET_ROLE_DEFINITIONS.items():
380359
role_id = uuid4()
381360
role_id_by_slug[slug] = role_id
382361
role_values.append(
@@ -401,15 +380,15 @@ async def seed_system_roles_for_org(
401380
# Re-query to get actual role IDs (may differ if roles already existed)
402381
existing_roles_stmt = select(Role.id, Role.slug).where(
403382
Role.organization_id == organization_id,
404-
Role.slug.in_([slug for slug, _, _, _ in PRESET_ROLE_DEFINITIONS]),
383+
Role.slug.in_(PRESET_ROLE_DEFINITIONS),
405384
)
406385
existing_roles_result = await session.execute(existing_roles_stmt)
407386
actual_role_id_by_slug: dict[str | None, UUID] = {
408387
slug: role_id for role_id, slug in existing_roles_result.tuples().all()
409388
}
410389

411390
# Link scopes to roles
412-
for slug, _, _, scope_names in PRESET_ROLE_DEFINITIONS:
391+
for slug, (_, _, scope_names) in PRESET_ROLE_DEFINITIONS.items():
413392
role_id = actual_role_id_by_slug.get(slug)
414393
if role_id is None:
415394
logger.warning(

0 commit comments

Comments
 (0)