api.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. from uuid import UUID
  2. from django.http import Http404, HttpResponse
  3. from django.shortcuts import aget_object_or_404
  4. from ninja import Router
  5. from ninja.errors import ValidationError
  6. from ninja.pagination import paginate
  7. from apps.organizations_ext.models import Organization, OrganizationUserRole
  8. from apps.shared.types import MeID
  9. from apps.teams.models import Team
  10. from apps.teams.schema import ProjectTeamSchema
  11. from glitchtip.api.permissions import AuthHttpRequest, has_permission
  12. from .models import Project, ProjectKey, UserProjectAlert
  13. from .schema import (
  14. ProjectIn,
  15. ProjectKeyIn,
  16. ProjectKeySchema,
  17. ProjectOrganizationSchema,
  18. ProjectSchema,
  19. StrKeyIntValue,
  20. )
  21. router = Router()
  22. """
  23. GET /api/0/projects/
  24. GET /api/0/projects/{organization_slug}/{project_slug}/
  25. DELETE /api/0/projects/{organization_slug}/{project_slug}/
  26. POST /api/0/projects/{organization_slug}/{project_slug}/teams/{team_slug}/ (See teams)
  27. DELETE /api/0/projects/{organization_slug}/{project_slug}/teams/{team_slug}/ (See teams)
  28. GET /api/0/projects/{organization_slug}/{team_slug}/keys/
  29. POST /api/0/projects/{organization_slug}/{team_slug}/keys/
  30. GET /api/0/projects/{organization_slug}/{project_slug}/keys/{key_id}/
  31. DELETE /api/0/projects/{organization_slug}/{project_slug}/keys/{key_id}/
  32. GET /api/0/teams/{organization_slug}/{team_slug}/projects/
  33. POST /api/0/teams/{organization_slug}/{team_slug}/projects/
  34. GET /api/0/organizations/{organization_slug}/projects/
  35. """
  36. def get_projects_queryset(
  37. user_id: int, organization_slug: str = None, team_slug: str = None
  38. ):
  39. qs = Project.annotate_is_member(
  40. Project.undeleted_objects.filter(organization__users=user_id), user_id
  41. )
  42. if organization_slug:
  43. qs = qs.filter(organization__slug=organization_slug)
  44. if team_slug:
  45. qs = qs.filter(teams__slug=team_slug)
  46. return qs
  47. def get_project_keys_queryset(
  48. user_id: int, organization_slug: str, project_slug: str, key_id: UUID | None = None
  49. ):
  50. qs = ProjectKey.objects.filter(
  51. project__organization__users=user_id,
  52. project__organization__slug=organization_slug,
  53. project__slug=project_slug,
  54. )
  55. if key_id:
  56. qs = qs.filter(public_key=key_id)
  57. return qs
  58. @router.get(
  59. "projects/",
  60. response=list[ProjectOrganizationSchema],
  61. by_alias=True,
  62. )
  63. @paginate
  64. @has_permission(["project:read"])
  65. async def list_projects(request: AuthHttpRequest, response: HttpResponse):
  66. """List all projects that a user has access to"""
  67. return (
  68. get_projects_queryset(request.auth.user_id)
  69. .select_related("organization")
  70. .order_by("name")
  71. )
  72. @router.get(
  73. "projects/{slug:organization_slug}/{slug:project_slug}/",
  74. response=ProjectOrganizationSchema,
  75. by_alias=True,
  76. )
  77. @has_permission(["project:read", "project:write", "project:admin"])
  78. async def get_project(
  79. request: AuthHttpRequest, organization_slug: str, project_slug: str
  80. ):
  81. return await aget_object_or_404(
  82. get_projects_queryset(request.auth.user_id, organization_slug).select_related(
  83. "organization"
  84. ),
  85. slug=project_slug,
  86. )
  87. @router.put(
  88. "projects/{slug:organization_slug}/{slug:project_slug}/",
  89. response=ProjectOrganizationSchema,
  90. by_alias=True,
  91. )
  92. @has_permission(["project:write", "project:admin"])
  93. async def update_project(
  94. request: AuthHttpRequest,
  95. organization_slug: str,
  96. project_slug: str,
  97. payload: ProjectIn,
  98. ):
  99. project = await aget_object_or_404(
  100. get_projects_queryset(request.auth.user_id, organization_slug).select_related(
  101. "organization"
  102. ),
  103. slug=project_slug,
  104. )
  105. for attr, value in payload.dict().items():
  106. setattr(project, attr, value)
  107. await project.asave()
  108. return project
  109. @router.delete(
  110. "projects/{slug:organization_slug}/{slug:project_slug}/",
  111. response={204: None},
  112. )
  113. @has_permission(["project:admin"])
  114. async def delete_project(
  115. request: AuthHttpRequest, organization_slug: str, project_slug: str
  116. ):
  117. result, _ = (
  118. await get_projects_queryset(request.auth.user_id, organization_slug)
  119. .filter(
  120. slug=project_slug,
  121. organization__organization_users__role__gte=OrganizationUserRole.ADMIN,
  122. )
  123. .adelete()
  124. )
  125. if not result:
  126. raise Http404
  127. return 204, None
  128. @router.get(
  129. "teams/{slug:organization_slug}/{slug:team_slug}/projects/",
  130. response=list[ProjectSchema],
  131. by_alias=True,
  132. )
  133. @paginate
  134. @has_permission(["project:read"])
  135. async def list_team_projects(
  136. request: AuthHttpRequest,
  137. response: HttpResponse,
  138. organization_slug: str,
  139. team_slug: str,
  140. ):
  141. """List all projects for a given team"""
  142. return get_projects_queryset(
  143. request.auth.user_id, organization_slug=organization_slug, team_slug=team_slug
  144. ).order_by("name")
  145. @router.post(
  146. "teams/{slug:organization_slug}/{slug:team_slug}/projects/",
  147. response={201: ProjectSchema},
  148. by_alias=True,
  149. )
  150. @has_permission(["project:write", "project:admin"])
  151. async def create_project(
  152. request: AuthHttpRequest, organization_slug: str, team_slug: str, payload: ProjectIn
  153. ):
  154. """Create a new project given a team and organization"""
  155. user_id = request.auth.user_id
  156. team = await aget_object_or_404(
  157. Team,
  158. slug=team_slug,
  159. organization__slug=organization_slug,
  160. organization__users=user_id,
  161. organization__organization_users__role__gte=OrganizationUserRole.ADMIN,
  162. )
  163. organization = await aget_object_or_404(
  164. Organization,
  165. slug=organization_slug,
  166. users=user_id,
  167. organization_users__role__gte=OrganizationUserRole.ADMIN,
  168. )
  169. project = await Project.objects.acreate(organization=organization, **payload.dict())
  170. await project.teams.aadd(team)
  171. project = await get_projects_queryset(user_id).aget(id=project.id)
  172. return 201, project
  173. @router.get(
  174. "organizations/{slug:organization_slug}/projects/",
  175. response=list[ProjectTeamSchema],
  176. by_alias=True,
  177. )
  178. @paginate
  179. @has_permission(["project:read"])
  180. async def list_organization_projects(
  181. request: AuthHttpRequest,
  182. response: HttpResponse,
  183. organization_slug: str,
  184. query: str | None = None,
  185. ):
  186. """
  187. Fetch list of organizations for a project
  188. Contains team information
  189. query: Filter on team, ex: ?query=!team:burke-software
  190. """
  191. queryset = (
  192. get_projects_queryset(request.auth.user_id, organization_slug=organization_slug)
  193. .prefetch_related("teams")
  194. .order_by("name")
  195. )
  196. # This query param isn't documented in sentry api but exists
  197. if query:
  198. query_parts = query.split()
  199. for query in query_parts:
  200. query_part = query.split(":", 1)
  201. if len(query_part) == 2:
  202. query_name, query_value = query_part
  203. if query_name == "team":
  204. queryset = queryset.filter(teams__slug=query_value)
  205. if query_name == "!team":
  206. queryset = queryset.exclude(teams__slug=query_value)
  207. return queryset
  208. @router.get(
  209. "projects/{slug:organization_slug}/{slug:project_slug}/keys/",
  210. response=list[ProjectKeySchema],
  211. by_alias=True,
  212. )
  213. @paginate
  214. @has_permission(["project:read", "project:write", "project:admin"])
  215. async def list_project_keys(
  216. request: AuthHttpRequest,
  217. response: HttpResponse,
  218. organization_slug: str,
  219. project_slug: str,
  220. status: str | None = None,
  221. ):
  222. """List all DSN keys for a given project"""
  223. return get_project_keys_queryset(
  224. request.auth.user_id, organization_slug, project_slug
  225. )
  226. @router.get(
  227. "projects/{slug:organization_slug}/{slug:project_slug}/keys/{uuid:key_id}/",
  228. response=ProjectKeySchema,
  229. by_alias=True,
  230. )
  231. @has_permission(["project:read", "project:write", "project:admin"])
  232. async def get_project_key(
  233. request: AuthHttpRequest,
  234. organization_slug: str,
  235. project_slug: str,
  236. key_id: UUID,
  237. ):
  238. return await aget_object_or_404(
  239. get_project_keys_queryset(
  240. request.auth.user_id, organization_slug, project_slug, key_id=key_id
  241. )
  242. )
  243. @router.put(
  244. "projects/{slug:organization_slug}/{slug:project_slug}/keys/{uuid:key_id}/",
  245. response=ProjectKeySchema,
  246. by_alias=True,
  247. )
  248. @has_permission(["project:write", "project:admin"])
  249. async def update_project_key(
  250. request: AuthHttpRequest,
  251. organization_slug: str,
  252. project_slug: str,
  253. key_id: UUID,
  254. payload: ProjectKeyIn,
  255. ):
  256. return await aget_object_or_404(
  257. get_project_keys_queryset(
  258. request.auth.user_id, organization_slug, project_slug, key_id=key_id
  259. )
  260. )
  261. @router.post(
  262. "projects/{slug:organization_slug}/{slug:project_slug}/keys/",
  263. response={201: ProjectKeySchema},
  264. by_alias=True,
  265. )
  266. @has_permission(["project:write", "project:admin"])
  267. async def create_project_key(
  268. request: AuthHttpRequest,
  269. organization_slug: str,
  270. project_slug: str,
  271. payload: ProjectKeyIn,
  272. ):
  273. """Create new key for project. Rate limiting not implemented."""
  274. project = await aget_object_or_404(
  275. get_projects_queryset(request.auth.user_id, organization_slug),
  276. slug=project_slug,
  277. )
  278. return 201, await ProjectKey.objects.acreate(
  279. project=project,
  280. name=payload.name,
  281. rate_limit_count=payload.rate_limit.count if payload.rate_limit else None,
  282. rate_limit_window=payload.rate_limit.window if payload.rate_limit else None,
  283. )
  284. @router.delete(
  285. "projects/{slug:organization_slug}/{slug:project_slug}/keys/{uuid:key_id}/",
  286. response={204: None},
  287. )
  288. @has_permission(["project:admin"])
  289. async def delete_project_key(
  290. request: AuthHttpRequest, organization_slug: str, project_slug: str, key_id: UUID
  291. ):
  292. result, _ = (
  293. await get_project_keys_queryset(
  294. request.auth.user_id, organization_slug, project_slug, key_id=key_id
  295. )
  296. .filter(
  297. project__organization__organization_users__role__gte=OrganizationUserRole.ADMIN
  298. )
  299. .adelete()
  300. )
  301. if not result:
  302. raise Http404
  303. return 204, None
  304. @router.get("/users/{slug:user_id}/notifications/alerts/", response=StrKeyIntValue)
  305. async def user_notification_alerts(request: AuthHttpRequest, user_id: MeID):
  306. """
  307. Returns dictionary of project_id: status. Now project_id status means it's "default"
  308. To update, submit `{project_id: status}` where status is -1 (default), 0, or 1
  309. """
  310. if user_id != request.auth.user_id and user_id != "me":
  311. raise Http404
  312. user_id = request.auth.user_id
  313. data = {}
  314. async for alert in UserProjectAlert.objects.filter(user_id=user_id):
  315. data[str(alert.project_id)] = alert.status
  316. return data
  317. @router.put("/users/{slug:user_id}/notifications/alerts/", response={204: None})
  318. async def update_user_notification_alerts(
  319. request: AuthHttpRequest, user_id: MeID, payload: StrKeyIntValue
  320. ):
  321. if user_id != request.auth.user_id and user_id != "me":
  322. raise Http404
  323. user_id = request.auth.user_id
  324. items = [x for x in payload.root.items()]
  325. if len(items) != 1:
  326. raise ValidationError("Invalid alert format, expected one value")
  327. project_id, alert_status = items[0]
  328. if alert_status not in [1, 0, -1]:
  329. raise ValidationError("Invalid status, must be -1, 0, or 1")
  330. alert = await UserProjectAlert.objects.filter(
  331. user_id=user_id, project_id=project_id
  332. ).afirst()
  333. if alert and alert_status == -1:
  334. await alert.adelete()
  335. else:
  336. await UserProjectAlert.objects.aupdate_or_create(
  337. user_id=user_id, project_id=project_id, defaults={"status": alert_status}
  338. )
  339. return 204, None