api.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. from typing import Optional
  2. from django.db.models import Count, Exists, OuterRef, Prefetch
  3. from django.http import Http404, HttpResponse
  4. from django.shortcuts import aget_object_or_404
  5. from ninja import Router
  6. from ninja.errors import HttpError
  7. from ninja.pagination import paginate
  8. from apps.organizations_ext.models import (
  9. Organization,
  10. OrganizationUser,
  11. OrganizationUserRole,
  12. )
  13. from apps.projects.models import Project
  14. from apps.shared.types import MeID
  15. from glitchtip.api.authentication import AuthHttpRequest
  16. from glitchtip.api.permissions import has_permission
  17. from .models import Team
  18. from .schema import ProjectTeamSchema, TeamIn, TeamProjectSchema, TeamSchema
  19. router = Router()
  20. """
  21. OSS Sentry supported
  22. GET /teams/{org}/{team}/
  23. PUT /teams/{org}/{team}/
  24. DELETE /teams/{org}/{team}/
  25. GET /teams/{org}/{team}/members/ (See organizations)
  26. GET /teams/{org}/{team}/projects/ (See projects)
  27. GET /teams/{org}/{team}/stats/ (Not implemented)
  28. GET /organizations/{org}/teams/
  29. POST /organizations/{org}/teams/
  30. POST /organizations/{org}/members/{me|member_id}/teams/{team}/ (join)
  31. DELETE /organizations/{org}/members/{me|member_id}/teams/{team}/ (leave)
  32. GET /api/0/projects/{organization_slug}/{project_slug}/teams/ (Not documented)
  33. POST /api/0/projects/{organization_slug}/{project_slug}/teams/{team_slug}/
  34. DELETE /api/0/projects/{organization_slug}/{project_slug}/teams/{team_slug}/
  35. """
  36. def get_team_queryset(
  37. organization_slug: str,
  38. team_slug: Optional[str] = None,
  39. project_slug: Optional[str] = None,
  40. user_id: Optional[int] = None,
  41. id: Optional[int] = None,
  42. add_details=False,
  43. add_projects=False,
  44. ):
  45. qs = Team.objects.filter(organization__slug=organization_slug)
  46. if team_slug:
  47. qs = qs.filter(slug=team_slug)
  48. if project_slug:
  49. qs = qs.filter(projects__slug=project_slug)
  50. if id:
  51. qs = qs.filter(id=id)
  52. if user_id:
  53. qs = qs.filter(organization__users=user_id)
  54. if add_details:
  55. qs = qs.annotate(
  56. is_member=Exists(
  57. OrganizationUser.objects.filter(
  58. teams=OuterRef("pk"), user_id=user_id
  59. )
  60. ),
  61. member_count=Count("members"),
  62. )
  63. if add_projects:
  64. qs = qs.prefetch_related(
  65. Prefetch(
  66. "projects",
  67. queryset=Project.objects.annotate(
  68. is_member=Exists(
  69. OrganizationUser.objects.filter(
  70. teams__members=OuterRef("pk"), user_id=user_id
  71. )
  72. ),
  73. ),
  74. )
  75. )
  76. return qs
  77. @router.get(
  78. "teams/{slug:organization_slug}/{slug:team_slug}/",
  79. response=TeamProjectSchema,
  80. by_alias=True,
  81. )
  82. @has_permission(["team:read", "team:write", "team:admin"])
  83. async def get_team(request: AuthHttpRequest, organization_slug: str, team_slug: str):
  84. user_id = request.auth.user_id
  85. return await aget_object_or_404(
  86. get_team_queryset(
  87. organization_slug,
  88. user_id=user_id,
  89. team_slug=team_slug,
  90. add_details=True,
  91. add_projects=True,
  92. )
  93. )
  94. @router.put(
  95. "teams/{slug:organization_slug}/{slug:team_slug}/",
  96. response=TeamProjectSchema,
  97. by_alias=True,
  98. )
  99. @has_permission(["team:write", "team:admin"])
  100. async def update_team(
  101. request: AuthHttpRequest, organization_slug: str, team_slug: str, payload: TeamIn
  102. ):
  103. user_id = request.auth.user_id
  104. team = await aget_object_or_404(
  105. get_team_queryset(
  106. organization_slug,
  107. user_id=user_id,
  108. team_slug=team_slug,
  109. add_details=True,
  110. add_projects=True,
  111. )
  112. )
  113. team.slug = payload.slug
  114. await team.asave()
  115. return team
  116. @router.delete("teams/{slug:organization_slug}/{slug:team_slug}/", response={204: None})
  117. @has_permission(["team:admin"])
  118. async def delete_team(request: AuthHttpRequest, organization_slug: str, team_slug: str):
  119. result, _ = (
  120. await get_team_queryset(
  121. organization_slug, team_slug=team_slug, user_id=request.auth.user_id
  122. )
  123. .filter(
  124. organization__organization_users__role__gte=OrganizationUserRole.ADMIN,
  125. )
  126. .adelete()
  127. )
  128. if not result:
  129. raise Http404
  130. return 204, None
  131. @router.get(
  132. "/organizations/{slug:organization_slug}/teams/",
  133. response=list[TeamProjectSchema],
  134. by_alias=True,
  135. )
  136. @paginate
  137. @has_permission(
  138. ["team:read", "team:write", "team:admin", "org:read", "org:write", "org:admin"]
  139. )
  140. async def list_teams(
  141. request: AuthHttpRequest, response: HttpResponse, organization_slug: str
  142. ):
  143. return get_team_queryset(
  144. organization_slug,
  145. user_id=request.auth.user_id,
  146. add_details=True,
  147. add_projects=True,
  148. )
  149. @router.post(
  150. "/organizations/{slug:organization_slug}/teams/",
  151. response={201: TeamProjectSchema},
  152. by_alias=True,
  153. )
  154. @has_permission(["team:write", "team:admin", "org:admin", "org:write"])
  155. async def create_team(
  156. request: AuthHttpRequest, organization_slug: str, payload: TeamIn
  157. ):
  158. user_id = request.auth.user_id
  159. organization = await aget_object_or_404(
  160. Organization,
  161. slug=organization_slug,
  162. users=user_id,
  163. organization_users__role__gte=OrganizationUserRole.ADMIN,
  164. )
  165. team = await Team.objects.acreate(organization=organization, slug=payload.slug)
  166. org_user = await organization.organization_users.filter(user=user_id).afirst()
  167. await team.members.aadd(org_user)
  168. return await get_team_queryset(
  169. organization_slug,
  170. user_id=user_id,
  171. id=team.id,
  172. add_details=True,
  173. add_projects=True,
  174. ).aget()
  175. async def modify_member_for_team(
  176. organization_slug: str,
  177. member_id: MeID,
  178. team_slug: str,
  179. user_id: int,
  180. add_member=True,
  181. ):
  182. team = await aget_object_or_404(
  183. get_team_queryset(
  184. organization_slug,
  185. user_id=user_id,
  186. team_slug=team_slug,
  187. add_details=True,
  188. add_projects=True,
  189. )
  190. )
  191. org_user_qs = OrganizationUser.objects.filter(
  192. organization__slug=organization_slug
  193. ).select_related("organization")
  194. if member_id == "me":
  195. org_user = await org_user_qs.aget(user_id=user_id)
  196. else:
  197. org_user = await aget_object_or_404(org_user_qs, id=member_id)
  198. open_membership = org_user.organization.open_membership
  199. is_self = org_user.user_id == user_id
  200. if not (open_membership and is_self):
  201. in_team = await team.members.filter(user_id=user_id).aexists()
  202. if in_team:
  203. required_role = OrganizationUserRole.ADMIN
  204. else:
  205. required_role = OrganizationUserRole.MANAGER
  206. if not await OrganizationUser.objects.filter(
  207. user_id=user_id, organization=org_user.organization, role__gte=required_role
  208. ).aexists():
  209. raise HttpError(403, "Must be admin to modify teams")
  210. if add_member:
  211. await team.members.aadd(org_user)
  212. team.is_member = True
  213. else:
  214. await team.members.aremove(org_user)
  215. return team
  216. @router.post(
  217. "/organizations/{slug:organization_slug}/members/{slug:member_id}/teams/{slug:team_slug}/",
  218. response={201: TeamProjectSchema},
  219. by_alias=True,
  220. )
  221. @has_permission(["team:write", "team:admin"])
  222. async def add_member_to_team(
  223. request: AuthHttpRequest, organization_slug: str, member_id: MeID, team_slug: str
  224. ):
  225. return 201, await modify_member_for_team(
  226. organization_slug, member_id, team_slug, request.auth.user_id, True
  227. )
  228. @router.delete(
  229. "/organizations/{slug:organization_slug}/members/{slug:member_id}/teams/{slug:team_slug}/",
  230. response=TeamProjectSchema,
  231. by_alias=True,
  232. )
  233. @has_permission(["team:write", "team:admin"])
  234. async def delete_member_from_team(
  235. request: AuthHttpRequest, organization_slug: str, member_id: MeID, team_slug: str
  236. ):
  237. return await modify_member_for_team(
  238. organization_slug, member_id, team_slug, request.auth.user_id, False
  239. )
  240. @router.get(
  241. "/projects/{slug:organization_slug}/{slug:project_slug}/teams/",
  242. response=list[TeamSchema],
  243. by_alias=True,
  244. )
  245. @paginate
  246. @has_permission(
  247. ["team:read", "team:write", "team:admin", "org:read", "org:write", "org:admin"]
  248. )
  249. async def list_project_teams(
  250. request: AuthHttpRequest,
  251. response: HttpResponse,
  252. organization_slug: str,
  253. project_slug: str,
  254. ):
  255. return get_team_queryset(
  256. organization_slug,
  257. user_id=request.auth.user_id,
  258. project_slug=project_slug,
  259. add_details=True,
  260. )
  261. @router.post(
  262. "/projects/{slug:organization_slug}/{slug:project_slug}/teams/{slug:team_slug}/",
  263. response={201: ProjectTeamSchema},
  264. by_alias=True,
  265. )
  266. @has_permission(["project.write", "project:admin"])
  267. async def add_team_to_project(
  268. request: AuthHttpRequest, organization_slug: str, project_slug: str, team_slug: str
  269. ):
  270. """Add team to project"""
  271. user_id = request.auth.user_id
  272. project = await aget_object_or_404(
  273. Project,
  274. slug=project_slug,
  275. organization__slug=organization_slug,
  276. organization__users=user_id,
  277. organization__organization_users__role__gte=OrganizationUserRole.MANAGER,
  278. )
  279. team = await aget_object_or_404(
  280. get_team_queryset(organization_slug, team_slug=team_slug)
  281. )
  282. await project.teams.aadd(team)
  283. project = await (
  284. Project.annotate_is_member(Project.objects, user_id)
  285. .prefetch_related("teams")
  286. .aget(id=project.id)
  287. )
  288. return 201, project
  289. @router.delete(
  290. "/projects/{slug:organization_slug}/{slug:project_slug}/teams/{slug:team_slug}/",
  291. response=ProjectTeamSchema,
  292. by_alias=True,
  293. )
  294. @has_permission(["project.write", "project:admin"])
  295. async def delete_team_from_project(
  296. request: AuthHttpRequest, organization_slug: str, project_slug: str, team_slug: str
  297. ):
  298. """Remove team from project"""
  299. user_id = request.auth.user_id
  300. team = await aget_object_or_404(
  301. get_team_queryset(
  302. organization_slug, project_slug=project_slug, team_slug=team_slug
  303. )
  304. )
  305. project = await aget_object_or_404(
  306. Project,
  307. slug=project_slug,
  308. organization__slug=organization_slug,
  309. organization__users=user_id,
  310. organization__organization_users__role__gte=OrganizationUserRole.MANAGER,
  311. )
  312. await project.teams.aremove(team)
  313. return await (
  314. Project.annotate_is_member(Project.objects, user_id)
  315. .prefetch_related("teams")
  316. .aget(id=project.id)
  317. )