1616)
1717from sentry .analytics .events .organization_created import OrganizationCreatedEvent
1818from sentry .api .api_publish_status import ApiPublishStatus
19- from sentry .api .base import Endpoint , cell_silo_endpoint
19+ from sentry .api .base import Endpoint , all_silo_endpoint
2020from sentry .api .bases .organization import OrganizationPermission
2121from sentry .api .paginator import DateTimePaginator , OffsetPaginator
2222from sentry .api .serializers import serialize
2323from sentry .api .serializers .models .organization import (
2424 BaseOrganizationSerializer ,
25+ ControlSiloOrganizationMappingSerializer ,
2526 OrganizationSummarySerializerResponse ,
2627)
2728from sentry .apidocs .constants import RESPONSE_FORBIDDEN , RESPONSE_NOT_FOUND , RESPONSE_UNAUTHORIZED
3334from sentry .demo_mode .utils import is_demo_user
3435from sentry .hybridcloud .rpc import IDEMPOTENCY_KEY_LENGTH
3536from sentry .models .organization import Organization , OrganizationStatus
37+ from sentry .models .organizationmapping import OrganizationMapping
3638from sentry .models .organizationmember import OrganizationMember
39+ from sentry .models .organizationmembermapping import OrganizationMemberMapping
3740from sentry .models .projectplatform import ProjectPlatform
3841from sentry .search .utils import tokenize_query
3942from sentry .services .organization import (
4346)
4447from sentry .services .organization .provisioning import organization_provisioning_service
4548from sentry .signals import org_setup_complete , terms_accepted
49+ from sentry .silo .base import SiloMode
4650from sentry .users .services .user .service import user_service
4751from sentry .utils .pagination_factory import PaginatorLike
4852
@@ -69,7 +73,7 @@ def validate_agreeTerms(self, value):
6973
7074
7175@extend_schema (tags = ["Users" ])
72- @cell_silo_endpoint
76+ @all_silo_endpoint
7377class OrganizationIndexEndpoint (Endpoint ):
7478 publish_status = {
7579 "GET" : ApiPublishStatus .PUBLIC ,
@@ -101,6 +105,12 @@ def get(self, request: Request) -> Response:
101105 Return a list of organizations available to the authenticated session in a region.
102106 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.
103107 """
108+ if SiloMode .get_current_mode () == SiloMode .CONTROL :
109+ return self ._get_from_control (request )
110+
111+ return self ._get_from_cell (request )
112+
113+ def _get_from_cell (self , request : Request ) -> Response :
104114 owner_only = request .GET .get ("owner" ) in ("1" , "true" )
105115
106116 queryset = Organization .objects .distinct ()
@@ -163,6 +173,9 @@ def get(self, request: Request) -> Response:
163173 }
164174 queryset = queryset .filter (Q (member_set__user_id__in = user_ids ))
165175 elif key == "platform" :
176+ # Note: platform filtering is kept here but is not present in the control version
177+ # of this endpoint, since the data is not in control and our UI isn't
178+ # passing this anymore.
166179 queryset = queryset .filter (
167180 project__in = ProjectPlatform .objects .filter (platform__in = value ).values (
168181 "project_id"
@@ -208,6 +221,110 @@ def get(self, request: Request) -> Response:
208221 paginator_cls = paginator_cls ,
209222 )
210223
224+ def _get_from_control (self , request : Request ) -> Response :
225+ owner_only = request .GET .get ("owner" ) in ("1" , "true" )
226+
227+ if owner_only :
228+ return Response (
229+ {"detail" : "The control-silo organizations endpoint does not support owner=1." },
230+ status = status .HTTP_400_BAD_REQUEST ,
231+ )
232+
233+ queryset = OrganizationMapping .objects .distinct ()
234+
235+ if request .auth and not request .user .is_authenticated :
236+ if hasattr (request .auth , "project" ):
237+ queryset = queryset .filter (organization_id = request .auth .project .organization_id )
238+ elif request .auth .organization_id is not None :
239+ queryset = queryset .filter (organization_id = request .auth .organization_id )
240+ elif not (is_active_superuser (request ) and request .GET .get ("show" ) == "all" ):
241+ queryset = queryset .filter (
242+ organization_id__in = OrganizationMemberMapping .objects .filter (
243+ user_id = request .user .id
244+ ).values ("organization_id" )
245+ )
246+ if request .auth and request .auth .organization_id is not None and queryset .count () > 1 :
247+ # If a token is limited to one organization, this endpoint should only return that one organization
248+ queryset = queryset .filter (organization_id = request .auth .organization_id )
249+
250+ query = request .GET .get ("query" )
251+ if query :
252+ tokens = tokenize_query (query )
253+ for key , value in tokens .items ():
254+ if key == "query" :
255+ query_value = " " .join (value )
256+ user_ids = {
257+ u .id
258+ for u in user_service .get_many_by_email (
259+ emails = [query_value ], is_verified = False
260+ )
261+ }
262+ queryset = queryset .filter (
263+ Q (name__icontains = query_value )
264+ | Q (slug__icontains = query_value )
265+ # Control-side equivalent of `Q(member_set__user_id__in=user_ids)`
266+ | Q (
267+ organization_id__in = OrganizationMemberMapping .objects .filter (
268+ user_id__in = user_ids
269+ ).values ("organization_id" )
270+ )
271+ )
272+ elif key == "slug" :
273+ queryset = queryset .filter (in_iexact ("slug" , value ))
274+ elif key == "email" :
275+ user_ids = {
276+ u .id
277+ for u in user_service .get_many_by_email (emails = value , is_verified = False )
278+ }
279+ queryset = queryset .filter (
280+ organization_id__in = OrganizationMemberMapping .objects .filter (
281+ user_id__in = user_ids
282+ ).values ("organization_id" )
283+ )
284+ elif key == "id" :
285+ queryset = queryset .filter (organization_id__in = value )
286+ elif key == "status" :
287+ try :
288+ queryset = queryset .filter (
289+ status__in = [OrganizationStatus [v .upper ()] for v in value ]
290+ )
291+ except KeyError :
292+ queryset = queryset .none ()
293+ elif key == "member_id" :
294+ queryset = queryset .filter (
295+ organization_id__in = OrganizationMemberMapping .objects .filter (
296+ organizationmember_id__in = value
297+ ).values ("organization_id" )
298+ )
299+ else :
300+ queryset = queryset .none ()
301+
302+ sort_by = request .GET .get ("sortBy" )
303+ paginator_cls : type [PaginatorLike ]
304+ if sort_by == "members" :
305+ queryset = queryset .annotate (member_count = Count ("organizationmembermapping" ))
306+ order_by = "-member_count"
307+ paginator_cls = OffsetPaginator
308+ elif sort_by == "projects" :
309+ queryset = queryset .none ()
310+ order_by = "-date_created"
311+ paginator_cls = OffsetPaginator
312+ else :
313+ order_by = "-date_created"
314+ paginator_cls = DateTimePaginator
315+
316+ return self .paginate (
317+ request = request ,
318+ queryset = queryset ,
319+ order_by = order_by ,
320+ on_results = lambda x : serialize (
321+ x ,
322+ request .user ,
323+ serializer = ControlSiloOrganizationMappingSerializer (),
324+ ),
325+ paginator_cls = paginator_cls ,
326+ )
327+
211328 # XXX: endpoint useless for end-users as it needs user context.
212329 def post (self , request : Request ) -> Response :
213330 """
@@ -225,6 +342,13 @@ def post(self, request: Request) -> Response:
225342 terms of service and privacy policy.
226343 :auth: required, user-context-needed
227344 """
345+ # TODO(cells): Move org creation to control as part of the broader
346+ # org-listing/org-provisioning cutover. Since POST is private, the
347+ # legacy cell-side path can be removed once the control implementation
348+ # is ready.
349+ if SiloMode .get_current_mode () == SiloMode .CONTROL :
350+ return Response (status = 404 )
351+
228352 if not request .user .is_authenticated :
229353 return Response ({"detail" : "This endpoint requires user info" }, status = 401 )
230354
0 commit comments