Skip to content

Commit 5708ae1

Browse files
devGregAclaude
andcommitted
feat(authorization): backfill authorized_users from existing RBAC tables
Forward-only data migration that translates Product_Member, Product_Type_Member, Product_Group, Product_Type_Group, and Global_Role rows into Product.authorized_users / Product_Type.authorized_users membership and is_superuser / is_staff flag flips. Mapping (per the legacy authorization upgrade design): - Product_Member.user (any role) -> Product.authorized_users - Product_Type_Member.user (any role) -> Product_Type.authorized_users - Product_Group.group + Dojo_Group_Member -> Product.authorized_users (group membership flattened) - Product_Type_Group.group + Dojo_Group_Member -> Product_Type.authorized_users - Global_Role(Owner) -> User.is_superuser = True - Global_Role(Maintainer | API_Importer) -> User.is_staff = True - Global_Role(Writer | Reader) -> no global elevation (relies on per-product membership) Idempotency is guarded on the dojo_role table being present, so fresh installs are a no-op. The RBAC source tables are read-only here — they remain in the database verbatim so a later dojo-pro install can adopt them unchanged. Verified against bare_bones fixtures: 1 Product_Member + 1 Product_Group row each map into 2 Product.authorized_users entries; same for Product_Type. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 535b1bc commit 5708ae1

1 file changed

