test_imports.py 108 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383
  1. from __future__ import annotations
  2. import io
  3. import os
  4. import tarfile
  5. import tempfile
  6. from datetime import date, datetime, timedelta
  7. from pathlib import Path
  8. from unittest.mock import patch
  9. import pytest
  10. import urllib3.exceptions
  11. from cryptography.fernet import Fernet
  12. from cryptography.hazmat.backends import default_backend
  13. from cryptography.hazmat.primitives import hashes, serialization
  14. from cryptography.hazmat.primitives.asymmetric import padding
  15. from django.db import connections, router
  16. from django.db.models import Model
  17. from django.utils import timezone
  18. from sentry.backup.crypto import LocalFileDecryptor
  19. from sentry.backup.dependencies import NormalizedModelName, dependencies, get_model, get_model_name
  20. from sentry.backup.helpers import ImportFlags
  21. from sentry.backup.imports import (
  22. ImportingError,
  23. import_in_config_scope,
  24. import_in_global_scope,
  25. import_in_organization_scope,
  26. import_in_user_scope,
  27. )
  28. from sentry.backup.scopes import ExportScope, ImportScope, RelocationScope
  29. from sentry.incidents.models import AlertRule, AlertRuleThresholdType
  30. from sentry.models.actor import ACTOR_TYPES, Actor
  31. from sentry.models.apitoken import DEFAULT_EXPIRATION, ApiToken, generate_token
  32. from sentry.models.authenticator import Authenticator
  33. from sentry.models.email import Email
  34. from sentry.models.importchunk import (
  35. ControlImportChunk,
  36. ControlImportChunkReplica,
  37. RegionImportChunk,
  38. )
  39. from sentry.models.lostpasswordhash import LostPasswordHash
  40. from sentry.models.options.option import ControlOption, Option
  41. from sentry.models.options.project_option import ProjectOption
  42. from sentry.models.organization import Organization
  43. from sentry.models.organizationmapping import OrganizationMapping
  44. from sentry.models.organizationmember import OrganizationMember
  45. from sentry.models.organizationmembermapping import OrganizationMemberMapping
  46. from sentry.models.organizationslugreservation import (
  47. OrganizationSlugReservation,
  48. OrganizationSlugReservationType,
  49. )
  50. from sentry.models.orgauthtoken import OrgAuthToken
  51. from sentry.models.project import Project
  52. from sentry.models.projectkey import ProjectKey
  53. from sentry.models.relay import Relay, RelayUsage
  54. from sentry.models.savedsearch import SavedSearch, Visibility
  55. from sentry.models.team import Team
  56. from sentry.models.user import User
  57. from sentry.models.useremail import UserEmail
  58. from sentry.models.userip import UserIP
  59. from sentry.models.userpermission import UserPermission
  60. from sentry.models.userrole import UserRole, UserRoleUser
  61. from sentry.monitors.models import Monitor
  62. from sentry.receivers import create_default_projects
  63. from sentry.services.hybrid_cloud.import_export.model import RpcImportErrorKind
  64. from sentry.silo.base import SiloMode
  65. from sentry.snuba.dataset import Dataset
  66. from sentry.snuba.models import QuerySubscription, SnubaQuery, SnubaQueryEventType
  67. from sentry.snuba.subscriptions import create_snuba_query
  68. from sentry.testutils.factories import get_fixture_path
  69. from sentry.testutils.helpers import override_options
  70. from sentry.testutils.helpers.backups import (
  71. NOOP_PRINTER,
  72. BackupTestCase,
  73. clear_database,
  74. export_to_file,
  75. generate_rsa_key_pair,
  76. is_control_model,
  77. )
  78. from sentry.testutils.hybrid_cloud import use_split_dbs
  79. from sentry.testutils.silo import assume_test_silo_mode, region_silo_test
  80. from sentry.utils import json
  81. from tests.sentry.backup import (
  82. expect_models,
  83. get_matching_exportable_models,
  84. verify_models_in_output,
  85. )
  86. class ImportTestCase(BackupTestCase):
  87. def export_to_tmp_file_and_clear_database(self, tmp_dir) -> Path:
  88. tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
  89. export_to_file(tmp_path, ExportScope.Global)
  90. clear_database()
  91. return tmp_path
  92. @region_silo_test
  93. class SanitizationTests(ImportTestCase):
  94. """
  95. Ensure that potentially damaging data is properly scrubbed at import time.
  96. """
  97. def test_users_sanitized_in_user_scope(self):
  98. with tempfile.TemporaryDirectory() as tmp_dir:
  99. tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
  100. self.generate_tmp_users_json_file(tmp_path)
  101. with open(tmp_path, "rb") as tmp_file:
  102. import_in_user_scope(tmp_file, printer=NOOP_PRINTER)
  103. with assume_test_silo_mode(SiloMode.CONTROL):
  104. assert User.objects.count() == 4
  105. assert (
  106. User.objects.filter(is_managed=False, is_staff=False, is_superuser=False).count()
  107. == 4
  108. )
  109. # Every user except `max_user` shares an email.
  110. assert Email.objects.count() == 2
  111. # All `UserEmail`s must have their verification status reset in this scope.
  112. assert UserEmail.objects.count() == 4
  113. assert UserEmail.objects.filter(is_verified=True).count() == 0
  114. assert (
  115. UserEmail.objects.filter(date_hash_added__lt=datetime(2023, 7, 1, 0, 0)).count()
  116. == 0
  117. )
  118. assert (
  119. UserEmail.objects.filter(validation_hash="mCnWesSVvYQcq7qXQ36AZHwosAd6cghE").count()
  120. == 0
  121. )
  122. assert User.objects.filter(is_unclaimed=True).count() == 4
  123. assert LostPasswordHash.objects.count() == 4
  124. assert User.objects.filter(is_managed=True).count() == 0
  125. assert User.objects.filter(is_staff=True).count() == 0
  126. assert User.objects.filter(is_superuser=True).count() == 0
  127. assert Authenticator.objects.count() == 0
  128. assert UserPermission.objects.count() == 0
  129. assert UserRole.objects.count() == 0
  130. assert UserRoleUser.objects.count() == 0
  131. def test_users_sanitized_in_organization_scope(self):
  132. with tempfile.TemporaryDirectory() as tmp_dir:
  133. tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
  134. self.generate_tmp_users_json_file(tmp_path)
  135. with open(tmp_path, "rb") as tmp_file:
  136. import_in_organization_scope(tmp_file, printer=NOOP_PRINTER)
  137. with assume_test_silo_mode(SiloMode.CONTROL):
  138. assert User.objects.count() == 4
  139. assert (
  140. User.objects.filter(is_managed=False, is_staff=False, is_superuser=False).count()
  141. == 4
  142. )
  143. # Every user except `max_user` shares an email.
  144. assert Email.objects.count() == 2
  145. # All `UserEmail`s must have their verification status reset in this scope.
  146. assert UserEmail.objects.count() == 4
  147. assert UserEmail.objects.filter(is_verified=True).count() == 0
  148. assert (
  149. UserEmail.objects.filter(date_hash_added__lt=datetime(2023, 7, 1, 0, 0)).count()
  150. == 0
  151. )
  152. assert (
  153. UserEmail.objects.filter(validation_hash="mCnWesSVvYQcq7qXQ36AZHwosAd6cghE").count()
  154. == 0
  155. )
  156. assert User.objects.filter(is_unclaimed=True).count() == 4
  157. assert LostPasswordHash.objects.count() == 4
  158. assert User.objects.filter(is_managed=True).count() == 0
  159. assert User.objects.filter(is_staff=True).count() == 0
  160. assert User.objects.filter(is_superuser=True).count() == 0
  161. assert Authenticator.objects.count() == 0
  162. assert UserPermission.objects.count() == 0
  163. assert UserRole.objects.count() == 0
  164. assert UserRoleUser.objects.count() == 0
  165. def test_users_unsanitized_in_config_scope(self):
  166. with tempfile.TemporaryDirectory() as tmp_dir:
  167. tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
  168. self.generate_tmp_users_json_file(tmp_path)
  169. with open(tmp_path, "rb") as tmp_file:
  170. import_in_config_scope(tmp_file, printer=NOOP_PRINTER)
  171. with assume_test_silo_mode(SiloMode.CONTROL):
  172. assert User.objects.count() == 4
  173. assert User.objects.filter(is_unclaimed=True).count() == 4
  174. assert LostPasswordHash.objects.count() == 4
  175. assert User.objects.filter(is_managed=True).count() == 1
  176. assert User.objects.filter(is_staff=True).count() == 2
  177. assert User.objects.filter(is_superuser=True).count() == 2
  178. assert (
  179. User.objects.filter(is_managed=False, is_staff=False, is_superuser=False).count()
  180. == 2
  181. )
  182. assert UserEmail.objects.count() == 4
  183. # Unlike the "global" scope, we do not keep authentication information for the "config"
  184. # scope.
  185. assert Authenticator.objects.count() == 0
  186. # Every user except `max_user` shares an email.
  187. assert Email.objects.count() == 2
  188. # All `UserEmail`s must have their verification status reset in this scope.
  189. assert UserEmail.objects.count() == 4
  190. assert UserEmail.objects.filter(is_verified=True).count() == 0
  191. assert (
  192. UserEmail.objects.filter(date_hash_added__lt=datetime(2023, 7, 1, 0, 0)).count()
  193. == 0
  194. )
  195. assert (
  196. UserEmail.objects.filter(validation_hash="mCnWesSVvYQcq7qXQ36AZHwosAd6cghE").count()
  197. == 0
  198. )
  199. # 1 from `max_user`, 1 from `permission_user`.
  200. assert UserPermission.objects.count() == 2
  201. # 1 from `max_user`.
  202. assert UserRole.objects.count() == 1
  203. assert UserRoleUser.objects.count() == 2
  204. def test_users_unsanitized_in_global_scope(self):
  205. with tempfile.TemporaryDirectory() as tmp_dir:
  206. tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
  207. self.generate_tmp_users_json_file(tmp_path)
  208. with open(tmp_path, "rb") as tmp_file:
  209. import_in_global_scope(tmp_file, printer=NOOP_PRINTER)
  210. with assume_test_silo_mode(SiloMode.CONTROL):
  211. assert User.objects.count() == 4
  212. # We don't mark `Global`ly imported `User`s unclaimed.
  213. assert User.objects.filter(is_unclaimed=True).count() == 0
  214. assert LostPasswordHash.objects.count() == 0
  215. assert User.objects.filter(is_managed=True).count() == 1
  216. assert User.objects.filter(is_staff=True).count() == 2
  217. assert User.objects.filter(is_superuser=True).count() == 2
  218. assert (
  219. User.objects.filter(is_managed=False, is_staff=False, is_superuser=False).count()
  220. == 2
  221. )
  222. assert UserEmail.objects.count() == 4
  223. # Unlike the "config" scope, we keep authentication information for the "global" scope.
  224. assert Authenticator.objects.count() == 4
  225. # Every user except `max_user` shares an email.
  226. assert Email.objects.count() == 2
  227. # All `UserEmail`s must have their imported verification status reset in this scope.
  228. assert UserEmail.objects.count() == 4
  229. assert UserEmail.objects.filter(is_verified=True).count() == 4
  230. assert (
  231. UserEmail.objects.filter(date_hash_added__lt=datetime(2023, 7, 1, 0, 0)).count()
  232. == 4
  233. )
  234. assert (
  235. UserEmail.objects.filter(validation_hash="mCnWesSVvYQcq7qXQ36AZHwosAd6cghE").count()
  236. == 4
  237. )
  238. # 1 from `max_user`, 1 from `permission_user`.
  239. assert UserPermission.objects.count() == 2
  240. # 1 from `max_user`.
  241. assert UserRole.objects.count() == 1
  242. assert UserRoleUser.objects.count() == 2
  243. def test_generate_suffix_for_already_taken_organization(self):
  244. owner = self.create_user(email="testing@example.com")
  245. self.create_organization(name="some-org", owner=owner)
  246. with tempfile.TemporaryDirectory() as tmp_dir:
  247. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  248. # Note that we have created an organization with the same name as one we are about to
  249. # import.
  250. existing_org = self.create_organization(owner=self.user, name="some-org")
  251. with open(tmp_path, "rb") as tmp_file:
  252. import_in_organization_scope(tmp_file, printer=NOOP_PRINTER)
  253. assert Organization.objects.count() == 2
  254. assert Organization.objects.filter(slug__icontains="some-org").count() == 2
  255. assert Organization.objects.filter(slug__iexact="some-org").count() == 1
  256. imported_organization = Organization.objects.get(slug__icontains="some-org-")
  257. assert imported_organization.id != existing_org.id
  258. org_chunk = RegionImportChunk.objects.get(
  259. model="sentry.organization", min_ordinal=1, max_ordinal=1
  260. )
  261. assert len(org_chunk.inserted_map) == 1
  262. assert len(org_chunk.inserted_identifiers) == 1
  263. for slug in org_chunk.inserted_identifiers.values():
  264. assert slug.startswith("some-org-")
  265. with assume_test_silo_mode(SiloMode.CONTROL):
  266. assert (
  267. OrganizationSlugReservation.objects.filter(
  268. slug__icontains="some-org",
  269. reservation_type=OrganizationSlugReservationType.PRIMARY,
  270. ).count()
  271. == 2
  272. )
  273. assert OrganizationSlugReservation.objects.filter(slug__iexact="some-org").count() == 1
  274. # Assert that the slug update RPC has completed and generated a valid matching primary
  275. # slug reservation.
  276. slug_reservation = OrganizationSlugReservation.objects.filter(
  277. slug__icontains="some-org-",
  278. reservation_type=OrganizationSlugReservationType.PRIMARY,
  279. ).get()
  280. assert OrganizationMapping.objects.count() == 2
  281. assert OrganizationMapping.objects.filter(slug__icontains="some-org").count() == 2
  282. assert OrganizationMapping.objects.filter(slug__iexact="some-org").count() == 1
  283. org_mapping = OrganizationMapping.objects.get(slug__icontains="some-org-")
  284. assert org_mapping.slug == slug_reservation.slug == imported_organization.slug
  285. assert (
  286. org_mapping.organization_id
  287. == slug_reservation.organization_id
  288. == imported_organization.id
  289. )
  290. def test_generate_suffix_for_already_taken_organization_with_control_option(self):
  291. with override_options({"hybrid_cloud.control-organization-provisioning": True}):
  292. self.test_generate_suffix_for_already_taken_organization()
  293. def test_generate_suffix_for_already_taken_username(self):
  294. with tempfile.TemporaryDirectory() as tmp_dir:
  295. self.create_user("min_user")
  296. tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
  297. with open(tmp_path, "w+") as tmp_file:
  298. models = self.json_of_exhaustive_user_with_minimum_privileges()
  299. json.dump(
  300. self.sort_in_memory_json(models),
  301. tmp_file,
  302. )
  303. # Import twice, to check that new suffixes are assigned both times.
  304. with open(tmp_path, "rb") as tmp_file:
  305. import_in_user_scope(tmp_file, printer=NOOP_PRINTER)
  306. with open(tmp_path, "rb") as tmp_file:
  307. import_in_user_scope(tmp_file, printer=NOOP_PRINTER)
  308. with assume_test_silo_mode(SiloMode.CONTROL):
  309. assert User.objects.count() == 3
  310. assert (
  311. User.objects.filter(username__icontains="min_user")
  312. .values("username")
  313. .distinct()
  314. .count()
  315. == 3
  316. )
  317. assert User.objects.filter(username__iexact="min_user").count() == 1
  318. assert User.objects.filter(username__icontains="min_user-").count() == 2
  319. def test_bad_invalid_user(self):
  320. with tempfile.TemporaryDirectory() as tmp_dir:
  321. tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
  322. with open(tmp_path, "w+") as tmp_file:
  323. models = self.json_of_exhaustive_user_with_minimum_privileges()
  324. # Modify all username to be longer than 128 characters.
  325. for model in models:
  326. if model["model"] == "sentry.user":
  327. model["fields"]["username"] = "x" * 129
  328. json.dump(models, tmp_file)
  329. with open(tmp_path, "rb") as tmp_file:
  330. with pytest.raises(ImportingError) as err:
  331. import_in_user_scope(tmp_file, printer=NOOP_PRINTER)
  332. assert err.value.context.get_kind() == RpcImportErrorKind.ValidationError
  333. assert err.value.context.on.model == "sentry.user"
  334. @patch("sentry.models.userip.geo_by_addr")
  335. def test_good_regional_user_ip_in_user_scope(self, mock_geo_by_addr):
  336. mock_geo_by_addr.return_value = {
  337. "country_code": "US",
  338. "region": "CA",
  339. "subdivision": "San Francisco",
  340. }
  341. with tempfile.TemporaryDirectory() as tmp_dir:
  342. tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
  343. with open(tmp_path, "w+") as tmp_file:
  344. models = self.json_of_exhaustive_user_with_minimum_privileges()
  345. # Modify the UserIP to be in California, USA.
  346. for model in models:
  347. if model["model"] == "sentry.userip":
  348. model["fields"]["ip_address"] = "8.8.8.8"
  349. json.dump(models, tmp_file)
  350. with open(tmp_path, "rb") as tmp_file:
  351. import_in_user_scope(tmp_file, printer=NOOP_PRINTER)
  352. with assume_test_silo_mode(SiloMode.CONTROL):
  353. assert UserIP.objects.count() == 1
  354. assert UserIP.objects.filter(ip_address="8.8.8.8").exists()
  355. assert UserIP.objects.filter(country_code="US").exists()
  356. assert UserIP.objects.filter(region_code="CA").exists()
  357. # Unlike global scope, this time must be reset.
  358. assert UserIP.objects.filter(last_seen__gt=datetime(2023, 7, 1, 0, 0)).exists()
  359. assert UserIP.objects.filter(first_seen__gt=datetime(2023, 7, 1, 0, 0)).exists()
  360. @patch("sentry.models.userip.geo_by_addr")
  361. def test_good_regional_user_ip_in_global_scope(self, mock_geo_by_addr):
  362. mock_geo_by_addr.return_value = {
  363. "country_code": "US",
  364. "region": "CA",
  365. "subdivision": "San Francisco",
  366. }
  367. with tempfile.TemporaryDirectory() as tmp_dir:
  368. tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
  369. with open(tmp_path, "w+") as tmp_file:
  370. models = self.json_of_exhaustive_user_with_minimum_privileges()
  371. # Modify the UserIP to be in California, USA.
  372. for model in models:
  373. if model["model"] == "sentry.userip":
  374. model["fields"]["ip_address"] = "8.8.8.8"
  375. json.dump(models, tmp_file)
  376. with open(tmp_path, "rb") as tmp_file:
  377. import_in_global_scope(tmp_file, printer=NOOP_PRINTER)
  378. with assume_test_silo_mode(SiloMode.CONTROL):
  379. assert UserIP.objects.count() == 1
  380. assert UserIP.objects.filter(ip_address="8.8.8.8").exists()
  381. assert UserIP.objects.filter(country_code="US").exists()
  382. assert UserIP.objects.filter(region_code="CA").exists()
  383. # Unlike org/user scope, this must NOT be reset.
  384. assert not UserIP.objects.filter(last_seen__gt=datetime(2023, 7, 1, 0, 0)).exists()
  385. assert not UserIP.objects.filter(first_seen__gt=datetime(2023, 7, 1, 0, 0)).exists()
  386. # Regression test for getsentry/self-hosted#2468.
  387. @patch("sentry.models.userip.geo_by_addr")
  388. def test_good_multiple_user_ips_per_user_in_global_scope(self, mock_geo_by_addr):
  389. mock_geo_by_addr.return_value = {
  390. "country_code": "US",
  391. "region": "CA",
  392. "subdivision": "San Francisco",
  393. }
  394. with tempfile.TemporaryDirectory() as tmp_dir:
  395. tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
  396. with open(tmp_path, "w+") as tmp_file:
  397. models = self.json_of_exhaustive_user_with_minimum_privileges()
  398. # Modify the UserIP to be in California, USA.
  399. for model in models:
  400. if model["model"] == "sentry.userip":
  401. model["fields"]["ip_address"] = "8.8.8.8"
  402. # Add a two copies of the same IP - so the user now has 2 `UserIP` models for the IP
  403. # `8.8.8.9`, 1 for `8.8.8.8`, and 1 for `8.8.8.7`. After import, we would expect to
  404. # only see one model for each IP.
  405. models.append(
  406. {
  407. "model": "sentry.userip",
  408. "pk": 3,
  409. "fields": {
  410. "user": 2,
  411. "ip_address": "8.8.8.9",
  412. "country_code": "US",
  413. "region_code": "CA",
  414. "first_seen": "2013-04-05T03:29:45.000Z",
  415. "last_seen": "2013-04-05T03:29:45.000Z",
  416. },
  417. }
  418. )
  419. models.append(
  420. {
  421. "model": "sentry.userip",
  422. "pk": 4,
  423. "fields": {
  424. "user": 2,
  425. "ip_address": "8.8.8.9",
  426. "country_code": "CA", # Incorrect value - importing should fix this.
  427. "region_code": "BC", # Incorrect value - importing should fix this.
  428. "first_seen": "2014-04-05T03:29:45.000Z",
  429. "last_seen": "2014-04-05T03:29:45.000Z",
  430. },
  431. }
  432. )
  433. models.append(
  434. {
  435. "model": "sentry.userip",
  436. "pk": 4,
  437. "fields": {
  438. "user": 2,
  439. "ip_address": "8.8.8.7",
  440. "country_code": None, # Unknown value - importing should fix this.
  441. "region_code": None, # Unknown value - importing should fix this.
  442. "first_seen": "2014-04-05T03:29:45.000Z",
  443. "last_seen": "2014-04-05T03:29:45.000Z",
  444. },
  445. }
  446. )
  447. json.dump(self.sort_in_memory_json(models), tmp_file)
  448. with open(tmp_path, "rb") as tmp_file:
  449. import_in_global_scope(tmp_file, printer=NOOP_PRINTER)
  450. with assume_test_silo_mode(SiloMode.CONTROL):
  451. assert UserIP.objects.count() == 3
  452. assert UserIP.objects.filter(ip_address="8.8.8.9").count() == 1
  453. assert UserIP.objects.filter(ip_address="8.8.8.8").count() == 1
  454. assert UserIP.objects.filter(ip_address="8.8.8.7").count() == 1
  455. assert UserIP.objects.filter(country_code="US").count() == 3
  456. assert UserIP.objects.filter(region_code="CA").count() == 3
  457. def test_bad_invalid_user_ip(self):
  458. with tempfile.TemporaryDirectory() as tmp_dir:
  459. tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
  460. with open(tmp_path, "w+") as tmp_file:
  461. models = self.json_of_exhaustive_user_with_minimum_privileges()
  462. # Modify the IP address to be in invalid.
  463. for m in models:
  464. if m["model"] == "sentry.userip":
  465. m["fields"]["ip_address"] = "0.1.2.3.4.5.6.7.8.9.abc.def"
  466. json.dump(list(models), tmp_file)
  467. with open(tmp_path, "rb") as tmp_file:
  468. with pytest.raises(ImportingError) as err:
  469. import_in_user_scope(tmp_file, printer=NOOP_PRINTER)
  470. assert err.value.context.get_kind() == RpcImportErrorKind.ValidationError
  471. assert err.value.context.on.model == "sentry.userip"
  472. # Regression test for getsentry/self-hosted#2571.
  473. def test_good_multiple_useremails_per_user_in_user_scope(self):
  474. with tempfile.TemporaryDirectory() as tmp_dir:
  475. tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
  476. with open(tmp_path, "w+") as tmp_file:
  477. models = self.json_of_exhaustive_user_with_minimum_privileges()
  478. # Add two copies (1 verified, 1 not) of the same `UserEmail` - so the user now has 3
  479. # `UserEmail` models, the latter of which have no corresponding `Email` entry.
  480. models.append(
  481. {
  482. "model": "sentry.useremail",
  483. "pk": 100,
  484. "fields": {
  485. "user": 2,
  486. "email": "second@example.com",
  487. "validation_hash": "7jvwev0oc8sFyEyEwfvDAwxidtGzpAov",
  488. "date_hash_added": "2023-06-22T22:59:56.521Z",
  489. "is_verified": True,
  490. },
  491. }
  492. )
  493. models.append(
  494. {
  495. "model": "sentry.useremail",
  496. "pk": 101,
  497. "fields": {
  498. "user": 2,
  499. "email": "third@example.com",
  500. "validation_hash": "",
  501. "date_hash_added": "2023-06-22T22:59:57.521Z",
  502. "is_verified": False,
  503. },
  504. }
  505. )
  506. json.dump(self.sort_in_memory_json(models), tmp_file)
  507. with open(tmp_path, "rb") as tmp_file:
  508. import_in_user_scope(tmp_file, printer=NOOP_PRINTER)
  509. with assume_test_silo_mode(SiloMode.CONTROL):
  510. assert UserEmail.objects.count() == 3
  511. assert UserEmail.objects.values("user").distinct().count() == 1
  512. assert UserEmail.objects.filter(email="testing@example.com").exists()
  513. assert UserEmail.objects.filter(email="second@example.com").exists()
  514. assert UserEmail.objects.filter(email="third@example.com").exists()
  515. # Validations are scrubbed and regenerated in non-global scopes.
  516. assert UserEmail.objects.filter(validation_hash="").count() == 0
  517. assert UserEmail.objects.filter(is_verified=True).count() == 0
  518. # Regression test for getsentry/self-hosted#2571.
  519. def test_good_multiple_useremails_per_user_in_global_scope(self):
  520. with tempfile.TemporaryDirectory() as tmp_dir:
  521. tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
  522. with open(tmp_path, "w+") as tmp_file:
  523. models = self.json_of_exhaustive_user_with_minimum_privileges()
  524. # Add two copies (1 verified, 1 not) of the same `UserEmail` - so the user now has 3
  525. # `UserEmail` models, the latter of which have no corresponding `Email` entry.
  526. models.append(
  527. {
  528. "model": "sentry.useremail",
  529. "pk": 100,
  530. "fields": {
  531. "user": 2,
  532. "email": "second@example.com",
  533. "validation_hash": "7jvwev0oc8sFyEyEwfvDAwxidtGzpAov",
  534. "date_hash_added": "2023-06-22T22:59:56.521Z",
  535. "is_verified": True,
  536. },
  537. }
  538. )
  539. models.append(
  540. {
  541. "model": "sentry.useremail",
  542. "pk": 101,
  543. "fields": {
  544. "user": 2,
  545. "email": "third@example.com",
  546. "validation_hash": "",
  547. "date_hash_added": "2023-06-22T22:59:57.521Z",
  548. "is_verified": False,
  549. },
  550. }
  551. )
  552. json.dump(self.sort_in_memory_json(models), tmp_file)
  553. with open(tmp_path, "rb") as tmp_file:
  554. import_in_global_scope(tmp_file, printer=NOOP_PRINTER)
  555. with assume_test_silo_mode(SiloMode.CONTROL):
  556. assert UserEmail.objects.count() == 3
  557. assert UserEmail.objects.values("user").distinct().count() == 1
  558. assert UserEmail.objects.filter(email="testing@example.com").exists()
  559. assert UserEmail.objects.filter(email="second@example.com").exists()
  560. assert UserEmail.objects.filter(email="third@example.com").exists()
  561. # Validation hashes are not touched in the global scope.
  562. assert UserEmail.objects.filter(validation_hash="").count() == 1
  563. assert UserEmail.objects.filter(is_verified=True).count() == 2
  564. def test_bad_invalid_user_option(self):
  565. with tempfile.TemporaryDirectory() as tmp_dir:
  566. tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
  567. with open(tmp_path, "w+") as tmp_file:
  568. models = self.json_of_exhaustive_user_with_minimum_privileges()
  569. # Modify the `timezone` option to be in invalid.
  570. for m in models:
  571. if m["model"] == "sentry.useroption" and m["fields"]["key"] == "timezone":
  572. m["fields"]["value"] = '"MiddleEarth/Gondor"'
  573. json.dump(list(models), tmp_file)
  574. with open(tmp_path, "rb") as tmp_file:
  575. with pytest.raises(ImportingError) as err:
  576. import_in_user_scope(tmp_file, printer=NOOP_PRINTER)
  577. assert err.value.context.get_kind() == RpcImportErrorKind.ValidationError
  578. assert err.value.context.on.model == "sentry.useroption"
  579. @region_silo_test
  580. class SignalingTests(ImportTestCase):
  581. """
  582. Some models are automatically created via signals and similar automagic from related models. We
  583. test that behavior here. Specifically, we test the following:
  584. - That `Email` and `UserEmail` are automatically created when `User` is.
  585. - That `OrganizationMapping` and `OrganizationMemberMapping` are automatically created when
  586. `Organization is.
  587. - That `ProjectKey` and `ProjectOption` instances are automatically created when `Project`
  588. is.
  589. """
  590. def test_import_signaling_user(self):
  591. self.create_exhaustive_user("user", email="me@example.com")
  592. with tempfile.TemporaryDirectory() as tmp_dir:
  593. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  594. with open(tmp_path, "rb") as tmp_file:
  595. import_in_user_scope(tmp_file, printer=NOOP_PRINTER)
  596. with assume_test_silo_mode(SiloMode.CONTROL):
  597. assert User.objects.count() == 1
  598. assert User.objects.filter(email="me@example.com").exists()
  599. assert UserEmail.objects.count() == 1
  600. assert UserEmail.objects.filter(email="me@example.com").exists()
  601. assert Email.objects.count() == 1
  602. assert Email.objects.filter(email="me@example.com").exists()
  603. def test_import_signaling_organization(self):
  604. owner = self.create_exhaustive_user("owner")
  605. invited = self.create_exhaustive_user("invited")
  606. member = self.create_exhaustive_user("member")
  607. self.create_exhaustive_organization("some-org", owner, invited, [member])
  608. with tempfile.TemporaryDirectory() as tmp_dir:
  609. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  610. with open(tmp_path, "rb") as tmp_file:
  611. import_in_organization_scope(tmp_file, printer=NOOP_PRINTER)
  612. # There should only be 1 organization at this point
  613. imported_organization = Organization.objects.get()
  614. assert imported_organization.slug == "some-org"
  615. assert OrganizationMember.objects.count() == 3
  616. # The exhaustive org has 2 projects which automatically get 1 key and 3 options each.
  617. assert Project.objects.count() == 2
  618. assert Project.objects.filter(name="project-some-org").exists()
  619. assert Project.objects.filter(name="other-project-some-org").exists()
  620. assert ProjectKey.objects.count() == 2
  621. assert ProjectOption.objects.count() == 6
  622. assert ProjectOption.objects.filter(key="sentry:relay-rev").exists()
  623. assert ProjectOption.objects.filter(key="sentry:relay-rev-lastchange").exists()
  624. assert ProjectOption.objects.filter(key="sentry:option-epoch").exists()
  625. with assume_test_silo_mode(SiloMode.CONTROL):
  626. # An organization slug reservation with a valid primary reservation type
  627. # signals that we've synchronously resolved the slug update RPC correctly.
  628. assert OrganizationSlugReservation.objects.filter(
  629. organization_id=imported_organization.id,
  630. slug="some-org",
  631. reservation_type=OrganizationSlugReservationType.PRIMARY,
  632. ).exists()
  633. assert OrganizationMapping.objects.count() == 1
  634. assert OrganizationMapping.objects.filter(
  635. organization_id=imported_organization.id, slug="some-org"
  636. ).exists()
  637. assert OrganizationMemberMapping.objects.count() == 3
  638. def test_import_signaling_organization_with_control_provisioning_option(self):
  639. with override_options({"hybrid_cloud.control-organization-provisioning": True}):
  640. self.test_import_signaling_organization()
  641. @region_silo_test
  642. class ScopingTests(ImportTestCase):
  643. """
  644. Ensures that only models with the allowed relocation scopes are actually imported.
  645. """
  646. @staticmethod
  647. def verify_model_inclusion(scope: ImportScope):
  648. """
  649. Ensure all in-scope models are included, and that no out-of-scope models are included.
  650. Additionally, we verify that each such model had an appropriate `*ImportChunk` written out
  651. atomically alongside it.
  652. """
  653. included_models = get_matching_exportable_models(
  654. lambda mr: len(mr.get_possible_relocation_scopes() & scope.value) > 0
  655. )
  656. excluded_models = get_matching_exportable_models(
  657. lambda mr: mr.get_possible_relocation_scopes() != {RelocationScope.Excluded}
  658. and not (mr.get_possible_relocation_scopes() & scope.value)
  659. )
  660. for model in included_models:
  661. model_name_str = str(get_model_name(model))
  662. if is_control_model(model):
  663. replica = ControlImportChunkReplica.objects.filter(model=model_name_str).first()
  664. assert replica is not None
  665. with assume_test_silo_mode(SiloMode.CONTROL):
  666. assert model.objects.count() > 0
  667. control = ControlImportChunk.objects.filter(model=model_name_str).first()
  668. assert control is not None
  669. # Ensure that the region-silo replica and the control-silo original are
  670. # identical.
  671. common_fields = {f.name for f in ControlImportChunk._meta.get_fields()} - {
  672. "id",
  673. "date_added",
  674. "date_updated",
  675. }
  676. for field in common_fields:
  677. assert getattr(replica, field, None) == getattr(control, field, None)
  678. else:
  679. assert model.objects.count() > 0
  680. assert RegionImportChunk.objects.filter(model=model_name_str).count() == 1
  681. for model in excluded_models:
  682. if is_control_model(model):
  683. with assume_test_silo_mode(SiloMode.CONTROL):
  684. assert model.objects.count() == 0
  685. else:
  686. assert model.objects.count() == 0
  687. def test_user_import_scoping(self):
  688. self.create_exhaustive_instance(is_superadmin=True)
  689. with tempfile.TemporaryDirectory() as tmp_dir:
  690. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  691. with open(tmp_path, "rb") as tmp_file:
  692. import_in_user_scope(tmp_file, printer=NOOP_PRINTER)
  693. self.verify_model_inclusion(ImportScope.User)
  694. # Test that the import UUID is auto-assigned properly.
  695. with assume_test_silo_mode(SiloMode.CONTROL):
  696. assert ControlImportChunk.objects.values("import_uuid").distinct().count() == 1
  697. assert ControlImportChunkReplica.objects.values("import_uuid").distinct().count() == 1
  698. def test_organization_import_scoping(self):
  699. self.create_exhaustive_instance(is_superadmin=True)
  700. with tempfile.TemporaryDirectory() as tmp_dir:
  701. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  702. with open(tmp_path, "rb") as tmp_file:
  703. import_in_organization_scope(tmp_file, printer=NOOP_PRINTER)
  704. self.verify_model_inclusion(ImportScope.Organization)
  705. # Test that the import UUID is auto-assigned properly.
  706. with assume_test_silo_mode(SiloMode.CONTROL):
  707. assert ControlImportChunk.objects.values("import_uuid").distinct().count() == 1
  708. assert ControlImportChunkReplica.objects.values("import_uuid").distinct().count() == 1
  709. assert RegionImportChunk.objects.values("import_uuid").distinct().count() == 1
  710. assert (
  711. ControlImportChunkReplica.objects.values("import_uuid").first()
  712. == RegionImportChunk.objects.values("import_uuid").first()
  713. )
  714. def test_config_import_scoping(self):
  715. self.create_exhaustive_instance(is_superadmin=True)
  716. with tempfile.TemporaryDirectory() as tmp_dir:
  717. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  718. with open(tmp_path, "rb") as tmp_file:
  719. import_in_config_scope(tmp_file, printer=NOOP_PRINTER)
  720. self.verify_model_inclusion(ImportScope.Config)
  721. # Test that the import UUID is auto-assigned properly.
  722. with assume_test_silo_mode(SiloMode.CONTROL):
  723. assert ControlImportChunk.objects.values("import_uuid").distinct().count() == 1
  724. assert ControlImportChunkReplica.objects.values("import_uuid").distinct().count() == 1
  725. assert RegionImportChunk.objects.values("import_uuid").distinct().count() == 1
  726. assert (
  727. ControlImportChunkReplica.objects.values("import_uuid").first()
  728. == RegionImportChunk.objects.values("import_uuid").first()
  729. )
  730. def test_global_import_scoping(self):
  731. self.create_exhaustive_instance(is_superadmin=True)
  732. with tempfile.TemporaryDirectory() as tmp_dir:
  733. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  734. with open(tmp_path, "rb") as tmp_file:
  735. import_in_global_scope(tmp_file, printer=NOOP_PRINTER)
  736. self.verify_model_inclusion(ImportScope.Global)
  737. # Test that the import UUID is auto-assigned properly.
  738. with assume_test_silo_mode(SiloMode.CONTROL):
  739. assert ControlImportChunk.objects.values("import_uuid").distinct().count() == 1
  740. assert ControlImportChunkReplica.objects.values("import_uuid").distinct().count() == 1
  741. assert RegionImportChunk.objects.values("import_uuid").distinct().count() == 1
  742. assert (
  743. ControlImportChunkReplica.objects.values("import_uuid").first()
  744. == RegionImportChunk.objects.values("import_uuid").first()
  745. )
  746. @region_silo_test
  747. class DatabaseResetTests(ImportTestCase):
  748. """
  749. Ensure that database resets work as intended in different import scopes.
  750. """
  751. def import_empty_backup_file(self, import_fn):
  752. with tempfile.TemporaryDirectory() as tmp_dir:
  753. tmp_empty_file_path = tmp_dir + "empty_backup.json"
  754. with open(tmp_empty_file_path, "w") as tmp_file:
  755. json.dump([], tmp_file)
  756. with open(tmp_empty_file_path, "rb") as empty_backup_json:
  757. import_fn(empty_backup_json, printer=NOOP_PRINTER)
  758. @pytest.mark.skipif(
  759. os.environ.get("SENTRY_USE_MONOLITH_DBS", "0") == "0",
  760. reason="only run when in `SENTRY_USE_MONOLITH_DBS=1` env variable is set",
  761. )
  762. def test_clears_existing_models_in_global_scope(self):
  763. create_default_projects()
  764. self.import_empty_backup_file(import_in_global_scope)
  765. for dependency in dependencies():
  766. model = get_model(dependency)
  767. assert model is not None
  768. assert model.objects.count() == 0
  769. with connections[router.db_for_read(model)].cursor() as cursor:
  770. cursor.execute(f"SELECT MAX(id) FROM {model._meta.db_table}")
  771. sequence_number = cursor.fetchone()[0]
  772. assert sequence_number == 1 or sequence_number is None
  773. # During the setup of a fresh Sentry instance, there are a couple of models that are
  774. # automatically created: the Sentry org, a Sentry team, and an internal project. During a
  775. # global import, we want to avoid persisting these default models and start from scratch.
  776. # These explicit assertions are here just to double check that these models have been wiped.
  777. assert Project.objects.count() == 0
  778. assert ProjectKey.objects.count() == 0
  779. assert Organization.objects.count() == 0
  780. assert OrganizationMember.objects.count() == 0
  781. assert Team.objects.count() == 0
  782. with tempfile.TemporaryDirectory() as tmp_dir:
  783. path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
  784. export_to_file(path, ExportScope.Global)
  785. with open(path) as tmp_file:
  786. assert tmp_file.read() == "[]"
  787. def test_persist_existing_models_in_user_scope(self):
  788. owner = self.create_exhaustive_user("owner", email="owner@example.com")
  789. user = self.create_exhaustive_user("user", email="user@example.com")
  790. self.create_exhaustive_organization("neworg", owner, user, None)
  791. assert Organization.objects.count() == 1
  792. with assume_test_silo_mode(SiloMode.CONTROL):
  793. assert User.objects.count() == 3
  794. self.import_empty_backup_file(import_in_user_scope)
  795. assert Organization.objects.count() == 1
  796. with assume_test_silo_mode(SiloMode.CONTROL):
  797. assert User.objects.count() == 3
  798. def test_persist_existing_models_in_config_scope(self):
  799. owner = self.create_exhaustive_user("owner", email="owner@example.com")
  800. user = self.create_exhaustive_user("user", email="user@example.com")
  801. self.create_exhaustive_organization("neworg", owner, user, None)
  802. assert Organization.objects.count() == 1
  803. with assume_test_silo_mode(SiloMode.CONTROL):
  804. assert User.objects.count() == 3
  805. self.import_empty_backup_file(import_in_config_scope)
  806. assert Organization.objects.count() == 1
  807. with assume_test_silo_mode(SiloMode.CONTROL):
  808. assert User.objects.count() == 3
  809. def test_persist_existing_models_in_organization_scope(self):
  810. owner = self.create_exhaustive_user("owner", email="owner@example.com")
  811. user = self.create_exhaustive_user("user", email="user@example.com")
  812. self.create_exhaustive_organization("neworg", owner, user, None)
  813. assert Organization.objects.count() == 1
  814. with assume_test_silo_mode(SiloMode.CONTROL):
  815. assert User.objects.count() == 3
  816. self.import_empty_backup_file(import_in_organization_scope)
  817. assert Organization.objects.count() == 1
  818. with assume_test_silo_mode(SiloMode.CONTROL):
  819. assert User.objects.count() == 3
  820. # Filters should work identically in both silo and monolith modes, so no need to repeat the tests
  821. # here.
  822. @region_silo_test
  823. class DecryptionTests(ImportTestCase):
  824. """
  825. Ensures that decryption actually works. We only test one model for each scope, because it's
  826. extremely unlikely that a failed decryption will leave only part of the data unmangled.
  827. """
  828. @staticmethod
  829. def encrypt_json_fixture(tmp_dir) -> tuple[Path, Path]:
  830. good_file_path = get_fixture_path("backup", "fresh-install.json")
  831. (priv_key_pem, pub_key_pem) = generate_rsa_key_pair()
  832. tmp_priv_key_path = Path(tmp_dir).joinpath("key")
  833. with open(tmp_priv_key_path, "wb") as f:
  834. f.write(priv_key_pem)
  835. tmp_pub_key_path = Path(tmp_dir).joinpath("key.pub")
  836. with open(tmp_pub_key_path, "wb") as f:
  837. f.write(pub_key_pem)
  838. with open(good_file_path) as f:
  839. json_data = json.load(f)
  840. tmp_tarball_path = Path(tmp_dir).joinpath("input.tar")
  841. with open(tmp_tarball_path, "wb") as i, open(tmp_pub_key_path, "rb") as p:
  842. pem = p.read()
  843. data_encryption_key = Fernet.generate_key()
  844. backup_encryptor = Fernet(data_encryption_key)
  845. encrypted_json_export = backup_encryptor.encrypt(json.dumps(json_data).encode("utf-8"))
  846. dek_encryption_key = serialization.load_pem_public_key(pem, default_backend())
  847. sha256 = hashes.SHA256()
  848. mgf = padding.MGF1(algorithm=sha256)
  849. oaep_padding = padding.OAEP(mgf=mgf, algorithm=sha256, label=None)
  850. encrypted_dek = dek_encryption_key.encrypt(data_encryption_key, oaep_padding) # type: ignore
  851. tar_buffer = io.BytesIO()
  852. with tarfile.open(fileobj=tar_buffer, mode="w") as tar:
  853. json_info = tarfile.TarInfo("export.json")
  854. json_info.size = len(encrypted_json_export)
  855. tar.addfile(json_info, fileobj=io.BytesIO(encrypted_json_export))
  856. key_info = tarfile.TarInfo("data.key")
  857. key_info.size = len(encrypted_dek)
  858. tar.addfile(key_info, fileobj=io.BytesIO(encrypted_dek))
  859. pub_info = tarfile.TarInfo("key.pub")
  860. pub_info.size = len(pem)
  861. tar.addfile(pub_info, fileobj=io.BytesIO(pem))
  862. i.write(tar_buffer.getvalue())
  863. return (tmp_tarball_path, tmp_priv_key_path)
  864. def test_user_import_decryption(self):
  865. with tempfile.TemporaryDirectory() as tmp_dir:
  866. (tmp_tarball_path, tmp_priv_key_path) = self.encrypt_json_fixture(tmp_dir)
  867. with assume_test_silo_mode(SiloMode.CONTROL):
  868. assert User.objects.count() == 0
  869. with open(tmp_tarball_path, "rb") as tmp_tarball_file, open(
  870. tmp_priv_key_path, "rb"
  871. ) as tmp_priv_key_file:
  872. import_in_user_scope(
  873. tmp_tarball_file,
  874. decryptor=LocalFileDecryptor(tmp_priv_key_file),
  875. printer=NOOP_PRINTER,
  876. )
  877. with assume_test_silo_mode(SiloMode.CONTROL):
  878. assert User.objects.count() > 0
  879. def test_organization_import_decryption(self):
  880. with tempfile.TemporaryDirectory() as tmp_dir:
  881. (tmp_tarball_path, tmp_priv_key_path) = self.encrypt_json_fixture(tmp_dir)
  882. assert Organization.objects.count() == 0
  883. with open(tmp_tarball_path, "rb") as tmp_tarball_file, open(
  884. tmp_priv_key_path, "rb"
  885. ) as tmp_priv_key_file:
  886. import_in_organization_scope(
  887. tmp_tarball_file,
  888. decryptor=LocalFileDecryptor(tmp_priv_key_file),
  889. printer=NOOP_PRINTER,
  890. )
  891. assert Organization.objects.count() > 0
  892. def test_config_import_decryption(self):
  893. with tempfile.TemporaryDirectory() as tmp_dir:
  894. (tmp_tarball_path, tmp_priv_key_path) = self.encrypt_json_fixture(tmp_dir)
  895. with assume_test_silo_mode(SiloMode.CONTROL):
  896. assert UserRole.objects.count() == 0
  897. with open(tmp_tarball_path, "rb") as tmp_tarball_file, open(
  898. tmp_priv_key_path, "rb"
  899. ) as tmp_priv_key_file:
  900. import_in_config_scope(
  901. tmp_tarball_file,
  902. decryptor=LocalFileDecryptor(tmp_priv_key_file),
  903. printer=NOOP_PRINTER,
  904. )
  905. with assume_test_silo_mode(SiloMode.CONTROL):
  906. assert UserRole.objects.count() > 0
  907. def test_global_import_decryption(self):
  908. with tempfile.TemporaryDirectory() as tmp_dir:
  909. (tmp_tarball_path, tmp_priv_key_path) = self.encrypt_json_fixture(tmp_dir)
  910. assert Organization.objects.count() == 0
  911. with assume_test_silo_mode(SiloMode.CONTROL):
  912. assert User.objects.count() == 0
  913. assert UserRole.objects.count() == 0
  914. with open(tmp_tarball_path, "rb") as tmp_tarball_file, open(
  915. tmp_priv_key_path, "rb"
  916. ) as tmp_priv_key_file:
  917. import_in_global_scope(
  918. tmp_tarball_file,
  919. decryptor=LocalFileDecryptor(tmp_priv_key_file),
  920. printer=NOOP_PRINTER,
  921. )
  922. assert Organization.objects.count() > 0
  923. with assume_test_silo_mode(SiloMode.CONTROL):
  924. assert User.objects.count() > 0
  925. assert UserRole.objects.count() > 0
  926. # Filters should work identically in both silo and monolith modes, so no need to repeat the tests
  927. # here.
  928. @region_silo_test
  929. class FilterTests(ImportTestCase):
  930. """
  931. Ensures that filtering operations include the correct models.
  932. """
  933. def test_import_filter_users(self):
  934. self.create_exhaustive_user("user_1")
  935. self.create_exhaustive_user("user_2")
  936. with tempfile.TemporaryDirectory() as tmp_dir:
  937. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  938. with open(tmp_path, "rb") as tmp_file:
  939. import_in_user_scope(tmp_file, user_filter={"user_2"}, printer=NOOP_PRINTER)
  940. with assume_test_silo_mode(SiloMode.CONTROL):
  941. # Count users, but also count a random model naively derived from just `User` alone,
  942. # like `UserIP`. Because `Email` and `UserEmail` have some automagic going on that
  943. # causes them to be created when a `User` is, we explicitly check to ensure that they
  944. # are behaving correctly as well.
  945. assert User.objects.count() == 1
  946. assert UserIP.objects.count() == 1
  947. assert UserEmail.objects.count() == 1
  948. assert Email.objects.count() == 1
  949. assert (
  950. ControlImportChunk.objects.filter(
  951. model="sentry.user", min_ordinal=1, max_ordinal=1
  952. ).count()
  953. == 1
  954. )
  955. assert (
  956. ControlImportChunk.objects.filter(
  957. model="sentry.userip", min_ordinal=1, max_ordinal=1
  958. ).count()
  959. == 1
  960. )
  961. assert (
  962. ControlImportChunk.objects.filter(
  963. model="sentry.useremail", min_ordinal=1, max_ordinal=1
  964. ).count()
  965. == 1
  966. )
  967. assert (
  968. ControlImportChunk.objects.filter(
  969. model="sentry.email", min_ordinal=1, max_ordinal=1
  970. ).count()
  971. == 1
  972. )
  973. assert not User.objects.filter(username="user_1").exists()
  974. assert User.objects.filter(username="user_2").exists()
  975. def test_export_filter_users_shared_email(self):
  976. self.create_exhaustive_user("user_1", email="a@example.com")
  977. self.create_exhaustive_user("user_2", email="b@example.com")
  978. self.create_exhaustive_user("user_3", email="a@example.com")
  979. self.create_exhaustive_user("user_4", email="b@example.com")
  980. with tempfile.TemporaryDirectory() as tmp_dir:
  981. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  982. with open(tmp_path, "rb") as tmp_file:
  983. import_in_user_scope(
  984. tmp_file, user_filter={"user_1", "user_2", "user_3"}, printer=NOOP_PRINTER
  985. )
  986. with assume_test_silo_mode(SiloMode.CONTROL):
  987. assert User.objects.count() == 3
  988. assert UserIP.objects.count() == 3
  989. assert UserEmail.objects.count() == 3
  990. assert Email.objects.count() == 2 # Lower due to shared emails
  991. assert (
  992. ControlImportChunk.objects.filter(
  993. model="sentry.user", min_ordinal=1, max_ordinal=3
  994. ).count()
  995. == 1
  996. )
  997. assert (
  998. ControlImportChunk.objects.filter(
  999. model="sentry.userip", min_ordinal=1, max_ordinal=3
  1000. ).count()
  1001. == 1
  1002. )
  1003. assert (
  1004. ControlImportChunk.objects.filter(
  1005. model="sentry.useremail", min_ordinal=1, max_ordinal=3
  1006. ).count()
  1007. == 1
  1008. )
  1009. assert (
  1010. ControlImportChunk.objects.filter(
  1011. model="sentry.email", min_ordinal=1, max_ordinal=2
  1012. ).count()
  1013. == 1
  1014. )
  1015. assert User.objects.filter(username="user_1").exists()
  1016. assert User.objects.filter(username="user_2").exists()
  1017. assert User.objects.filter(username="user_3").exists()
  1018. assert not User.objects.filter(username="user_4").exists()
  1019. def test_import_filter_users_empty(self):
  1020. self.create_exhaustive_user("user_1")
  1021. self.create_exhaustive_user("user_2")
  1022. with tempfile.TemporaryDirectory() as tmp_dir:
  1023. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  1024. with open(tmp_path, "rb") as tmp_file:
  1025. import_in_user_scope(tmp_file, user_filter=set(), printer=NOOP_PRINTER)
  1026. with assume_test_silo_mode(SiloMode.CONTROL):
  1027. assert User.objects.count() == 0
  1028. assert UserIP.objects.count() == 0
  1029. assert UserEmail.objects.count() == 0
  1030. assert Email.objects.count() == 0
  1031. def test_import_filter_orgs_single(self):
  1032. a = self.create_exhaustive_user("user_a_only", email="shared@example.com")
  1033. b = self.create_exhaustive_user("user_b_only", email="shared@example.com")
  1034. c = self.create_exhaustive_user("user_c_only", email="shared@example.com")
  1035. a_b = self.create_exhaustive_user("user_a_and_b")
  1036. b_c = self.create_exhaustive_user("user_b_and_c")
  1037. a_b_c = self.create_exhaustive_user("user_all", email="shared@example.com")
  1038. self.create_exhaustive_organization("org-a", a, a_b, [a_b_c])
  1039. self.create_exhaustive_organization("org-b", b_c, a_b_c, [b, a_b])
  1040. self.create_exhaustive_organization("org-c", a_b_c, b_c, [c])
  1041. with tempfile.TemporaryDirectory() as tmp_dir:
  1042. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  1043. with open(tmp_path, "rb") as tmp_file:
  1044. import_in_organization_scope(tmp_file, org_filter={"org-b"}, printer=NOOP_PRINTER)
  1045. assert Organization.objects.count() == 1
  1046. assert (
  1047. RegionImportChunk.objects.filter(
  1048. model="sentry.organization", min_ordinal=1, max_ordinal=1
  1049. ).count()
  1050. == 1
  1051. )
  1052. assert not Organization.objects.filter(slug="org-a").exists()
  1053. assert Organization.objects.filter(slug="org-b").exists()
  1054. assert not Organization.objects.filter(slug="org-c").exists()
  1055. with assume_test_silo_mode(SiloMode.CONTROL):
  1056. assert OrgAuthToken.objects.count() == 1
  1057. assert User.objects.count() == 4
  1058. assert UserIP.objects.count() == 4
  1059. assert UserEmail.objects.count() == 4
  1060. assert Email.objects.count() == 3 # Lower due to `shared@example.com`
  1061. assert not User.objects.filter(username="user_a_only").exists()
  1062. assert User.objects.filter(username="user_b_only").exists()
  1063. assert not User.objects.filter(username="user_c_only").exists()
  1064. assert User.objects.filter(username="user_a_and_b").exists()
  1065. assert User.objects.filter(username="user_b_and_c").exists()
  1066. assert User.objects.filter(username="user_all").exists()
  1067. def test_import_filter_orgs_multiple(self):
  1068. a = self.create_exhaustive_user("user_a_only", email="shared@example.com")
  1069. b = self.create_exhaustive_user("user_b_only", email="shared@example.com")
  1070. c = self.create_exhaustive_user("user_c_only", email="shared@example.com")
  1071. a_b = self.create_exhaustive_user("user_a_and_b")
  1072. b_c = self.create_exhaustive_user("user_b_and_c")
  1073. a_b_c = self.create_exhaustive_user("user_all", email="shared@example.com")
  1074. self.create_exhaustive_organization("org-a", a, a_b, [a_b_c])
  1075. self.create_exhaustive_organization("org-b", b_c, a_b_c, [b, a_b])
  1076. self.create_exhaustive_organization("org-c", a_b_c, b_c, [c])
  1077. with tempfile.TemporaryDirectory() as tmp_dir:
  1078. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  1079. with open(tmp_path, "rb") as tmp_file:
  1080. import_in_organization_scope(
  1081. tmp_file, org_filter={"org-a", "org-c"}, printer=NOOP_PRINTER
  1082. )
  1083. assert Organization.objects.count() == 2
  1084. assert (
  1085. RegionImportChunk.objects.filter(
  1086. model="sentry.organization", min_ordinal=1, max_ordinal=2
  1087. ).count()
  1088. == 1
  1089. )
  1090. assert Organization.objects.filter(slug="org-a").exists()
  1091. assert not Organization.objects.filter(slug="org-b").exists()
  1092. assert Organization.objects.filter(slug="org-c").exists()
  1093. with assume_test_silo_mode(SiloMode.CONTROL):
  1094. assert OrgAuthToken.objects.count() == 2
  1095. assert (
  1096. ControlImportChunk.objects.filter(
  1097. model="sentry.orgauthtoken", min_ordinal=1, max_ordinal=2
  1098. ).count()
  1099. == 1
  1100. )
  1101. assert User.objects.count() == 5
  1102. assert UserIP.objects.count() == 5
  1103. assert UserEmail.objects.count() == 5
  1104. assert Email.objects.count() == 3 # Lower due to `shared@example.com`
  1105. assert User.objects.filter(username="user_a_only").exists()
  1106. assert not User.objects.filter(username="user_b_only").exists()
  1107. assert User.objects.filter(username="user_c_only").exists()
  1108. assert User.objects.filter(username="user_a_and_b").exists()
  1109. assert User.objects.filter(username="user_b_and_c").exists()
  1110. assert User.objects.filter(username="user_all").exists()
  1111. def test_import_filter_orgs_empty(self):
  1112. a = self.create_exhaustive_user("user_a_only")
  1113. b = self.create_exhaustive_user("user_b_only")
  1114. c = self.create_exhaustive_user("user_c_only")
  1115. a_b = self.create_exhaustive_user("user_a_and_b")
  1116. b_c = self.create_exhaustive_user("user_b_and_c")
  1117. a_b_c = self.create_exhaustive_user("user_all")
  1118. self.create_exhaustive_organization("org-a", a, a_b, [a_b_c])
  1119. self.create_exhaustive_organization("org-b", b_c, a_b_c, [b, a_b])
  1120. self.create_exhaustive_organization("org-c", a_b_c, b_c, [c])
  1121. with tempfile.TemporaryDirectory() as tmp_dir:
  1122. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  1123. with open(tmp_path, "rb") as tmp_file:
  1124. import_in_organization_scope(tmp_file, org_filter=set(), printer=NOOP_PRINTER)
  1125. assert Organization.objects.count() == 0
  1126. with assume_test_silo_mode(SiloMode.CONTROL):
  1127. assert OrgAuthToken.objects.count() == 0
  1128. assert User.objects.count() == 0
  1129. assert UserIP.objects.count() == 0
  1130. assert UserEmail.objects.count() == 0
  1131. assert Email.objects.count() == 0
  1132. COLLISION_TESTED: set[NormalizedModelName] = set()
  1133. @region_silo_test
  1134. class CollisionTests(ImportTestCase):
  1135. """
  1136. Ensure that collisions are properly handled in different flag modes.
  1137. """
  1138. @expect_models(COLLISION_TESTED, ApiToken)
  1139. def test_colliding_api_token(self, expected_models: list[type[Model]]):
  1140. owner = self.create_exhaustive_user("owner")
  1141. expires_at = timezone.now() + DEFAULT_EXPIRATION
  1142. # Take note of the `ApiTokens` that were created by the exhaustive organization - this is
  1143. # the one we'll be importing.
  1144. with assume_test_silo_mode(SiloMode.CONTROL):
  1145. colliding_no_refresh_set = ApiToken.objects.create(user=owner, token=generate_token())
  1146. colliding_no_refresh_set.refresh_token = None
  1147. colliding_no_refresh_set.expires_at = None
  1148. colliding_no_refresh_set.save()
  1149. colliding_same_refresh_only = ApiToken.objects.create(
  1150. user=owner,
  1151. token=generate_token(),
  1152. refresh_token=generate_token(),
  1153. expires_at=expires_at,
  1154. )
  1155. colliding_same_token_only = ApiToken.objects.create(
  1156. user=owner,
  1157. token=generate_token(),
  1158. refresh_token=generate_token(),
  1159. expires_at=expires_at,
  1160. )
  1161. colliding_same_both = ApiToken.objects.create(
  1162. user=owner,
  1163. token=generate_token(),
  1164. refresh_token=generate_token(),
  1165. expires_at=expires_at,
  1166. )
  1167. with tempfile.TemporaryDirectory() as tmp_dir:
  1168. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  1169. owner = self.create_exhaustive_user(username="owner")
  1170. # Re-insert colliding tokens, pointed at the new user.
  1171. with assume_test_silo_mode(SiloMode.CONTROL):
  1172. colliding_no_refresh_set.user_id = owner.id
  1173. colliding_no_refresh_set.save()
  1174. colliding_same_refresh_only.token = generate_token()
  1175. colliding_same_refresh_only.user_id = owner.id
  1176. colliding_same_refresh_only.save()
  1177. colliding_same_token_only.refresh_token = generate_token()
  1178. colliding_same_token_only.user_id = owner.id
  1179. colliding_same_token_only.save()
  1180. colliding_same_both.user_id = owner.id
  1181. colliding_same_both.save()
  1182. assert ApiToken.objects.count() == 4
  1183. assert (
  1184. ApiToken.objects.filter(
  1185. token=colliding_no_refresh_set.token,
  1186. refresh_token__isnull=True,
  1187. expires_at__isnull=True,
  1188. ).count()
  1189. == 1
  1190. )
  1191. assert (
  1192. ApiToken.objects.filter(
  1193. refresh_token=colliding_same_refresh_only.refresh_token
  1194. ).count()
  1195. == 1
  1196. )
  1197. assert ApiToken.objects.filter(token=colliding_same_token_only.token).count() == 1
  1198. assert (
  1199. ApiToken.objects.filter(
  1200. token=colliding_same_both.token,
  1201. refresh_token=colliding_same_both.refresh_token,
  1202. ).count()
  1203. == 1
  1204. )
  1205. with open(tmp_path, "rb") as tmp_file:
  1206. import_in_config_scope(tmp_file, printer=NOOP_PRINTER)
  1207. # Ensure that old tokens have not been mutated.
  1208. with assume_test_silo_mode(SiloMode.CONTROL):
  1209. assert ApiToken.objects.count() == 8
  1210. assert (
  1211. ApiToken.objects.filter(
  1212. token=colliding_no_refresh_set.token,
  1213. refresh_token__isnull=True,
  1214. expires_at__isnull=True,
  1215. ).count()
  1216. == 1
  1217. )
  1218. assert (
  1219. ApiToken.objects.filter(
  1220. refresh_token=colliding_same_refresh_only.refresh_token
  1221. ).count()
  1222. == 1
  1223. )
  1224. assert ApiToken.objects.filter(token=colliding_same_token_only.token).count() == 1
  1225. assert (
  1226. ApiToken.objects.filter(
  1227. token=colliding_same_both.token,
  1228. refresh_token=colliding_same_both.refresh_token,
  1229. ).count()
  1230. == 1
  1231. )
  1232. # Ensure newly added entries with nulled `refresh_token` and/or `expires_at` have
  1233. # kept those fields nulled.
  1234. assert (
  1235. ApiToken.objects.filter(
  1236. refresh_token__isnull=True,
  1237. expires_at__isnull=True,
  1238. ).count()
  1239. == 2
  1240. )
  1241. with open(tmp_path, "rb") as tmp_file:
  1242. verify_models_in_output(expected_models, json.load(tmp_file))
  1243. @expect_models(COLLISION_TESTED, Monitor)
  1244. def test_colliding_monitor(self, expected_models: list[type[Model]]):
  1245. owner = self.create_exhaustive_user("owner")
  1246. invited = self.create_exhaustive_user("invited")
  1247. self.create_exhaustive_organization("some-org", owner, invited)
  1248. # Take note of a `Monitor` that was created by the exhaustive organization - this is the
  1249. # one we'll be importing.
  1250. colliding = Monitor.objects.filter().first()
  1251. with tempfile.TemporaryDirectory() as tmp_dir:
  1252. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  1253. # After exporting and clearing the database, insert a copy of the same `Monitor` as
  1254. # the one found in the import.
  1255. colliding.organization_id = self.create_organization().id
  1256. colliding.project_id = self.create_project().id
  1257. colliding.save()
  1258. assert Monitor.objects.count() == 1
  1259. assert Monitor.objects.filter(guid=colliding.guid).count() == 1
  1260. with open(tmp_path, "rb") as tmp_file:
  1261. import_in_organization_scope(tmp_file, printer=NOOP_PRINTER)
  1262. assert Monitor.objects.count() == 2
  1263. assert Monitor.objects.filter(guid=colliding.guid).count() == 1
  1264. with open(tmp_path, "rb") as tmp_file:
  1265. verify_models_in_output(expected_models, json.load(tmp_file))
  1266. @expect_models(COLLISION_TESTED, OrgAuthToken)
  1267. def test_colliding_org_auth_token(self, expected_models: list[type[Model]]):
  1268. owner = self.create_exhaustive_user("owner")
  1269. invited = self.create_exhaustive_user("invited")
  1270. member = self.create_exhaustive_user("member")
  1271. self.create_exhaustive_organization("some-org", owner, invited, [member])
  1272. # Take note of the `OrgAuthToken` that was created by the exhaustive organization - this is
  1273. # the one we'll be importing.
  1274. with assume_test_silo_mode(SiloMode.CONTROL):
  1275. colliding = OrgAuthToken.objects.filter().first()
  1276. with tempfile.TemporaryDirectory() as tmp_dir:
  1277. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  1278. # After exporting and clearing the database, insert a copy of the same `OrgAuthToken` as
  1279. # the one found in the import.
  1280. org = self.create_organization()
  1281. with assume_test_silo_mode(SiloMode.CONTROL):
  1282. colliding.organization_id = org.id
  1283. colliding.project_last_used_id = self.create_project(organization=org).id
  1284. colliding.save()
  1285. assert OrgAuthToken.objects.count() == 1
  1286. assert OrgAuthToken.objects.filter(token_hashed=colliding.token_hashed).count() == 1
  1287. assert (
  1288. OrgAuthToken.objects.filter(
  1289. token_last_characters=colliding.token_last_characters
  1290. ).count()
  1291. == 1
  1292. )
  1293. with open(tmp_path, "rb") as tmp_file:
  1294. import_in_organization_scope(tmp_file, printer=NOOP_PRINTER)
  1295. with assume_test_silo_mode(SiloMode.CONTROL):
  1296. assert OrgAuthToken.objects.count() == 2
  1297. assert OrgAuthToken.objects.filter(token_hashed=colliding.token_hashed).count() == 1
  1298. assert (
  1299. OrgAuthToken.objects.filter(
  1300. token_last_characters=colliding.token_last_characters
  1301. ).count()
  1302. == 1
  1303. )
  1304. with open(tmp_path, "rb") as tmp_file:
  1305. verify_models_in_output(expected_models, json.load(tmp_file))
  1306. @expect_models(COLLISION_TESTED, ProjectKey)
  1307. def test_colliding_project_key(self, expected_models: list[type[Model]]):
  1308. owner = self.create_exhaustive_user("owner")
  1309. invited = self.create_exhaustive_user("invited")
  1310. member = self.create_exhaustive_user("member")
  1311. self.create_exhaustive_organization("some-org", owner, invited, [member])
  1312. # Take note of a `ProjectKey` that was created by the exhaustive organization - this is the
  1313. # one we'll be importing.
  1314. colliding = ProjectKey.objects.filter().first()
  1315. with tempfile.TemporaryDirectory() as tmp_dir:
  1316. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  1317. # After exporting and clearing the database, insert a copy of the same `ProjectKey` as
  1318. # the one found in the import.
  1319. colliding.project = self.create_project()
  1320. colliding.save()
  1321. assert ProjectKey.objects.count() < 4
  1322. assert ProjectKey.objects.filter(public_key=colliding.public_key).count() == 1
  1323. assert ProjectKey.objects.filter(secret_key=colliding.secret_key).count() == 1
  1324. with open(tmp_path, "rb") as tmp_file:
  1325. import_in_organization_scope(tmp_file, printer=NOOP_PRINTER)
  1326. assert ProjectKey.objects.count() == 4
  1327. assert ProjectKey.objects.filter(public_key=colliding.public_key).count() == 1
  1328. assert ProjectKey.objects.filter(secret_key=colliding.secret_key).count() == 1
  1329. with open(tmp_path, "rb") as tmp_file:
  1330. verify_models_in_output(expected_models, json.load(tmp_file))
  1331. @pytest.mark.xfail(
  1332. not use_split_dbs(),
  1333. reason="Preexisting failure: getsentry/team-ospo#206",
  1334. raises=urllib3.exceptions.MaxRetryError,
  1335. strict=True,
  1336. )
  1337. @expect_models(COLLISION_TESTED, QuerySubscription)
  1338. def test_colliding_query_subscription(self, expected_models: list[type[Model]]):
  1339. # We need a celery task running to properly test the `subscription_id` assignment, otherwise
  1340. # its value just defaults to `None`.
  1341. with self.tasks():
  1342. owner = self.create_exhaustive_user("owner")
  1343. invited = self.create_exhaustive_user("invited")
  1344. member = self.create_exhaustive_user("member")
  1345. self.create_exhaustive_organization("some-org", owner, invited, [member])
  1346. # Take note of the `QuerySubscription` that was created by the exhaustive organization -
  1347. # this is the one we'll be importing.
  1348. colliding_snuba_query = SnubaQuery.objects.all().first()
  1349. colliding_query_subscription = QuerySubscription.objects.filter(
  1350. snuba_query=colliding_snuba_query
  1351. ).first()
  1352. with tempfile.TemporaryDirectory() as tmp_dir:
  1353. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  1354. # After exporting and clearing the database, insert a copy of the same
  1355. # `QuerySubscription.subscription_id` as the one found in the import.
  1356. colliding_snuba_query.save()
  1357. colliding_query_subscription.project = self.create_project()
  1358. colliding_query_subscription.snuba_query = colliding_snuba_query
  1359. colliding_query_subscription.save()
  1360. assert SnubaQuery.objects.count() == 1
  1361. assert QuerySubscription.objects.count() == 1
  1362. assert (
  1363. QuerySubscription.objects.filter(
  1364. subscription_id=colliding_query_subscription.subscription_id
  1365. ).count()
  1366. == 1
  1367. )
  1368. with open(tmp_path, "rb") as tmp_file:
  1369. import_in_organization_scope(tmp_file, printer=NOOP_PRINTER)
  1370. assert SnubaQuery.objects.count() > 1
  1371. assert QuerySubscription.objects.count() > 1
  1372. assert (
  1373. QuerySubscription.objects.filter(
  1374. subscription_id=colliding_query_subscription.subscription_id
  1375. ).count()
  1376. == 1
  1377. )
  1378. with open(tmp_path, "rb") as tmp_file:
  1379. verify_models_in_output(expected_models, json.load(tmp_file))
  1380. @expect_models(COLLISION_TESTED, SavedSearch)
  1381. def test_colliding_saved_search(self, expected_models: list[type[Model]]):
  1382. self.create_organization("some-org", owner=self.user)
  1383. SavedSearch.objects.create(
  1384. name="Global Search",
  1385. query="saved query",
  1386. is_global=True,
  1387. visibility=Visibility.ORGANIZATION,
  1388. )
  1389. assert SavedSearch.objects.count() == 1
  1390. with tempfile.TemporaryDirectory() as tmp_dir:
  1391. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  1392. assert SavedSearch.objects.count() == 0
  1393. # Allow `is_global` searches for `ImportScope.Global` imports.
  1394. with open(tmp_path, "rb") as tmp_file:
  1395. import_in_global_scope(tmp_file, printer=NOOP_PRINTER)
  1396. assert SavedSearch.objects.count() == 1
  1397. # Disallow `is_global` searches for `ImportScope.Organization` imports.
  1398. with open(tmp_path, "rb") as tmp_file:
  1399. import_in_organization_scope(tmp_file, printer=NOOP_PRINTER)
  1400. assert SavedSearch.objects.count() == 1
  1401. with open(tmp_path, "rb") as tmp_file:
  1402. verify_models_in_output(expected_models, json.load(tmp_file))
  1403. @expect_models(COLLISION_TESTED, ControlOption, Option, Relay, RelayUsage, UserRole)
  1404. def test_colliding_configs_overwrite_configs_enabled_in_config_scope(
  1405. self, expected_models: list[type[Model]]
  1406. ):
  1407. owner = self.create_exhaustive_user("owner", is_admin=True)
  1408. self.create_exhaustive_global_configs(owner)
  1409. # Take note of the configs we want to track - this is the one we'll be importing.
  1410. colliding_option = Option.objects.all().first()
  1411. colliding_relay = Relay.objects.all().first()
  1412. colliding_relay_usage = RelayUsage.objects.all().first()
  1413. old_relay_public_key = colliding_relay.public_key
  1414. old_relay_usage_public_key = colliding_relay_usage.public_key
  1415. with assume_test_silo_mode(SiloMode.CONTROL):
  1416. colliding_control_option = ControlOption.objects.all().first()
  1417. colliding_user_role = UserRole.objects.all().first()
  1418. old_user_role_permissions = colliding_user_role.permissions
  1419. with tempfile.TemporaryDirectory() as tmp_dir:
  1420. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  1421. colliding_option.value = "y"
  1422. colliding_option.save()
  1423. colliding_relay.public_key = "invalid"
  1424. colliding_relay.save()
  1425. colliding_relay_usage.public_key = "invalid"
  1426. colliding_relay_usage.save()
  1427. assert Option.objects.count() == 1
  1428. assert Relay.objects.count() == 1
  1429. assert RelayUsage.objects.count() == 1
  1430. with assume_test_silo_mode(SiloMode.CONTROL):
  1431. colliding_control_option.value = "z"
  1432. colliding_control_option.save()
  1433. colliding_user_role.permissions = ["other.admin"]
  1434. colliding_user_role.save()
  1435. assert ControlOption.objects.count() == 1
  1436. assert UserRole.objects.count() == 1
  1437. with open(tmp_path, "rb") as tmp_file:
  1438. import_in_config_scope(
  1439. tmp_file, flags=ImportFlags(overwrite_configs=True), printer=NOOP_PRINTER
  1440. )
  1441. option_chunk = RegionImportChunk.objects.get(
  1442. model="sentry.option", min_ordinal=1, max_ordinal=1
  1443. )
  1444. assert len(option_chunk.inserted_map) == 0
  1445. assert len(option_chunk.existing_map) == 0
  1446. assert len(option_chunk.overwrite_map) == 1
  1447. assert Option.objects.count() == 1
  1448. assert Option.objects.filter(value__exact="a").exists()
  1449. relay_chunk = RegionImportChunk.objects.get(
  1450. model="sentry.relay", min_ordinal=1, max_ordinal=1
  1451. )
  1452. assert len(relay_chunk.inserted_map) == 0
  1453. assert len(relay_chunk.existing_map) == 0
  1454. assert len(relay_chunk.overwrite_map) == 1
  1455. assert Relay.objects.count() == 1
  1456. assert Relay.objects.filter(public_key__exact=old_relay_public_key).exists()
  1457. relay_usage_chunk = RegionImportChunk.objects.get(
  1458. model="sentry.relayusage", min_ordinal=1, max_ordinal=1
  1459. )
  1460. assert len(relay_usage_chunk.inserted_map) == 0
  1461. assert len(relay_usage_chunk.existing_map) == 0
  1462. assert len(relay_usage_chunk.overwrite_map) == 1
  1463. assert RelayUsage.objects.count() == 1
  1464. assert RelayUsage.objects.filter(public_key__exact=old_relay_usage_public_key).exists()
  1465. with assume_test_silo_mode(SiloMode.CONTROL):
  1466. control_option_chunk = ControlImportChunk.objects.get(
  1467. model="sentry.controloption", min_ordinal=1, max_ordinal=1
  1468. )
  1469. assert len(control_option_chunk.inserted_map) == 0
  1470. assert len(control_option_chunk.existing_map) == 0
  1471. assert len(control_option_chunk.overwrite_map) == 1
  1472. assert ControlOption.objects.count() == 1
  1473. assert ControlOption.objects.filter(value__exact="b").exists()
  1474. actual_user_role = UserRole.objects.first()
  1475. assert len(actual_user_role.permissions) == len(old_user_role_permissions)
  1476. for i, actual_permission in enumerate(actual_user_role.permissions):
  1477. assert actual_permission == old_user_role_permissions[i]
  1478. with open(tmp_path, "rb") as tmp_file:
  1479. verify_models_in_output(expected_models, json.load(tmp_file))
  1480. @expect_models(COLLISION_TESTED, ControlOption, Option, Relay, RelayUsage, UserRole)
  1481. def test_colliding_configs_overwrite_configs_disabled_in_config_scope(
  1482. self, expected_models: list[type[Model]]
  1483. ):
  1484. owner = self.create_exhaustive_user("owner", is_admin=True)
  1485. self.create_exhaustive_global_configs(owner)
  1486. # Take note of the configs we want to track - this is the one we'll be importing.
  1487. colliding_option = Option.objects.all().first()
  1488. colliding_relay = Relay.objects.all().first()
  1489. colliding_relay_usage = RelayUsage.objects.all().first()
  1490. with assume_test_silo_mode(SiloMode.CONTROL):
  1491. colliding_control_option = ControlOption.objects.all().first()
  1492. colliding_user_role = UserRole.objects.all().first()
  1493. with tempfile.TemporaryDirectory() as tmp_dir:
  1494. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  1495. colliding_option.value = "y"
  1496. colliding_option.save()
  1497. colliding_relay.public_key = "invalid"
  1498. colliding_relay.save()
  1499. colliding_relay_usage.public_key = "invalid"
  1500. colliding_relay_usage.save()
  1501. assert Option.objects.count() == 1
  1502. assert Relay.objects.count() == 1
  1503. assert RelayUsage.objects.count() == 1
  1504. with assume_test_silo_mode(SiloMode.CONTROL):
  1505. colliding_control_option.value = "z"
  1506. colliding_control_option.save()
  1507. colliding_user_role.permissions = ["other.admin"]
  1508. colliding_user_role.save()
  1509. assert ControlOption.objects.count() == 1
  1510. assert UserRole.objects.count() == 1
  1511. with open(tmp_path, "rb") as tmp_file:
  1512. import_in_config_scope(
  1513. tmp_file, flags=ImportFlags(overwrite_configs=False), printer=NOOP_PRINTER
  1514. )
  1515. option_chunk = RegionImportChunk.objects.get(
  1516. model="sentry.option", min_ordinal=1, max_ordinal=1
  1517. )
  1518. assert len(option_chunk.inserted_map) == 0
  1519. assert len(option_chunk.existing_map) == 1
  1520. assert len(option_chunk.overwrite_map) == 0
  1521. assert Option.objects.count() == 1
  1522. assert Option.objects.filter(value__exact="y").exists()
  1523. relay_chunk = RegionImportChunk.objects.get(
  1524. model="sentry.relay", min_ordinal=1, max_ordinal=1
  1525. )
  1526. assert len(relay_chunk.inserted_map) == 0
  1527. assert len(relay_chunk.existing_map) == 1
  1528. assert len(relay_chunk.overwrite_map) == 0
  1529. assert Relay.objects.count() == 1
  1530. assert Relay.objects.filter(public_key__exact="invalid").exists()
  1531. relay_usage_chunk = RegionImportChunk.objects.get(
  1532. model="sentry.relayusage", min_ordinal=1, max_ordinal=1
  1533. )
  1534. assert len(relay_usage_chunk.inserted_map) == 0
  1535. assert len(relay_usage_chunk.existing_map) == 1
  1536. assert len(relay_usage_chunk.overwrite_map) == 0
  1537. assert RelayUsage.objects.count() == 1
  1538. assert RelayUsage.objects.filter(public_key__exact="invalid").exists()
  1539. with assume_test_silo_mode(SiloMode.CONTROL):
  1540. control_option_chunk = ControlImportChunk.objects.get(
  1541. model="sentry.controloption", min_ordinal=1, max_ordinal=1
  1542. )
  1543. assert len(control_option_chunk.inserted_map) == 0
  1544. assert len(control_option_chunk.existing_map) == 1
  1545. assert len(control_option_chunk.overwrite_map) == 0
  1546. assert ControlOption.objects.count() == 1
  1547. assert ControlOption.objects.filter(value__exact="z").exists()
  1548. assert UserRole.objects.count() == 1
  1549. actual_user_role = UserRole.objects.first()
  1550. assert len(actual_user_role.permissions) == 1
  1551. assert actual_user_role.permissions[0] == "other.admin"
  1552. with open(tmp_path, "rb") as tmp_file:
  1553. verify_models_in_output(expected_models, json.load(tmp_file))
  1554. @expect_models(COLLISION_TESTED, Email, User, UserEmail, UserIP)
  1555. def test_colliding_user_with_merging_enabled_in_user_scope(
  1556. self, expected_models: list[type[Model]]
  1557. ):
  1558. self.create_exhaustive_user(username="owner", email="importing@example.com")
  1559. with tempfile.TemporaryDirectory() as tmp_dir:
  1560. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  1561. with open(tmp_path, "rb") as tmp_file:
  1562. self.create_exhaustive_user(username="owner", email="existing@example.com")
  1563. import_in_user_scope(
  1564. tmp_file,
  1565. flags=ImportFlags(merge_users=True),
  1566. printer=NOOP_PRINTER,
  1567. )
  1568. with assume_test_silo_mode(SiloMode.CONTROL):
  1569. assert User.objects.count() == 1
  1570. assert UserEmail.objects.count() == 1 # Keep only original when merging.
  1571. assert UserIP.objects.count() == 1 # Keep only original when merging.
  1572. assert Authenticator.objects.count() == 1
  1573. assert Email.objects.count() == 2
  1574. user_chunk = ControlImportChunk.objects.get(
  1575. model="sentry.user", min_ordinal=1, max_ordinal=1
  1576. )
  1577. assert len(user_chunk.inserted_map) == 0
  1578. assert len(user_chunk.existing_map) == 1
  1579. assert User.objects.filter(username__iexact="owner").exists()
  1580. assert not User.objects.filter(username__iexact="owner-").exists()
  1581. assert User.objects.filter(is_unclaimed=True).count() == 0
  1582. assert LostPasswordHash.objects.count() == 0
  1583. assert User.objects.filter(is_unclaimed=False).count() == 1
  1584. assert UserEmail.objects.filter(email__icontains="existing@").exists()
  1585. assert not UserEmail.objects.filter(email__icontains="importing@").exists()
  1586. # Incoming `UserEmail`s, `UserPermissions`, and `UserIP`s for imported users are
  1587. # completely scrubbed when merging is enabled.
  1588. assert not ControlImportChunk.objects.filter(model="sentry.useremail").exists()
  1589. assert not ControlImportChunk.objects.filter(model="sentry.userip").exists()
  1590. assert not ControlImportChunk.objects.filter(model="sentry.userpermission").exists()
  1591. with open(tmp_path, "rb") as tmp_file:
  1592. verify_models_in_output(expected_models, json.load(tmp_file))
  1593. @expect_models(COLLISION_TESTED, Email, User, UserEmail, UserIP)
  1594. def test_colliding_user_with_merging_disabled_in_user_scope(
  1595. self, expected_models: list[type[Model]]
  1596. ):
  1597. self.create_exhaustive_user(username="owner", email="importing@example.com")
  1598. with tempfile.TemporaryDirectory() as tmp_dir:
  1599. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  1600. with open(tmp_path, "rb") as tmp_file:
  1601. self.create_exhaustive_user(username="owner", email="existing@example.com")
  1602. import_in_user_scope(
  1603. tmp_file,
  1604. flags=ImportFlags(merge_users=False),
  1605. printer=NOOP_PRINTER,
  1606. )
  1607. with assume_test_silo_mode(SiloMode.CONTROL):
  1608. assert User.objects.count() == 2
  1609. assert UserIP.objects.count() == 2
  1610. assert UserEmail.objects.count() == 2
  1611. assert Authenticator.objects.count() == 1 # Only imported in global scope
  1612. assert Email.objects.count() == 2
  1613. user_chunk = ControlImportChunk.objects.get(
  1614. model="sentry.user", min_ordinal=1, max_ordinal=1
  1615. )
  1616. assert len(user_chunk.inserted_map) == 1
  1617. assert len(user_chunk.existing_map) == 0
  1618. assert User.objects.filter(username__iexact="owner").exists()
  1619. assert User.objects.filter(username__icontains="owner-").exists()
  1620. assert User.objects.filter(is_unclaimed=True).count() == 1
  1621. assert LostPasswordHash.objects.count() == 1
  1622. assert User.objects.filter(is_unclaimed=False).count() == 1
  1623. useremail_chunk = ControlImportChunk.objects.get(
  1624. model="sentry.useremail", min_ordinal=1, max_ordinal=1
  1625. )
  1626. assert len(useremail_chunk.inserted_map) == 1
  1627. assert len(useremail_chunk.existing_map) == 0
  1628. assert UserEmail.objects.filter(email__icontains="existing@").exists()
  1629. assert UserEmail.objects.filter(email__icontains="importing@").exists()
  1630. with open(tmp_path, "rb") as tmp_file:
  1631. verify_models_in_output(expected_models, json.load(tmp_file))
  1632. @expect_models(
  1633. COLLISION_TESTED, Email, Organization, OrganizationMember, User, UserEmail, UserIP
  1634. )
  1635. def test_colliding_user_with_merging_enabled_in_organization_scope(
  1636. self, expected_models: list[type[Model]]
  1637. ):
  1638. owner = self.create_exhaustive_user(username="owner", email="importing@example.com")
  1639. self.create_organization("some-org", owner=owner)
  1640. with tempfile.TemporaryDirectory() as tmp_dir:
  1641. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  1642. with open(tmp_path, "rb") as tmp_file:
  1643. owner = self.create_exhaustive_user(username="owner", email="existing@example.com")
  1644. self.create_organization("some-org", owner=owner)
  1645. import_in_organization_scope(
  1646. tmp_file,
  1647. flags=ImportFlags(merge_users=True),
  1648. printer=NOOP_PRINTER,
  1649. )
  1650. with assume_test_silo_mode(SiloMode.CONTROL):
  1651. user = User.objects.get(username="owner")
  1652. assert User.objects.count() == 1
  1653. assert UserEmail.objects.count() == 1 # Keep only original when merging.
  1654. assert UserIP.objects.count() == 1 # Keep only original when merging.
  1655. assert Authenticator.objects.count() == 1 # Only imported in global scope
  1656. assert Email.objects.count() == 2
  1657. user_chunk = ControlImportChunk.objects.get(
  1658. model="sentry.user", min_ordinal=1, max_ordinal=1
  1659. )
  1660. assert len(user_chunk.inserted_map) == 0
  1661. assert len(user_chunk.existing_map) == 1
  1662. assert User.objects.filter(username__iexact="owner").exists()
  1663. assert not User.objects.filter(username__icontains="owner-").exists()
  1664. assert User.objects.filter(is_unclaimed=True).count() == 0
  1665. assert LostPasswordHash.objects.count() == 0
  1666. assert User.objects.filter(is_unclaimed=False).count() == 1
  1667. assert UserEmail.objects.filter(email__icontains="existing@").exists()
  1668. assert not UserEmail.objects.filter(email__icontains="importing@").exists()
  1669. # Incoming `UserEmail`s, `UserPermissions`, and `UserIP`s for imported users are
  1670. # completely dropped when merging is enabled.
  1671. assert not ControlImportChunk.objects.filter(model="sentry.useremail").exists()
  1672. assert not ControlImportChunk.objects.filter(model="sentry.userip").exists()
  1673. assert not ControlImportChunk.objects.filter(model="sentry.userpermission").exists()
  1674. assert Organization.objects.count() == 2
  1675. assert OrganizationMember.objects.count() == 2 # Same user in both orgs
  1676. existing = Organization.objects.get(slug="some-org")
  1677. imported = Organization.objects.filter(slug__icontains="some-org-").first()
  1678. assert (
  1679. OrganizationMember.objects.filter(user_id=user.id, organization=existing).count()
  1680. == 1
  1681. )
  1682. assert (
  1683. OrganizationMember.objects.filter(user_id=user.id, organization=imported).count()
  1684. == 1
  1685. )
  1686. with assume_test_silo_mode(SiloMode.CONTROL):
  1687. assert OrganizationMapping.objects.count() == 2
  1688. assert OrganizationMemberMapping.objects.count() == 2 # Same user in both orgs
  1689. assert (
  1690. OrganizationMemberMapping.objects.filter(
  1691. user=user, organization_id=existing.id
  1692. ).count()
  1693. == 1
  1694. )
  1695. with open(tmp_path, "rb") as tmp_file:
  1696. verify_models_in_output(expected_models, json.load(tmp_file))
  1697. @expect_models(
  1698. COLLISION_TESTED, Email, Organization, OrganizationMember, User, UserEmail, UserIP
  1699. )
  1700. def test_colliding_user_with_merging_disabled_in_organization_scope(
  1701. self, expected_models: list[type[Model]]
  1702. ):
  1703. owner = self.create_exhaustive_user(username="owner", email="importing@example.com")
  1704. self.create_organization("some-org", owner=owner)
  1705. with tempfile.TemporaryDirectory() as tmp_dir:
  1706. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  1707. with open(tmp_path, "rb") as tmp_file:
  1708. owner = self.create_exhaustive_user(username="owner", email="existing@example.com")
  1709. self.create_organization("some-org", owner=owner)
  1710. import_in_organization_scope(
  1711. tmp_file,
  1712. flags=ImportFlags(merge_users=False),
  1713. printer=NOOP_PRINTER,
  1714. )
  1715. with assume_test_silo_mode(SiloMode.CONTROL):
  1716. existing_user = User.objects.get(username="owner")
  1717. imported_user = User.objects.get(username__icontains="owner-")
  1718. assert User.objects.count() == 2
  1719. assert UserIP.objects.count() == 2
  1720. assert UserEmail.objects.count() == 2
  1721. assert Authenticator.objects.count() == 1 # Only imported in global scope
  1722. assert Email.objects.count() == 2
  1723. user_chunk = ControlImportChunk.objects.get(
  1724. model="sentry.user", min_ordinal=1, max_ordinal=1
  1725. )
  1726. assert len(user_chunk.inserted_map) == 1
  1727. assert len(user_chunk.existing_map) == 0
  1728. assert User.objects.filter(username__iexact="owner").exists()
  1729. assert User.objects.filter(username__icontains="owner-").exists()
  1730. assert User.objects.filter(is_unclaimed=True).count() == 1
  1731. assert LostPasswordHash.objects.count() == 1
  1732. assert User.objects.filter(is_unclaimed=False).count() == 1
  1733. useremail_chunk = ControlImportChunk.objects.get(
  1734. model="sentry.useremail", min_ordinal=1, max_ordinal=1
  1735. )
  1736. assert len(useremail_chunk.inserted_map) == 1
  1737. assert len(useremail_chunk.existing_map) == 0
  1738. assert UserEmail.objects.filter(email__icontains="existing@").exists()
  1739. assert UserEmail.objects.filter(email__icontains="importing@").exists()
  1740. assert Organization.objects.count() == 2
  1741. assert OrganizationMember.objects.count() == 2
  1742. existing_org = Organization.objects.get(slug="some-org")
  1743. imported_org = Organization.objects.filter(slug__icontains="some-org-").first()
  1744. assert (
  1745. OrganizationMember.objects.filter(
  1746. user_id=existing_user.id, organization=existing_org
  1747. ).count()
  1748. == 1
  1749. )
  1750. assert (
  1751. OrganizationMember.objects.filter(
  1752. user_id=imported_user.id, organization=imported_org
  1753. ).count()
  1754. == 1
  1755. )
  1756. with assume_test_silo_mode(SiloMode.CONTROL):
  1757. assert OrganizationMapping.objects.count() == 2
  1758. assert OrganizationMemberMapping.objects.count() == 2
  1759. assert (
  1760. OrganizationMemberMapping.objects.filter(
  1761. user=existing_user, organization_id=existing_org.id
  1762. ).count()
  1763. == 1
  1764. )
  1765. with open(tmp_path, "rb") as tmp_file:
  1766. verify_models_in_output(expected_models, json.load(tmp_file))
  1767. @expect_models(COLLISION_TESTED, Email, User, UserEmail, UserIP, UserPermission)
  1768. def test_colliding_user_with_merging_enabled_in_config_scope(
  1769. self, expected_models: list[type[Model]]
  1770. ):
  1771. self.create_exhaustive_user(username="owner", email="importing@example.com", is_admin=True)
  1772. with tempfile.TemporaryDirectory() as tmp_dir:
  1773. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  1774. with open(tmp_path, "rb") as tmp_file:
  1775. self.create_exhaustive_user(
  1776. username="owner", email="existing@example.com", is_admin=True
  1777. )
  1778. import_in_config_scope(
  1779. tmp_file,
  1780. flags=ImportFlags(merge_users=True),
  1781. printer=NOOP_PRINTER,
  1782. )
  1783. with assume_test_silo_mode(SiloMode.CONTROL):
  1784. assert User.objects.count() == 1
  1785. assert UserEmail.objects.count() == 1 # Keep only original when merging.
  1786. assert UserIP.objects.count() == 1 # Keep only original when merging.
  1787. assert UserPermission.objects.count() == 1 # Keep only original when merging.
  1788. assert Authenticator.objects.count() == 1
  1789. assert Email.objects.count() == 2
  1790. user_chunk = ControlImportChunk.objects.get(
  1791. model="sentry.user", min_ordinal=1, max_ordinal=1
  1792. )
  1793. assert len(user_chunk.inserted_map) == 0
  1794. assert len(user_chunk.existing_map) == 1
  1795. assert User.objects.filter(username__iexact="owner").exists()
  1796. assert not User.objects.filter(username__iexact="owner-").exists()
  1797. assert User.objects.filter(is_unclaimed=True).count() == 0
  1798. assert LostPasswordHash.objects.count() == 0
  1799. assert User.objects.filter(is_unclaimed=False).count() == 1
  1800. assert UserEmail.objects.filter(email__icontains="existing@").exists()
  1801. assert not UserEmail.objects.filter(email__icontains="importing@").exists()
  1802. # Incoming `UserEmail`s, `UserPermissions`, and `UserIP`s for imported users are
  1803. # completely dropped when merging is enabled.
  1804. assert not ControlImportChunk.objects.filter(model="sentry.useremail").exists()
  1805. assert not ControlImportChunk.objects.filter(model="sentry.userip").exists()
  1806. assert not ControlImportChunk.objects.filter(model="sentry.userpermission").exists()
  1807. with open(tmp_path, "rb") as tmp_file:
  1808. verify_models_in_output(expected_models, json.load(tmp_file))
  1809. @expect_models(COLLISION_TESTED, Email, User, UserEmail, UserIP, UserPermission)
  1810. def test_colliding_user_with_merging_disabled_in_config_scope(
  1811. self, expected_models: list[type[Model]]
  1812. ):
  1813. self.create_exhaustive_user(username="owner", email="importing@example.com", is_admin=True)
  1814. with tempfile.TemporaryDirectory() as tmp_dir:
  1815. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  1816. with open(tmp_path, "rb") as tmp_file:
  1817. self.create_exhaustive_user(
  1818. username="owner", email="existing@example.com", is_admin=True
  1819. )
  1820. import_in_config_scope(
  1821. tmp_file,
  1822. flags=ImportFlags(merge_users=False),
  1823. printer=NOOP_PRINTER,
  1824. )
  1825. with assume_test_silo_mode(SiloMode.CONTROL):
  1826. assert User.objects.count() == 2
  1827. assert UserIP.objects.count() == 2
  1828. assert UserEmail.objects.count() == 2
  1829. assert UserPermission.objects.count() == 2
  1830. assert Authenticator.objects.count() == 1 # Only imported in global scope
  1831. assert Email.objects.count() == 2
  1832. user_chunk = ControlImportChunk.objects.get(
  1833. model="sentry.user", min_ordinal=1, max_ordinal=1
  1834. )
  1835. assert len(user_chunk.inserted_map) == 1
  1836. assert len(user_chunk.existing_map) == 0
  1837. assert User.objects.filter(username__iexact="owner").exists()
  1838. assert User.objects.filter(username__icontains="owner-").exists()
  1839. assert User.objects.filter(is_unclaimed=True).count() == 1
  1840. assert LostPasswordHash.objects.count() == 1
  1841. assert User.objects.filter(is_unclaimed=False).count() == 1
  1842. useremail_chunk = ControlImportChunk.objects.get(
  1843. model="sentry.useremail", min_ordinal=1, max_ordinal=1
  1844. )
  1845. assert len(useremail_chunk.inserted_map) == 1
  1846. assert len(useremail_chunk.existing_map) == 0
  1847. assert UserEmail.objects.filter(email__icontains="existing@").exists()
  1848. assert UserEmail.objects.filter(email__icontains="importing@").exists()
  1849. with open(tmp_path, "rb") as tmp_file:
  1850. verify_models_in_output(expected_models, json.load(tmp_file))
  1851. CUSTOM_IMPORT_BEHAVIOR_TESTED: set[NormalizedModelName] = set()
  1852. # There is no need to in both monolith and region mode for model-level unit tests - region mode
  1853. # testing along should suffice.
  1854. @region_silo_test
  1855. class CustomImportBehaviorTests(ImportTestCase):
  1856. """
  1857. Test bespoke, per-model behavior. Since these tests are relatively expensive to set up and tear
  1858. down (think on the order of 5-10 seconds per test case), we encourage combining model test cases
  1859. as much as reasonably possible.
  1860. """
  1861. # TODO(hybrid-cloud): actor refactor. Remove this test case when done.
  1862. @expect_models(CUSTOM_IMPORT_BEHAVIOR_TESTED, Actor, AlertRule)
  1863. def test_alert_rule_with_owner_id(self, expected_models: list[type[Model]]):
  1864. user = self.create_user()
  1865. org = self.create_organization(name="test-org", owner=user)
  1866. team = self.create_team(name="test-team", organization=org)
  1867. def create_fake_snuba_query() -> SnubaQuery:
  1868. return create_snuba_query(
  1869. query_type=SnubaQuery.Type.ERROR,
  1870. dataset=Dataset.Events,
  1871. query="level:error",
  1872. aggregate="count()",
  1873. time_window=timedelta(minutes=10),
  1874. resolution=timedelta(minutes=1),
  1875. environment=None,
  1876. event_types=[SnubaQueryEventType.EventType.ERROR],
  1877. )
  1878. # Create four `AlertRule`. Both of them fell through the `owner_id` migration, and therefore
  1879. # DO have an `owner_id`, but have NEITHER a `team_id` nor `user_id`.
  1880. #
  1881. # For the first two `AlertRule` rules, we'll include an `Actor` model with the correct data,
  1882. # meaning we just have to do a DB lookup, but the model import should go ahead as if the
  1883. # migration had been successful. For the third `AlertRule`, the `Actor` will also have both
  1884. # `team` and `user` set to null. Finally, the last instance will have no `Actor` at all.
  1885. #
  1886. # The expected result is that the first two instances succeed, while the last two are
  1887. # ignored.]
  1888. common_alert_rule_args = {
  1889. "organization": org,
  1890. "threshold_type": AlertRuleThresholdType.ABOVE.value,
  1891. "resolve_threshold": 10,
  1892. "threshold_period": 1,
  1893. "include_all_projects": False,
  1894. "comparison_delta": None,
  1895. }
  1896. # Use `bulk_create` to avoid the `.save()` checks that catch some otherwise invalid data -
  1897. # the whole point of this test is to ensure we gracefully recover when importing such data!
  1898. AlertRule.objects.bulk_create(
  1899. [
  1900. AlertRule(
  1901. name="user-alert-rule",
  1902. owner=Actor.objects.create(user_id=user.id, type=ACTOR_TYPES["user"]),
  1903. snuba_query=create_fake_snuba_query(),
  1904. **common_alert_rule_args,
  1905. ),
  1906. AlertRule(
  1907. name="team-alert-rule",
  1908. owner=Actor.objects.get(team=team, type=ACTOR_TYPES["team"]),
  1909. snuba_query=create_fake_snuba_query(),
  1910. **common_alert_rule_args,
  1911. ),
  1912. AlertRule(
  1913. name="null-alert-rule",
  1914. owner=Actor.objects.create(team=None, user_id=None, type=ACTOR_TYPES["team"]),
  1915. snuba_query=create_fake_snuba_query(),
  1916. **common_alert_rule_args,
  1917. ),
  1918. AlertRule(
  1919. name="unowned-alert-rule",
  1920. owner=None,
  1921. snuba_query=create_fake_snuba_query(),
  1922. **common_alert_rule_args,
  1923. ),
  1924. ]
  1925. )
  1926. with tempfile.TemporaryDirectory() as tmp_dir:
  1927. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  1928. with open(tmp_path, "rb") as tmp_file:
  1929. import_in_organization_scope(
  1930. tmp_file,
  1931. printer=NOOP_PRINTER,
  1932. )
  1933. user_alert_rule: AlertRule = AlertRule.objects.get(name="user-alert-rule")
  1934. user_actor: Actor = Actor.objects.get(id=user_alert_rule.owner_id)
  1935. assert user_alert_rule.owner is not None
  1936. assert user_alert_rule.user_id == user_actor.user_id
  1937. assert user_alert_rule.team is None
  1938. assert user_actor.team is None
  1939. team_alert_rule: AlertRule = AlertRule.objects.get(name="team-alert-rule")
  1940. team_actor: Actor = Actor.objects.get(id=team_alert_rule.owner_id)
  1941. assert team_alert_rule.owner is not None
  1942. assert team_alert_rule.team == team_actor.team
  1943. assert team_alert_rule.user_id is None
  1944. assert team_actor.user_id is None
  1945. null_alert_rule: AlertRule = AlertRule.objects.get(name="null-alert-rule")
  1946. unowned_alert_rule: AlertRule = AlertRule.objects.get(name="unowned-alert-rule")
  1947. assert null_alert_rule.owner is None
  1948. assert unowned_alert_rule.owner is None
  1949. with open(tmp_path, "rb") as tmp_file:
  1950. verify_models_in_output(expected_models, json.load(tmp_file))
  1951. @expect_models(CUSTOM_IMPORT_BEHAVIOR_TESTED, OrganizationMember)
  1952. def test_organization_member_inviter_id(self, expected_models: list[type[Model]]):
  1953. admin = self.create_exhaustive_user("admin", email="admin@test.com", is_superuser=True)
  1954. owner = self.create_exhaustive_user("owner", email="owner@test.com")
  1955. member = self.create_exhaustive_user("member", email="member@test.com")
  1956. org = self.create_exhaustive_organization(
  1957. slug="test-org",
  1958. owner=owner,
  1959. member=member,
  1960. invites={
  1961. admin: "invited-by-admin@test.com",
  1962. owner: "invited-by-owner@test.com",
  1963. },
  1964. )
  1965. # Give each member an inviter that is not in the organization itself (the "admin"), meaning
  1966. # they will not be imported if we only filter down to `test-org`. The desired outcome is
  1967. # that the inviter is nulled out.
  1968. for org_member in OrganizationMember.objects.filter(organization=org):
  1969. if not org_member.inviter_id:
  1970. org_member.inviter_id = admin.id
  1971. org_member.save()
  1972. assert (
  1973. OrganizationMember.objects.filter(organization=org.id, inviter_id__isnull=False).count()
  1974. == 4
  1975. )
  1976. with tempfile.TemporaryDirectory() as tmp_dir:
  1977. tmp_path = self.export_to_tmp_file_and_clear_database(tmp_dir)
  1978. with open(tmp_path, "rb") as tmp_file:
  1979. import_in_organization_scope(
  1980. tmp_file,
  1981. org_filter={"test-org"},
  1982. printer=NOOP_PRINTER,
  1983. )
  1984. # `owner` and `member` should both have had their `inviter_id` scrubbed.
  1985. org_id = Organization.objects.get(slug="test-org").id
  1986. assert OrganizationMember.objects.filter(
  1987. organization=org_id,
  1988. user_email="owner@test.com",
  1989. email__isnull=True,
  1990. inviter_id__isnull=True,
  1991. ).exists()
  1992. assert OrganizationMember.objects.filter(
  1993. organization=org_id,
  1994. user_email="member@test.com",
  1995. email__isnull=True,
  1996. inviter_id__isnull=True,
  1997. ).exists()
  1998. # The invitee invited by the not-imported `admin` should lose their `inviter_id`, but
  1999. # the one invited by `owner` should keep it.
  2000. assert OrganizationMember.objects.filter(
  2001. organization=org_id,
  2002. email="invited-by-admin@test.com",
  2003. inviter_id__isnull=True,
  2004. ).exists()
  2005. assert OrganizationMember.objects.filter(
  2006. organization=org_id,
  2007. email="invited-by-owner@test.com",
  2008. inviter_id__isnull=False,
  2009. ).exists()
  2010. with open(tmp_path, "rb") as tmp_file:
  2011. verify_models_in_output(expected_models, json.load(tmp_file))
  2012. @pytest.mark.skipif(reason="not legacy")
  2013. class TestLegacyTestSuite:
  2014. def test_deleteme(self):
  2015. """
  2016. The monolith-dbs test suite should only exist until relocation code
  2017. handles monolith- and hybrid-database modes with the same code path,
  2018. which is planned work.
  2019. """
  2020. assert date.today() <= date(
  2021. 2023, 11, 11
  2022. ), "Please delete the monolith-dbs test suite!" # or else bump the date