api.py 11 KB

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