Skip to content

Commit 8b38c70

Browse files
committed
fix(backup): Handle UserRole name collisions
Issue: getsentry/team-ospo#184
1 parent a95255e commit 8b38c70

File tree

2 files changed

+51
-3
lines changed

2 files changed

+51
-3
lines changed

src/sentry/models/userrole.py

+27-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1-
from typing import FrozenSet
1+
from typing import FrozenSet, Optional
22

33
from django.conf import settings
44
from django.db import models
55

6-
from sentry.backup.scopes import RelocationScope
6+
from sentry.backup.dependencies import PrimaryKeyMap
7+
from sentry.backup.helpers import ImportFlags
8+
from sentry.backup.scopes import ImportScope, RelocationScope
79
from sentry.db.models import ArrayField, DefaultFieldsModel, control_silo_only_model, sane_repr
810
from sentry.db.models.fields.foreignkey import FlexibleForeignKey
11+
from sentry.db.models.utils import unique_db_instance
912
from sentry.signals import post_upgrade
1013
from sentry.silo import SiloMode
1114

15+
MAX_USER_ROLE_NAME_LENGTH = 32
16+
1217

1318
@control_silo_only_model
1419
class UserRole(DefaultFieldsModel):
@@ -18,7 +23,7 @@ class UserRole(DefaultFieldsModel):
1823

1924
__relocation_scope__ = RelocationScope.Config
2025

21-
name = models.CharField(max_length=32, unique=True)
26+
name = models.CharField(max_length=MAX_USER_ROLE_NAME_LENGTH, unique=True)
2227
permissions = ArrayField()
2328
users = models.ManyToManyField("sentry.User", through="sentry.UserRoleUser")
2429

@@ -39,6 +44,25 @@ def permissions_for_user(cls, user_id: int) -> FrozenSet[str]:
3944
for i in sl
4045
)
4146

47+
def normalize_before_relocation_import(
48+
self, pk_map: PrimaryKeyMap, scope: ImportScope, flags: ImportFlags
49+
) -> Optional[int]:
50+
51+
old_pk = super().normalize_before_relocation_import(pk_map, scope, flags)
52+
if old_pk is None:
53+
return None
54+
55+
# Circumvent unique name collisions.
56+
if self.objects.filter(name__exact=self.name):
57+
unique_db_instance(
58+
self,
59+
self.name,
60+
max_length=MAX_USER_ROLE_NAME_LENGTH,
61+
field_name="name",
62+
)
63+
64+
return old_pk
65+
4266

4367
@control_silo_only_model
4468
class UserRoleUser(DefaultFieldsModel):

tests/sentry/backup/test_imports.py

+24
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,30 @@ def test_colliding_query_subscription(self):
741741
== 1
742742
)
743743

744+
def test_colliding_user_role(self):
745+
self.create_exhaustive_user("owner", is_admin=True)
746+
747+
# Take note of the `UserRole` - this is the one we'll be importing.
748+
colliding = UserRole.objects.all().first()
749+
750+
with tempfile.TemporaryDirectory() as tmp_dir:
751+
tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
752+
753+
# After exporting and clearing the database, insert a copy of the same `UserRole` as the
754+
# one found in the import.
755+
colliding.save()
756+
757+
assert UserRole.objects.count() == 1
758+
assert UserRole.objects.filter(name="test-admin-role").count() == 1
759+
760+
with open(tmp_path) as tmp_file:
761+
import_in_global_scope(tmp_file, printer=NOOP_PRINTER)
762+
763+
assert UserRole.objects.count() == 2
764+
assert UserRole.objects.filter(name__contains="test-admin-role").count() == 2
765+
assert UserRole.objects.filter(name__exact="test-admin-role").count() == 1
766+
assert UserRole.objects.filter(name__contains="test-admin-role-").count() == 1
767+
744768
def test_colliding_user_with_merging_enabled_in_user_scope(self):
745769
self.create_exhaustive_user(username="owner", email="[email protected]")
746770

0 commit comments

Comments
 (0)