api.py 9.8 KB

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