api.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. from typing import Optional
  2. from django.db.models import Count, Exists, OuterRef, Prefetch, Q
  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 apps.organizations_ext.models import (
  8. Organization,
  9. OrganizationUser,
  10. OrganizationUserRole,
  11. )
  12. from apps.projects.models import Project
  13. from apps.projects.schema import ProjectSchema
  14. from glitchtip.api.authentication import AuthHttpRequest
  15. from glitchtip.api.pagination import paginate
  16. from glitchtip.api.permissions import has_permission
  17. from .models import Team
  18. from .schema import ProjectTeamSchema, TeamIn, 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/
  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. team=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. team__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=TeamSchema,
  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=TeamSchema,
  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[TeamSchema],
  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: TeamSchema},
  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: str,
  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: TeamSchema},
  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: str, 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=TeamSchema,
  231. )
  232. @has_permission(["team:write", "team:admin"])
  233. async def delete_member_from_team(
  234. request: AuthHttpRequest, organization_slug: str, member_id: str, team_slug: str
  235. ):
  236. return await modify_member_for_team(
  237. organization_slug, member_id, team_slug, request.auth.user_id, False
  238. )
  239. @router.get(
  240. "/projects/{slug:organization_slug}/{slug:project_slug}/teams/",
  241. response=list[ProjectTeamSchema],
  242. by_alias=True,
  243. )
  244. @paginate
  245. @has_permission(
  246. ["team:read", "team:write", "team:admin", "org:read", "org:write", "org:admin"]
  247. )
  248. async def list_project_teams(
  249. request: AuthHttpRequest,
  250. response: HttpResponse,
  251. organization_slug: str,
  252. project_slug: str,
  253. ):
  254. return get_team_queryset(
  255. organization_slug,
  256. user_id=request.auth.user_id,
  257. project_slug=project_slug,
  258. add_details=True,
  259. )
  260. @router.post(
  261. "/projects/{slug:organization_slug}/{slug:project_slug}/teams/{slug:team_slug}/",
  262. response={201: ProjectSchema},
  263. )
  264. @has_permission(["project.write", "project:admin"])
  265. async def add_team_to_project(
  266. request: AuthHttpRequest, organization_slug: str, project_slug: str, team_slug: str
  267. ):
  268. """Add team to project"""
  269. user_id = request.auth.user_id
  270. project = await aget_object_or_404(
  271. Project.objects.annotate(
  272. is_member=Count("team__members", filter=Q(team__members__id=user_id))
  273. ),
  274. slug=project_slug,
  275. organization__slug=organization_slug,
  276. organization__users=request.user,
  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.team_set.aadd(team)
  283. return 201, project
  284. @router.delete(
  285. "/projects/{slug:organization_slug}/{slug:project_slug}/teams/{slug:team_slug}/",
  286. response=ProjectSchema,
  287. )
  288. @has_permission(["project.write", "project:admin"])
  289. async def delete_team_from_project(
  290. request: AuthHttpRequest, organization_slug: str, project_slug: str, team_slug: str
  291. ):
  292. """Remove team from project"""
  293. user_id = request.auth.user_id
  294. team = await aget_object_or_404(
  295. get_team_queryset(
  296. organization_slug, project_slug=project_slug, team_slug=team_slug
  297. )
  298. )
  299. qs = Project.objects.annotate(
  300. is_member=Count("team__members", filter=Q(team__members__id=user_id))
  301. )
  302. project = await aget_object_or_404(
  303. qs,
  304. slug=project_slug,
  305. organization__slug=organization_slug,
  306. organization__users=request.user,
  307. organization__organization_users__role__gte=OrganizationUserRole.MANAGER,
  308. )
  309. await project.team_set.aremove(team)
  310. return project