api.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. from typing import Optional
  2. from uuid import UUID
  3. from django.http import Http404, HttpResponse
  4. from django.shortcuts import aget_object_or_404
  5. from ninja import Router
  6. from apps.organizations_ext.models import Organization, OrganizationUserRole
  7. from apps.teams.models import Team
  8. from apps.teams.schema import ProjectTeamSchema
  9. from glitchtip.api.pagination import paginate
  10. from glitchtip.api.permissions import AuthHttpRequest, has_permission
  11. from .models import Project, ProjectKey
  12. from .schema import (
  13. ProjectIn,
  14. ProjectKeyIn,
  15. ProjectKeySchema,
  16. ProjectOrganizationSchema,
  17. ProjectSchema,
  18. )
  19. router = Router()
  20. """
  21. GET /api/0/projects/
  22. GET /api/0/projects/{organization_slug}/{project_slug}/
  23. DELETE /api/0/projects/{organization_slug}/{project_slug}/
  24. POST /api/0/projects/{organization_slug}/{project_slug}/teams/{team_slug}/ (See teams)
  25. DELETE /api/0/projects/{organization_slug}/{project_slug}/teams/{team_slug}/ (See teams)
  26. GET /api/0/projects/{organization_slug}/{team_slug}/keys/
  27. POST /api/0/projects/{organization_slug}/{team_slug}/keys/
  28. GET /api/0/projects/{organization_slug}/{project_slug}/keys/{key_id}/
  29. DELETE /api/0/projects/{organization_slug}/{project_slug}/keys/{key_id}/
  30. GET /api/0/teams/{organization_slug}/{team_slug}/projects/
  31. POST /api/0/teams/{organization_slug}/{team_slug}/projects/
  32. GET /api/0/organizations/{organization_slug}/projects/
  33. """
  34. def get_projects_queryset(
  35. user_id: int, organization_slug: str = None, team_slug: str = None
  36. ):
  37. qs = Project.annotate_is_member(
  38. Project.undeleted_objects.filter(organization__users=user_id), user_id
  39. )
  40. if organization_slug:
  41. qs = qs.filter(organization__slug=organization_slug)
  42. if team_slug:
  43. qs = qs.filter(teams__slug=team_slug)
  44. return qs
  45. def get_project_keys_queryset(
  46. user_id: int,
  47. organization_slug: str,
  48. project_slug: str,
  49. key_id: Optional[UUID] = 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: Optional[str] = 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: Optional[str] = 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