views.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. from django.core.exceptions import ObjectDoesNotExist
  2. from django.shortcuts import get_object_or_404
  3. from django.http import Http404
  4. from drf_yasg.utils import swagger_auto_schema
  5. from rest_framework import viewsets, views, exceptions, permissions
  6. from rest_framework.decorators import action
  7. from rest_framework.response import Response
  8. from rest_framework.exceptions import PermissionDenied
  9. from organizations.backends import invitation_backend
  10. from teams.serializers import TeamSerializer
  11. from users.utils import is_user_registration_open
  12. from projects.views import NestedProjectViewSet
  13. from .permissions import (
  14. OrganizationPermission,
  15. OrganizationMemberPermission,
  16. OrganizationMemberTeamsPermission,
  17. )
  18. from .invitation_backend import InvitationTokenGenerator
  19. from .models import Organization, OrganizationUserRole, OrganizationUser
  20. from .serializers.serializers import (
  21. OrganizationSerializer,
  22. OrganizationDetailSerializer,
  23. OrganizationUserSerializer,
  24. OrganizationUserDetailSerializer,
  25. OrganizationUserProjectsSerializer,
  26. AcceptInviteSerializer,
  27. ReinviteSerializer,
  28. )
  29. class OrganizationViewSet(viewsets.ModelViewSet):
  30. queryset = Organization.objects.all()
  31. serializer_class = OrganizationSerializer
  32. lookup_field = "slug"
  33. permission_classes = [OrganizationPermission]
  34. def get_serializer_class(self):
  35. if self.action in ["retrieve"]:
  36. return OrganizationDetailSerializer
  37. return super().get_serializer_class()
  38. def get_queryset(self):
  39. if not self.request.user.is_authenticated:
  40. return self.queryset.none()
  41. return self.queryset.filter(users=self.request.user).prefetch_related(
  42. "projects__team_set__members",
  43. )
  44. def perform_create(self, serializer):
  45. """ Create organization with current user as owner """
  46. if not is_user_registration_open():
  47. raise exceptions.PermissionDenied("Registration is not open")
  48. organization = serializer.save()
  49. organization.add_user(self.request.user, role=OrganizationUserRole.OWNER)
  50. class OrganizationMemberViewSet(viewsets.ModelViewSet):
  51. """
  52. API compatible with undocumented Sentry endpoint `/api/organizations/<slug>/members/`
  53. """
  54. queryset = OrganizationUser.objects.all()
  55. serializer_class = OrganizationUserSerializer
  56. permission_classes = [OrganizationMemberPermission]
  57. def get_serializer_class(self):
  58. if self.action in ["retrieve"]:
  59. return OrganizationUserDetailSerializer
  60. return super().get_serializer_class()
  61. def get_queryset(self):
  62. if not self.request.user.is_authenticated:
  63. return self.queryset.none()
  64. queryset = self.queryset.filter(organization__users=self.request.user)
  65. organization_slug = self.kwargs.get("organization_slug")
  66. if organization_slug:
  67. queryset = queryset.filter(organization__slug=organization_slug)
  68. team_slug = self.kwargs.get("team_slug")
  69. if team_slug:
  70. queryset = queryset.filter(team__slug=team_slug)
  71. return queryset.select_related("organization", "user").prefetch_related(
  72. "user__socialaccount_set"
  73. )
  74. def get_object(self):
  75. pk = self.kwargs.get("pk")
  76. if pk == "me":
  77. obj = get_object_or_404(self.get_queryset(), user=self.request.user)
  78. self.check_object_permissions(self.request, obj)
  79. return obj
  80. return super().get_object()
  81. def check_permissions(self, request):
  82. if self.request.user.is_authenticated and self.action in [
  83. "create",
  84. "update",
  85. "partial_update",
  86. "destroy",
  87. ]:
  88. org_slug = self.kwargs.get("organization_slug")
  89. try:
  90. user_org_user = self.request.user.organizations_ext_organizationuser.get(
  91. organization__slug=org_slug
  92. )
  93. except ObjectDoesNotExist:
  94. raise PermissionDenied("User is not member of this organization")
  95. if user_org_user.role < OrganizationUserRole.MANAGER:
  96. raise PermissionDenied(
  97. "User must be manager or higher to add organization members"
  98. )
  99. return super().check_permissions(request)
  100. def update(self, request, *args, **kwargs):
  101. """
  102. Update can both reinvite a user or change the org user which require different request data
  103. However it always returns OrganizationUserSerializer regardless
  104. Updates are always partial. Only teams and role may be edited.
  105. """
  106. if self.action in ["update"] and self.request.data.get("reinvite"):
  107. return self.reinvite(request)
  108. kwargs["partial"] = True
  109. return super().update(request, *args, **kwargs)
  110. def reinvite(self, request):
  111. """
  112. Send additional invitation to user
  113. This works more like a rest action, but is embedded within the update view for compatibility
  114. """
  115. instance = self.get_object()
  116. serializer = ReinviteSerializer(instance, data=request.data)
  117. serializer.is_valid(raise_exception=True)
  118. self.perform_update(serializer)
  119. invitation_backend().send_invitation(instance)
  120. serializer = self.serializer_class(instance)
  121. return Response(serializer.data)
  122. def perform_create(self, serializer):
  123. try:
  124. organization = self.request.user.organizations_ext_organization.get(
  125. slug=self.kwargs.get("organization_slug")
  126. )
  127. except ObjectDoesNotExist:
  128. raise Http404
  129. org_user = serializer.save(organization=organization)
  130. invitation_backend().send_invitation(org_user)
  131. return org_user
  132. def check_team_member_permission(self, org_user, user, team):
  133. """ Check if user has permission to update team members """
  134. open_membership = org_user.organization.open_membership
  135. is_self = org_user.user == user
  136. if open_membership and is_self:
  137. return # Ok to modify yourself in any way with open_membership
  138. in_team = team.members.filter(user=user).exists()
  139. if in_team:
  140. required_role = OrganizationUserRole.ADMIN
  141. else:
  142. required_role = OrganizationUserRole.MANAGER
  143. if not self.request.user.organizations_ext_organizationuser.filter(
  144. organization=org_user.organization, role__gte=required_role
  145. ).exists():
  146. raise exceptions.PermissionDenied("Must be admin to modify teams")
  147. @action(
  148. detail=True,
  149. methods=["post", "delete"],
  150. url_path=r"teams/(?P<members_team_slug>[-\w]+)",
  151. )
  152. def teams(self, request, pk=None, organization_slug=None, members_team_slug=None):
  153. """ Add existing organization user to a team """
  154. if not pk or not organization_slug or not members_team_slug:
  155. raise exceptions.MethodNotAllowed(request.method)
  156. pk = self.kwargs.get("pk")
  157. if pk == "me":
  158. org_user = get_object_or_404(self.get_queryset(), user=self.request.user)
  159. else:
  160. queryset = self.filter_queryset(self.get_queryset())
  161. lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
  162. filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
  163. org_user = get_object_or_404(queryset, **filter_kwargs)
  164. team = org_user.organization.teams.filter(slug=members_team_slug).first()
  165. # Instead of check_object_permissions
  166. permission = OrganizationMemberTeamsPermission()
  167. if not permission.has_permission(request, self):
  168. self.permission_denied(
  169. request, message=getattr(permission, "message", None)
  170. )
  171. self.check_team_member_permission(org_user, self.request.user, team)
  172. if not team:
  173. raise exceptions.NotFound()
  174. if request.method == "POST":
  175. team.members.add(org_user)
  176. serializer = TeamSerializer(team, context={"request": request})
  177. return Response(serializer.data, status=201)
  178. elif request.method == "DELETE":
  179. team.members.remove(org_user)
  180. serializer = TeamSerializer(team, context={"request": request})
  181. return Response(serializer.data, status=200)
  182. class OrganizationUserViewSet(OrganizationMemberViewSet):
  183. """
  184. Extension of OrganizationMemberViewSet that adds projects the user has access to
  185. API compatible with [get-organization-users](https://docs.sentry.io/api/organizations/get-organization-users/)
  186. """
  187. serializer_class = OrganizationUserProjectsSerializer
  188. class AcceptInviteView(views.APIView):
  189. """ Accept invite to organization """
  190. serializer_class = AcceptInviteSerializer
  191. permission_classes = [permissions.IsAuthenticatedOrReadOnly]
  192. def validate_token(self, org_user, token):
  193. if not InvitationTokenGenerator().check_token(org_user, token):
  194. raise exceptions.PermissionDenied("Invalid invite token")
  195. @swagger_auto_schema(responses={200: AcceptInviteSerializer()})
  196. def get(self, request, org_user_id=None, token=None):
  197. org_user = get_object_or_404(OrganizationUser, pk=org_user_id)
  198. self.validate_token(org_user, token)
  199. serializer = self.serializer_class(
  200. {"accept_invite": False, "org_user": org_user}
  201. )
  202. return Response(serializer.data)
  203. @swagger_auto_schema(responses={200: AcceptInviteSerializer()})
  204. def post(self, request, org_user_id=None, token=None):
  205. org_user = get_object_or_404(OrganizationUser, pk=org_user_id)
  206. self.validate_token(org_user, token)
  207. serializer = self.serializer_class(data=request.data)
  208. serializer.is_valid(raise_exception=True)
  209. if serializer.validated_data["accept_invite"]:
  210. org_user.accept_invite(request.user)
  211. serializer = self.serializer_class(
  212. {
  213. "accept_invite": serializer.validated_data["accept_invite"],
  214. "org_user": org_user,
  215. }
  216. )
  217. return Response(serializer.data)
  218. class OrganizationProjectsViewSet(NestedProjectViewSet):
  219. def get_queryset(self, *args, **kwargs):
  220. queryset = super().get_queryset(*args, **kwargs)
  221. queries = self.request.GET.get("query")
  222. # Pretty simplistic filters that don't match how django-filter works
  223. # If this needs used more extensively, it should be abstracted more
  224. if queries:
  225. for query in queries.split():
  226. query_part = query.split(":", 1)
  227. if len(query_part) == 2:
  228. query_name, query_value = query_part
  229. if query_name == "team":
  230. queryset = queryset.filter(team__slug=query_value)
  231. if query_name == "!team":
  232. queryset = queryset.exclude(team__slug=query_value)
  233. return queryset