importer.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import aiohttp
  2. import tablib
  3. from asgiref.sync import sync_to_async
  4. from django.db.models import Q
  5. from django.urls import reverse
  6. from apps.organizations_ext.admin import OrganizationResource, OrganizationUserResource
  7. from apps.organizations_ext.models import OrganizationUser, OrganizationUserRole
  8. from apps.projects.admin import ProjectKeyResource, ProjectResource
  9. from apps.projects.models import Project
  10. from apps.shared.types import TypeJson
  11. from apps.teams.admin import TeamResource
  12. from apps.users.admin import UserResource
  13. from apps.users.models import User
  14. from .exceptions import ImporterException
  15. class GlitchTipImporter:
  16. """
  17. Generic importer tool to use with cli or web
  18. If used by a non server admin, it's important to assume all incoming
  19. JSON is hostile and not from a real GT server. Foreign Key ids could be
  20. faked and used to elevate privileges. Always confirm new data is associated with
  21. appropriate organization. Also assume user is at least an org admin, no need to
  22. double check permissions when creating assets within the organization.
  23. create_users should be False unless running as superuser/management command
  24. """
  25. def __init__(
  26. self, url: str, auth_token: str, organization_slug: str, create_users=False
  27. ):
  28. self.url = url.rstrip("/")
  29. self.headers = {"Authorization": f"Bearer {auth_token}"}
  30. self.create_users = create_users
  31. self.organization_slug = organization_slug
  32. self.organization_id = None
  33. self.organization_url = reverse(
  34. "organization-detail", kwargs={"slug": self.organization_slug}
  35. )
  36. self.organization_users_url = reverse(
  37. "organization-users-list",
  38. kwargs={"organization_slug": self.organization_slug},
  39. )
  40. self.projects_url = reverse(
  41. "organization-projects-list",
  42. kwargs={"organization_slug": self.organization_slug},
  43. )
  44. self.teams_url = reverse(
  45. "organization-teams-list",
  46. kwargs={"organization_slug": self.organization_slug},
  47. )
  48. async def run(self, organization_id=None):
  49. """Set organization_id to None to import (superuser only)"""
  50. if organization_id is None:
  51. await self.import_organization()
  52. else:
  53. self.organization_id = organization_id
  54. await self.import_organization_users()
  55. await self.import_projects()
  56. await self.import_teams()
  57. async def get(self, url: str) -> TypeJson:
  58. async with aiohttp.ClientSession() as session:
  59. async with session.get(url, headers=self.headers) as res:
  60. return await res.json()
  61. async def import_organization(self):
  62. resource = OrganizationResource()
  63. data = await self.get(self.url + self.organization_url)
  64. self.organization_id = data["id"] # TODO unsafe for web usage
  65. dataset = tablib.Dataset()
  66. dataset.dict = [data]
  67. await sync_to_async(resource.import_data)(dataset, raise_errors=True)
  68. async def import_organization_users(self):
  69. resource = OrganizationUserResource()
  70. org_users = await self.get(self.url + self.organization_users_url)
  71. if not org_users:
  72. return
  73. if self.create_users:
  74. user_resource = UserResource()
  75. users_list = [
  76. org_user["user"] for org_user in org_users if org_user is not None
  77. ]
  78. users = [
  79. {k: v for k, v in user.items() if k in ["id", "email", "name"]}
  80. for user in users_list
  81. ]
  82. dataset = tablib.Dataset()
  83. dataset.dict = users
  84. await sync_to_async(user_resource.import_data)(dataset, raise_errors=True)
  85. for org_user in org_users:
  86. org_user["organization"] = self.organization_id
  87. org_user["role"] = OrganizationUserRole.from_string(org_user["role"])
  88. if self.create_users:
  89. org_user["user"] = (
  90. User.objects.filter(email=org_user["user"]["email"])
  91. .values_list("pk", flat=True)
  92. .first()
  93. )
  94. else:
  95. org_user["user"] = None
  96. dataset = tablib.Dataset()
  97. dataset.dict = org_users
  98. await sync_to_async(resource.import_data)(dataset, raise_errors=True)
  99. async def import_projects(self):
  100. project_resource = ProjectResource()
  101. project_key_resource = ProjectKeyResource()
  102. projects = await self.get(self.url + self.projects_url)
  103. project_keys = []
  104. for project in projects:
  105. project["organization"] = self.organization_id
  106. keys = await self.get(
  107. self.url
  108. + reverse(
  109. "project-keys-list",
  110. kwargs={
  111. "project_pk": f"{self.organization_slug}/{project['slug']}",
  112. },
  113. )
  114. )
  115. for key in keys:
  116. key["project"] = project["id"]
  117. key["public_key"] = key["public"]
  118. project_keys += keys
  119. dataset = tablib.Dataset()
  120. dataset.dict = projects
  121. await sync_to_async(project_resource.import_data)(dataset, raise_errors=True)
  122. owned_project_ids = [
  123. pk
  124. async for pk in Project.objects.filter(
  125. organization_id=self.organization_id,
  126. pk__in=[d["projectId"] for d in project_keys],
  127. ).values_list("pk", flat=True)
  128. ]
  129. project_keys = list(
  130. filter(lambda key: key["projectId"] in owned_project_ids, project_keys)
  131. )
  132. dataset.dict = project_keys
  133. await sync_to_async(project_key_resource.import_data)(
  134. dataset, raise_errors=True
  135. )
  136. async def import_teams(self):
  137. resource = TeamResource()
  138. teams = await self.get(self.url + self.teams_url)
  139. for team in teams:
  140. team["organization"] = self.organization_id
  141. team["projects"] = ",".join(
  142. map(
  143. str,
  144. [
  145. pk
  146. async for pk in Project.objects.filter(
  147. organization_id=self.organization_id,
  148. pk__in=[int(d["id"]) for d in team["projects"]],
  149. ).values_list("id", flat=True)
  150. ],
  151. )
  152. )
  153. team_members = await self.get(
  154. self.url
  155. + reverse(
  156. "team-members-list",
  157. kwargs={"team_pk": f"{self.organization_slug}/{team['slug']}"},
  158. )
  159. )
  160. team_member_emails = [d["email"] for d in team_members]
  161. team["members"] = ",".join(
  162. [
  163. str(i)
  164. async for i in OrganizationUser.objects.filter(
  165. organization_id=self.organization_id
  166. )
  167. .filter(
  168. Q(email__in=team_member_emails)
  169. | Q(user__email__in=team_member_emails)
  170. )
  171. .values_list("pk", flat=True)
  172. ]
  173. )
  174. dataset = tablib.Dataset()
  175. dataset.dict = teams
  176. await sync_to_async(resource.import_data)(dataset, raise_errors=True)
  177. async def check_auth(self):
  178. async with aiohttp.ClientSession() as session:
  179. async with session.get(self.url + "/api/0/", headers=self.headers) as res:
  180. data = await res.json()
  181. if res.status != 200 or not data["user"]:
  182. raise ImporterException("Bad auth token")