views.py 12 KB

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