|
| 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