Lines changed: 132 additions & 0 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"""Backfill authorized_users from RBAC tables.
2+
3+
Forward-only data migration. Translates the dojo_product_member /
4+
dojo_product_type_member / dojo_product_group / dojo_product_type_group /
5+
dojo_global_role / dojo_dojo_group_member rows into Product.authorized_users
6+
and Product_Type.authorized_users membership, plus is_superuser / is_staff
7+
flag flips for users with elevated Global_Roles.
8+
9+
Idempotent: guarded on the presence of the dojo_role table so fresh OS
10+
installs (which never had RBAC) become a no-op. The RBAC tables themselves
11+
are NOT modified or dropped — they remain available verbatim so a later
12+
dojo-pro install can pick them up unchanged.
13+
14+
Mapping (per the legacy authorization design):
15+
16+
Product_Member.user (any role) -> Product.authorized_users
17+
Product_Type_Member.user (any role) -> Product_Type.authorized_users
18+
Product_Group.group + Dojo_Group_Member.user
19+
-> Product.authorized_users (flattened)
20+
Product_Type_Group.group + Dojo_Group_Member.user
21+
-> Product_Type.authorized_users (flattened)
22+
Global_Role(Owner) for user -> User.is_superuser = True
23+
Global_Role(Owner) via group -> all group members.is_superuser = True
24+
Global_Role(Maintainer|API_Importer) for user
25+
-> User.is_staff = True
26+
Global_Role(Maintainer|API_Importer) via group
27+
-> all group members.is_staff = True
28+
Global_Role(Writer|Reader) -> no global elevation
29+
(relies on per-product membership)
30+
31+
Things lost on this transition (acknowledged in the upgrade release notes):
32+
- Reader / Writer / Maintainer / Owner per-product role granularity
33+
- Group structure as a permission-bearing entity
34+
- The API_Importer global role specifically
35+
- Configuration permissions per add/edit/delete codename
36+
"""
37+
from django.db import migrations
38+
39+
40+
def backfill_authorized_users(apps, schema_editor):
41+
connection = schema_editor.connection
42+
if "dojo_role" not in connection.introspection.table_names():
43+
# Fresh install: no RBAC tables. Nothing to do.
44+
return
45+
46+
try:
47+
Product = apps.get_model("dojo", "Product")
48+
Product_Type = apps.get_model("dojo", "Product_Type")
49+
Dojo_User = apps.get_model("dojo", "Dojo_User")
50+
Product_Member = apps.get_model("dojo", "Product_Member")
51+
Product_Type_Member = apps.get_model("dojo", "Product_Type_Member")
52+
Product_Group = apps.get_model("dojo", "Product_Group")
53+
Product_Type_Group = apps.get_model("dojo", "Product_Type_Group")
54+
Dojo_Group_Member = apps.get_model("dojo", "Dojo_Group_Member")
55+
Global_Role = apps.get_model("dojo", "Global_Role")
56+
except LookupError:
57+
# Models already released from the dojo app state. Nothing to do.
58+
return
59+
60+
# 1. Direct per-product / per-product-type memberships.
61+
for product_id, user_id in Product_Member.objects.values_list("product_id", "user_id"):
62+
Product.authorized_users.through.objects.get_or_create(
63+
product_id=product_id, dojo_user_id=user_id,
64+
)
65+
for product_type_id, user_id in Product_Type_Member.objects.values_list("product_type_id", "user_id"):
66+
Product_Type.authorized_users.through.objects.get_or_create(
67+
product_type_id=product_type_id, dojo_user_id=user_id,
68+
)
69+
70+
# 2. Group memberships: flatten Dojo_Group_Member.user into authorized_users.
71+
for product_id, group_id in Product_Group.objects.values_list("product_id", "group_id"):
72+
member_user_ids = Dojo_Group_Member.objects.filter(group_id=group_id).values_list("user_id", flat=True)
73+
for user_id in member_user_ids:
74+
Product.authorized_users.through.objects.get_or_create(
75+
product_id=product_id, dojo_user_id=user_id,
76+
)
77+
for product_type_id, group_id in Product_Type_Group.objects.values_list("product_type_id", "group_id"):
78+
member_user_ids = Dojo_Group_Member.objects.filter(group_id=group_id).values_list("user_id", flat=True)
79+
for user_id in member_user_ids:
80+
Product_Type.authorized_users.through.objects.get_or_create(
81+
product_type_id=product_type_id, dojo_user_id=user_id,
82+
)
83+
84+
# 3. Global_Role -> is_superuser / is_staff flags.
85+
owner_user_ids = list(
86+
Global_Role.objects.filter(role__name="Owner", user__isnull=False).values_list("user_id", flat=True),
87+
)
88+
owner_group_ids = list(
89+
Global_Role.objects.filter(role__name="Owner", group__isnull=False).values_list("group_id", flat=True),
90+
)
91+
owner_user_ids.extend(
92+
Dojo_Group_Member.objects.filter(group_id__in=owner_group_ids).values_list("user_id", flat=True),
93+
)
94+
if owner_user_ids:
95+
Dojo_User.objects.filter(id__in=owner_user_ids).update(is_superuser=True)
96+
97+
elevated_user_ids = list(
98+
Global_Role.objects.filter(
99+
role__name__in=("Maintainer", "API_Importer"),
100+
user__isnull=False,
101+
).values_list("user_id", flat=True),
102+
)
103+
elevated_group_ids = list(
104+
Global_Role.objects.filter(
105+
role__name__in=("Maintainer", "API_Importer"),
106+
group__isnull=False,
107+
).values_list("group_id", flat=True),
108+
)
109+
elevated_user_ids.extend(
110+
Dojo_Group_Member.objects.filter(group_id__in=elevated_group_ids).values_list("user_id", flat=True),
111+
)
112+
if elevated_user_ids:
113+
Dojo_User.objects.filter(id__in=elevated_user_ids).update(is_staff=True)
114+
115+
116+
def reverse_noop(apps, schema_editor):
117+
# Reverse is a no-op. Backfilled authorized_users membership and is_superuser /
118+
# is_staff flags are preserved if this migration is rolled back; reverse cannot
119+
# reliably distinguish migrated entries from manually-added ones, and the source
120+
# RBAC tables are still intact for a forward re-run anyway.
121+
return
122+
123+
124+
class Migration(migrations.Migration):
125+
126+
dependencies = [
127+
("dojo", "0266_reintroduce_authorized_users"),
128+
]
129+
130+
operations = [
131+
migrations.RunPython(backfill_authorized_users, reverse_noop),
132+
]

0 commit comments

Comments
 (0)