diff --git a/changes/12883.fix.md b/changes/12883.fix.md new file mode 100644 index 00000000000..86f986b0f84 --- /dev/null +++ b/changes/12883.fix.md @@ -0,0 +1 @@ +Make RBAC backfill migrations skip already-present rows so re-running them inserts no duplicates diff --git a/src/ai/backend/manager/models/alembic/versions/013a6676866c_migrate_notification_data_to_rbac.py b/src/ai/backend/manager/models/alembic/versions/013a6676866c_migrate_notification_data_to_rbac.py index 3d935c4478f..a2c2eaceb7d 100644 --- a/src/ai/backend/manager/models/alembic/versions/013a6676866c_migrate_notification_data_to_rbac.py +++ b/src/ai/backend/manager/models/alembic/versions/013a6676866c_migrate_notification_data_to_rbac.py @@ -112,10 +112,20 @@ def _migrate_new_entity_type(db_conn: Connection) -> None: f"('{input.permission_group_id}', '{input.entity_type}', '{input.operation}')" for input in inputs ) + # `permissions` has no unique constraint on these columns at this + # revision, so ON CONFLICT has nothing to match on -- skip existing + # rows explicitly to keep re-runs idempotent. query = sa.text(f""" INSERT INTO permissions (permission_group_id, entity_type, operation) - VALUES {values} - ON CONFLICT DO NOTHING + SELECT DISTINCT v.permission_group_id::uuid, v.entity_type, v.operation + FROM (VALUES {values}) AS v(permission_group_id, entity_type, operation) + WHERE NOT EXISTS ( + SELECT 1 + FROM permissions p + WHERE p.permission_group_id = v.permission_group_id::uuid + AND p.entity_type = v.entity_type + AND p.operation = v.operation + ) """) db_conn.execute(query) diff --git a/src/ai/backend/manager/models/alembic/versions/2185ae0dd371_migrate_vfolder_data_to_rbac.py b/src/ai/backend/manager/models/alembic/versions/2185ae0dd371_migrate_vfolder_data_to_rbac.py index 73732bc313d..ab2a838c752 100644 --- a/src/ai/backend/manager/models/alembic/versions/2185ae0dd371_migrate_vfolder_data_to_rbac.py +++ b/src/ai/backend/manager/models/alembic/versions/2185ae0dd371_migrate_vfolder_data_to_rbac.py @@ -105,10 +105,20 @@ def _migrate_new_entity_type(db_conn: Connection) -> None: f"('{perm_id}', '{entity_type}', '{operation}')" for perm_id, entity_type, operation in inputs ) + # `permissions` has no unique constraint on these columns at this + # revision, so ON CONFLICT has nothing to match on -- skip existing + # rows explicitly to keep re-runs idempotent. query = sa.text(f""" INSERT INTO permissions (permission_group_id, entity_type, operation) - VALUES {values} - ON CONFLICT DO NOTHING + SELECT DISTINCT v.permission_group_id::uuid, v.entity_type, v.operation + FROM (VALUES {values}) AS v(permission_group_id, entity_type, operation) + WHERE NOT EXISTS ( + SELECT 1 + FROM permissions p + WHERE p.permission_group_id = v.permission_group_id::uuid + AND p.entity_type = v.entity_type + AND p.operation = v.operation + ) """) db_conn.execute(query) @@ -224,10 +234,20 @@ def _add_permission_groups_for_vfolder_invitations(db_conn: Connection) -> None: if values_list: values = ", ".join(values_list) + # `permission_groups` has no unique constraint on these columns at + # this revision, so ON CONFLICT has nothing to match on -- skip + # existing rows explicitly to keep re-runs idempotent. insert_query = sa.text(f""" INSERT INTO permission_groups (role_id, scope_type, scope_id) - VALUES {values} - ON CONFLICT DO NOTHING + SELECT DISTINCT v.role_id::uuid, v.scope_type, v.scope_id + FROM (VALUES {values}) AS v(role_id, scope_type, scope_id) + WHERE NOT EXISTS ( + SELECT 1 + FROM permission_groups pg + WHERE pg.role_id = v.role_id::uuid + AND pg.scope_type = v.scope_type + AND pg.scope_id = v.scope_id + ) """) db_conn.execute(insert_query) @@ -283,10 +303,21 @@ def _add_object_permissions_for_vfolder_invitations(db_conn: Connection) -> None if values_list: values = ", ".join(values_list) + # `object_permissions` has no unique constraint on these columns at + # this revision, so ON CONFLICT has nothing to match on -- skip + # existing rows explicitly to keep re-runs idempotent. insert_query = sa.text(f""" INSERT INTO object_permissions (role_id, entity_type, entity_id, operation) - VALUES {values} - ON CONFLICT DO NOTHING + SELECT DISTINCT v.role_id::uuid, v.entity_type, v.entity_id, v.operation + FROM (VALUES {values}) AS v(role_id, entity_type, entity_id, operation) + WHERE NOT EXISTS ( + SELECT 1 + FROM object_permissions o + WHERE o.role_id = v.role_id::uuid + AND o.entity_type = v.entity_type + AND o.entity_id = v.entity_id + AND o.operation = v.operation + ) """) db_conn.execute(insert_query) diff --git a/src/ai/backend/manager/models/alembic/versions/67f5338ff571_migrate_artifact_data_to_rbac.py b/src/ai/backend/manager/models/alembic/versions/67f5338ff571_migrate_artifact_data_to_rbac.py index 0cd275f50d4..1faf7731a84 100644 --- a/src/ai/backend/manager/models/alembic/versions/67f5338ff571_migrate_artifact_data_to_rbac.py +++ b/src/ai/backend/manager/models/alembic/versions/67f5338ff571_migrate_artifact_data_to_rbac.py @@ -94,10 +94,20 @@ def _migrate_new_entity_type(db_conn: Connection) -> None: f"('{perm_id}', '{entity_type}', '{operation}')" for perm_id, entity_type, operation in inputs ) + # `permissions` has no unique constraint on these columns at this + # revision, so ON CONFLICT has nothing to match on -- skip existing + # rows explicitly to keep re-runs idempotent. query = sa.text(f""" INSERT INTO permissions (permission_group_id, entity_type, operation) - VALUES {values} - ON CONFLICT DO NOTHING + SELECT DISTINCT v.permission_group_id::uuid, v.entity_type, v.operation + FROM (VALUES {values}) AS v(permission_group_id, entity_type, operation) + WHERE NOT EXISTS ( + SELECT 1 + FROM permissions p + WHERE p.permission_group_id = v.permission_group_id::uuid + AND p.entity_type = v.entity_type + AND p.operation = v.operation + ) """) db_conn.execute(query) diff --git a/src/ai/backend/manager/models/alembic/versions/6d850788c7c8_migrate_artifact_registry_data_to_rbac.py b/src/ai/backend/manager/models/alembic/versions/6d850788c7c8_migrate_artifact_registry_data_to_rbac.py index fcf31f2fa44..70fc8b3cd7b 100644 --- a/src/ai/backend/manager/models/alembic/versions/6d850788c7c8_migrate_artifact_registry_data_to_rbac.py +++ b/src/ai/backend/manager/models/alembic/versions/6d850788c7c8_migrate_artifact_registry_data_to_rbac.py @@ -94,10 +94,20 @@ def _migrate_new_entity_type(db_conn: Connection) -> None: f"('{perm_id}', '{entity_type}', '{operation}')" for perm_id, entity_type, operation in inputs ) + # `permissions` has no unique constraint on these columns at this + # revision, so ON CONFLICT has nothing to match on -- skip existing + # rows explicitly to keep re-runs idempotent. query = sa.text(f""" INSERT INTO permissions (permission_group_id, entity_type, operation) - VALUES {values} - ON CONFLICT DO NOTHING + SELECT DISTINCT v.permission_group_id::uuid, v.entity_type, v.operation + FROM (VALUES {values}) AS v(permission_group_id, entity_type, operation) + WHERE NOT EXISTS ( + SELECT 1 + FROM permissions p + WHERE p.permission_group_id = v.permission_group_id::uuid + AND p.entity_type = v.entity_type + AND p.operation = v.operation + ) """) db_conn.execute(query) diff --git a/src/ai/backend/manager/models/alembic/versions/a5e87ed3b6d4_migrate_app_config_data_to_rbac.py b/src/ai/backend/manager/models/alembic/versions/a5e87ed3b6d4_migrate_app_config_data_to_rbac.py index b95a627a0ca..00c7bd98234 100644 --- a/src/ai/backend/manager/models/alembic/versions/a5e87ed3b6d4_migrate_app_config_data_to_rbac.py +++ b/src/ai/backend/manager/models/alembic/versions/a5e87ed3b6d4_migrate_app_config_data_to_rbac.py @@ -103,10 +103,20 @@ def _migrate_new_entity_type(db_conn: Connection) -> None: f"('{input.permission_group_id}', '{input.entity_type}', '{input.operation}')" for input in inputs ) + # `permissions` has no unique constraint on these columns at this + # revision, so ON CONFLICT has nothing to match on -- skip existing + # rows explicitly to keep re-runs idempotent. query = sa.text(f""" INSERT INTO permissions (permission_group_id, entity_type, operation) - VALUES {values} - ON CONFLICT DO NOTHING + SELECT DISTINCT v.permission_group_id::uuid, v.entity_type, v.operation + FROM (VALUES {values}) AS v(permission_group_id, entity_type, operation) + WHERE NOT EXISTS ( + SELECT 1 + FROM permissions p + WHERE p.permission_group_id = v.permission_group_id::uuid + AND p.entity_type = v.entity_type + AND p.operation = v.operation + ) """) db_conn.execute(query) diff --git a/src/ai/backend/manager/models/alembic/versions/d0a3c0716970_migrate_model_deployment_data_to_rbac.py b/src/ai/backend/manager/models/alembic/versions/d0a3c0716970_migrate_model_deployment_data_to_rbac.py index 275cbb8cac4..a157735a32e 100644 --- a/src/ai/backend/manager/models/alembic/versions/d0a3c0716970_migrate_model_deployment_data_to_rbac.py +++ b/src/ai/backend/manager/models/alembic/versions/d0a3c0716970_migrate_model_deployment_data_to_rbac.py @@ -106,10 +106,20 @@ def _migrate_new_entity_type(db_conn: Connection) -> None: f"('{input.permission_group_id}', '{input.entity_type}', '{input.operation}')" for input in inputs ) + # `permissions` has no unique constraint on these columns at this + # revision, so ON CONFLICT has nothing to match on -- skip existing + # rows explicitly to keep re-runs idempotent. query = sa.text(f""" INSERT INTO permissions (permission_group_id, entity_type, operation) - VALUES {values} - ON CONFLICT DO NOTHING + SELECT DISTINCT v.permission_group_id::uuid, v.entity_type, v.operation + FROM (VALUES {values}) AS v(permission_group_id, entity_type, operation) + WHERE NOT EXISTS ( + SELECT 1 + FROM permissions p + WHERE p.permission_group_id = v.permission_group_id::uuid + AND p.entity_type = v.entity_type + AND p.operation = v.operation + ) """) db_conn.execute(query)