api.py 12 KB

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