Skip to content

Commit f4a11e9

Browse files
fregataaclaude
andauthored
feat(BA-2938): Migrate Session data to RBAC database (#9636)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent b01d8a9 commit f4a11e9

2 files changed

Lines changed: 237 additions & 0 deletions

File tree

changes/9636.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Migrate Session entities to RBAC database with entity-type permissions and AUTO scope associations
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
"""migrate_session_data_to_rbac
2+
3+
Revision ID: 30c8308738ee
4+
Revises: 5a4e677aea42
5+
Create Date: 2026-03-05 03:10:36.273207
6+
7+
"""
8+
9+
from uuid import UUID
10+
11+
import sqlalchemy as sa
12+
from alembic import op
13+
from sqlalchemy.engine import Connection
14+
15+
from ai.backend.manager.models.rbac_models.migration.enums import (
16+
EntityType,
17+
OperationType,
18+
)
19+
20+
# revision identifiers, used by Alembic.
21+
revision = "30c8308738ee"
22+
down_revision = "5a4e677aea42"
23+
branch_labels = None
24+
depends_on = None
25+
26+
# Constants
27+
BATCH_SIZE = 1000
28+
MEMBER_ROLE_SUFFIX = "member"
29+
SESSION_ENTITY_TYPE = EntityType.SESSION.value
30+
AUTO_RELATION_TYPE = "auto"
31+
32+
33+
def _add_entity_type_permissions(db_conn: Connection) -> None:
34+
"""Add SESSION entity-type permissions to all role+scope combinations.
35+
36+
Uses a single set-based INSERT ... SELECT to derive SESSION permissions
37+
for all role+scope combinations without application-side pagination.
38+
"""
39+
# Precompute operation lists
40+
member_ops = [op.value for op in OperationType.member_operations()]
41+
owner_ops = [op.value for op in OperationType.owner_operations()]
42+
43+
# Insert SESSION permissions in a single set-based query
44+
#
45+
# Rules:
46+
# - Skip roles where scope_type == 'domain' and role_name ends with 'member'
47+
# - For non-domain member roles, use member_ops (READ only)
48+
# - For all other roles (owner/admin), use owner_ops (all operations)
49+
insert_query = sa.text("""
50+
WITH role_scopes AS (
51+
SELECT DISTINCT
52+
p.role_id,
53+
r.name AS role_name,
54+
p.scope_type,
55+
p.scope_id
56+
FROM permissions p
57+
JOIN roles r ON p.role_id = r.id
58+
),
59+
role_operations AS (
60+
-- Member operations for non-domain member roles
61+
SELECT
62+
rs.role_id,
63+
rs.scope_type,
64+
rs.scope_id,
65+
unnest(CAST(:member_ops AS text[])) AS operation
66+
FROM role_scopes rs
67+
WHERE rs.scope_type != 'domain'
68+
AND rs.role_name LIKE :member_pattern
69+
70+
UNION ALL
71+
72+
-- Owner operations for non-member roles
73+
SELECT
74+
rs.role_id,
75+
rs.scope_type,
76+
rs.scope_id,
77+
unnest(CAST(:owner_ops AS text[])) AS operation
78+
FROM role_scopes rs
79+
WHERE NOT (rs.role_name LIKE :member_pattern)
80+
)
81+
INSERT INTO permissions (role_id, scope_type, scope_id, entity_type, operation)
82+
SELECT
83+
role_id,
84+
scope_type,
85+
scope_id,
86+
:entity_type AS entity_type,
87+
operation
88+
FROM role_operations
89+
ON CONFLICT (role_id, scope_type, scope_id, entity_type, operation) DO NOTHING
90+
""")
91+
92+
db_conn.execute(
93+
insert_query,
94+
{
95+
"member_ops": member_ops,
96+
"owner_ops": owner_ops,
97+
"member_pattern": f"%{MEMBER_ROLE_SUFFIX}",
98+
"entity_type": SESSION_ENTITY_TYPE,
99+
},
100+
)
101+
102+
103+
def _associate_sessions_to_scopes(db_conn: Connection) -> None:
104+
"""Associate all sessions to their owner scopes (USER and PROJECT).
105+
106+
Creates AUTO edges from:
107+
- User scope (user_uuid) → Session
108+
- Project scope (group_id) → Session
109+
110+
Uses keyset pagination for scalability.
111+
"""
112+
# Process User scope edges
113+
last_id = UUID("00000000-0000-0000-0000-000000000000")
114+
while True:
115+
query = sa.text("""
116+
SELECT id, user_uuid
117+
FROM sessions
118+
WHERE id > :last_id
119+
ORDER BY id
120+
LIMIT :limit
121+
""")
122+
rows = db_conn.execute(query, {"last_id": last_id, "limit": BATCH_SIZE}).all()
123+
if not rows:
124+
break
125+
126+
last_id = rows[-1].id
127+
128+
# Bulk insert using parameterized query
129+
values_list = [
130+
{
131+
"scope_type": "user",
132+
"scope_id": str(row.user_uuid),
133+
"entity_type": SESSION_ENTITY_TYPE,
134+
"entity_id": str(row.id),
135+
"relation_type": AUTO_RELATION_TYPE,
136+
}
137+
for row in rows
138+
]
139+
140+
if values_list:
141+
insert_query = sa.text("""
142+
INSERT INTO association_scopes_entities (scope_type, scope_id, entity_type, entity_id, relation_type)
143+
VALUES (:scope_type, :scope_id, :entity_type, :entity_id, :relation_type)
144+
ON CONFLICT (scope_type, scope_id, entity_id) DO NOTHING
145+
""")
146+
for values in values_list:
147+
db_conn.execute(insert_query, values)
148+
149+
# Process Project scope edges
150+
last_id = UUID("00000000-0000-0000-0000-000000000000")
151+
while True:
152+
query = sa.text("""
153+
SELECT id, group_id
154+
FROM sessions
155+
WHERE id > :last_id
156+
ORDER BY id
157+
LIMIT :limit
158+
""")
159+
rows = db_conn.execute(query, {"last_id": last_id, "limit": BATCH_SIZE}).all()
160+
if not rows:
161+
break
162+
163+
last_id = rows[-1].id
164+
165+
# Bulk insert using parameterized query
166+
values_list = [
167+
{
168+
"scope_type": "project",
169+
"scope_id": str(row.group_id),
170+
"entity_type": SESSION_ENTITY_TYPE,
171+
"entity_id": str(row.id),
172+
"relation_type": AUTO_RELATION_TYPE,
173+
}
174+
for row in rows
175+
]
176+
177+
if values_list:
178+
insert_query = sa.text("""
179+
INSERT INTO association_scopes_entities (scope_type, scope_id, entity_type, entity_id, relation_type)
180+
VALUES (:scope_type, :scope_id, :entity_type, :entity_id, :relation_type)
181+
ON CONFLICT (scope_type, scope_id, entity_id) DO NOTHING
182+
""")
183+
for values in values_list:
184+
db_conn.execute(insert_query, values)
185+
186+
187+
def _remove_session_permissions(db_conn: Connection) -> None:
188+
"""Remove all SESSION entity-type permissions."""
189+
while True:
190+
# Delete permissions in batches using a parameterized subquery
191+
delete_query = sa.text("""
192+
DELETE FROM permissions
193+
WHERE id IN (
194+
SELECT id FROM permissions
195+
WHERE entity_type = :entity_type
196+
LIMIT :limit
197+
)
198+
""")
199+
result = db_conn.execute(
200+
delete_query,
201+
{"entity_type": SESSION_ENTITY_TYPE, "limit": BATCH_SIZE},
202+
)
203+
if result.rowcount == 0:
204+
break
205+
206+
207+
def _remove_session_edges(db_conn: Connection) -> None:
208+
"""Remove all SESSION AUTO edges from association_scopes_entities."""
209+
while True:
210+
# Delete associations in batches using a parameterized subquery
211+
delete_query = sa.text("""
212+
DELETE FROM association_scopes_entities
213+
WHERE id IN (
214+
SELECT id FROM association_scopes_entities
215+
WHERE entity_type = :entity_type
216+
LIMIT :limit
217+
)
218+
""")
219+
result = db_conn.execute(
220+
delete_query,
221+
{"entity_type": SESSION_ENTITY_TYPE, "limit": BATCH_SIZE},
222+
)
223+
if result.rowcount == 0:
224+
break
225+
226+
227+
def upgrade() -> None:
228+
conn = op.get_bind()
229+
_add_entity_type_permissions(conn)
230+
_associate_sessions_to_scopes(conn)
231+
232+
233+
def downgrade() -> None:
234+
conn = op.get_bind()
235+
_remove_session_edges(conn)
236+
_remove_session_permissions(conn)

0 commit comments

Comments
 (0)