api.py 10 KB

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