33import sentry_sdk
44from django .conf import settings
55from django .db import IntegrityError
6- from django .db .models import Count , Q
6+ from django .db .models import Count , OuterRef , Q , Subquery
7+ from django .db .models .functions import Coalesce
78from drf_spectacular .utils import extend_schema
89from rest_framework import serializers , status
910from rest_framework .request import Request
1617)
1718from sentry .analytics .events .organization_created import OrganizationCreatedEvent
1819from sentry .api .api_publish_status import ApiPublishStatus
19- from sentry .api .base import Endpoint , cell_silo_endpoint
20+ from sentry .api .base import Endpoint , all_silo_endpoint
2021from sentry .api .bases .organization import OrganizationPermission
2122from sentry .api .paginator import DateTimePaginator , OffsetPaginator
2223from sentry .api .serializers import serialize
2324from sentry .api .serializers .models .organization import (
2425 BaseOrganizationSerializer ,
26+ ControlSiloOrganizationMappingSerializer ,
2527 OrganizationSummarySerializerResponse ,
2628)
2729from sentry .apidocs .constants import RESPONSE_FORBIDDEN , RESPONSE_NOT_FOUND , RESPONSE_UNAUTHORIZED
3335from sentry .demo_mode .utils import is_demo_user
3436from sentry .hybridcloud .rpc import IDEMPOTENCY_KEY_LENGTH
3537from sentry .models .organization import Organization , OrganizationStatus
38+ from sentry .models .organizationmapping import OrganizationMapping
3639from sentry .models .organizationmember import OrganizationMember
40+ from sentry .models .organizationmembermapping import OrganizationMemberMapping
3741from sentry .models .projectplatform import ProjectPlatform
3842from sentry .search .utils import tokenize_query
3943from sentry .services .organization import (
4347)
4448from sentry .services .organization .provisioning import organization_provisioning_service
4549from sentry .signals import org_setup_complete , terms_accepted
50+ from sentry .silo .base import SiloMode
4651from sentry .users .services .user .service import user_service
4752from sentry .utils .pagination_factory import PaginatorLike
4853
@@ -69,7 +74,7 @@ def validate_agreeTerms(self, value):
6974
7075
7176@extend_schema (tags = ["Users" ])
72- @cell_silo_endpoint
77+ @all_silo_endpoint
7378class OrganizationIndexEndpoint (Endpoint ):
7479 publish_status = {
7580 "GET" : ApiPublishStatus .PUBLIC ,
@@ -101,6 +106,12 @@ def get(self, request: Request) -> Response:
101106 Return a list of organizations available to the authenticated session in a region.
102107 This is particularly useful for requests with a user bound context. For API key-based requests this will only return the organization that belongs to the key.
103108 """
109+ if SiloMode .get_current_mode () == SiloMode .CONTROL :
110+ return self ._get_from_control (request )
111+
112+ return self ._get_from_cell (request )
113+
114+ def _get_from_cell (self , request : Request ) -> Response :
104115 owner_only = request .GET .get ("owner" ) in ("1" , "true" )
105116
106117 queryset = Organization .objects .distinct ()
@@ -163,6 +174,10 @@ def get(self, request: Request) -> Response:
163174 }
164175 queryset = queryset .filter (Q (member_set__user_id__in = user_ids ))
165176 elif key == "platform" :
177+ # Note: platform filtering is kept here but is not present in the control version
178+ # of this endpoint, since the data is not in control and our UI isn't
179+ # passing this anymore.
180+ sentry_sdk .capture_message ("organization_index.platform_filter_used" )
166181 queryset = queryset .filter (
167182 project__in = ProjectPlatform .objects .filter (platform__in = value ).values (
168183 "project_id"
@@ -193,6 +208,7 @@ def get(self, request: Request) -> Response:
193208 order_by = "-member_count"
194209 paginator_cls = OffsetPaginator
195210 elif sort_by == "projects" :
211+ sentry_sdk .capture_message ("organization_index.sort_by_projects_used" )
196212 queryset = queryset .annotate (project_count = Count ("project" ))
197213 order_by = "-project_count"
198214 paginator_cls = OffsetPaginator
@@ -208,6 +224,119 @@ def get(self, request: Request) -> Response:
208224 paginator_cls = paginator_cls ,
209225 )
210226
227+ def _get_from_control (self , request : Request ) -> Response :
228+ owner_only = request .GET .get ("owner" ) in ("1" , "true" )
229+
230+ if owner_only :
231+ return Response (
232+ {"detail" : "The control-silo organizations endpoint does not support owner=1." },
233+ status = status .HTTP_400_BAD_REQUEST ,
234+ )
235+
236+ queryset = OrganizationMapping .objects .distinct ()
237+
238+ if request .auth and not request .user .is_authenticated :
239+ if hasattr (request .auth , "project" ):
240+ queryset = queryset .filter (organization_id = request .auth .project .organization_id )
241+ elif request .auth .organization_id is not None :
242+ queryset = queryset .filter (organization_id = request .auth .organization_id )
243+ elif not (is_active_superuser (request ) and request .GET .get ("show" ) == "all" ):
244+ assert request .user .id is not None
245+ queryset = queryset .filter (
246+ organization_id__in = OrganizationMemberMapping .objects .filter (
247+ user_id = request .user .id
248+ ).values ("organization_id" )
249+ )
250+ if request .auth and request .auth .organization_id is not None and queryset .count () > 1 :
251+ # If a token is limited to one organization, this endpoint should only return that one organization
252+ queryset = queryset .filter (organization_id = request .auth .organization_id )
253+
254+ query = request .GET .get ("query" )
255+ if query :
256+ tokens = tokenize_query (query )
257+ for key , value in tokens .items ():
258+ if key == "query" :
259+ query_value = " " .join (value )
260+ user_ids = {
261+ u .id
262+ for u in user_service .get_many_by_email (
263+ emails = [query_value ], is_verified = False
264+ )
265+ }
266+ queryset = queryset .filter (
267+ Q (name__icontains = query_value )
268+ | Q (slug__icontains = query_value )
269+ # Control-side equivalent of `Q(member_set__user_id__in=user_ids)`
270+ | Q (
271+ organization_id__in = OrganizationMemberMapping .objects .filter (
272+ user_id__in = user_ids
273+ ).values ("organization_id" )
274+ )
275+ )
276+ elif key == "slug" :
277+ queryset = queryset .filter (in_iexact ("slug" , value ))
278+ elif key == "email" :
279+ user_ids = {
280+ u .id
281+ for u in user_service .get_many_by_email (emails = value , is_verified = False )
282+ }
283+ queryset = queryset .filter (
284+ organization_id__in = OrganizationMemberMapping .objects .filter (
285+ user_id__in = user_ids
286+ ).values ("organization_id" )
287+ )
288+ elif key == "id" :
289+ queryset = queryset .filter (organization_id__in = value )
290+ elif key == "status" :
291+ try :
292+ queryset = queryset .filter (
293+ status__in = [OrganizationStatus [v .upper ()] for v in value ]
294+ )
295+ except KeyError :
296+ queryset = queryset .none ()
297+ elif key == "member_id" :
298+ queryset = queryset .filter (
299+ organization_id__in = OrganizationMemberMapping .objects .filter (
300+ organizationmember_id__in = value
301+ ).values ("organization_id" )
302+ )
303+ else :
304+ queryset = queryset .none ()
305+
306+ sort_by = request .GET .get ("sortBy" )
307+ paginator_cls : type [PaginatorLike ]
308+ if sort_by == "members" :
309+ member_count_subquery = (
310+ OrganizationMemberMapping .objects .filter (
311+ organization_id = OuterRef ("organization_id" )
312+ )
313+ .values ("organization_id" )
314+ .annotate (member_count = Count ("id" ))
315+ .values ("member_count" )[:1 ]
316+ )
317+ queryset = queryset .annotate (member_count = Coalesce (Subquery (member_count_subquery ), 0 ))
318+ order_by = "-member_count"
319+ paginator_cls = OffsetPaginator
320+ elif sort_by == "projects" :
321+ queryset = queryset .none ()
322+ order_by = "-date_created"
323+ paginator_cls = OffsetPaginator
324+ else :
325+ order_by = "-date_created"
326+ paginator_cls = DateTimePaginator
327+
328+ return self .paginate (
329+ request = request ,
330+ queryset = queryset ,
331+ order_by = order_by ,
332+ on_results = lambda x : serialize (
333+ x ,
334+ request .user ,
335+ serializer = ControlSiloOrganizationMappingSerializer (),
336+ ),
337+ paginator_cls = paginator_cls ,
338+ )
339+
211340 # XXX: endpoint useless for end-users as it needs user context.
212341 def post (self , request : Request ) -> Response :
213342 """
@@ -225,6 +354,13 @@ def post(self, request: Request) -> Response:
225354 terms of service and privacy policy.
226355 :auth: required, user-context-needed
227356 """
357+ # TODO(cells): Move org creation to control as part of the broader
358+ # org-listing/org-provisioning cutover. Since POST is private, the
359+ # legacy cell-side path can be removed once the control implementation
360+ # is ready.
361+ if SiloMode .get_current_mode () == SiloMode .CONTROL :
362+ return Response (status = 404 )
363+
228364 if not request .user .is_authenticated :
229365 return Response ({"detail" : "This endpoint requires user info" }, status = 401 )
230366
0 commit comments