Skip to content

Commit 5d4108d

Browse files
committed
perf(auth): Skip full organization hydration during access checks
Fetch project and team IDs lazily for RPC-backed global access, allowing permission contexts to omit full resource collections. Preserve active-resource filtering and request-local caching. Refs ISWF-3361
1 parent 2a08451 commit 5d4108d

6 files changed

Lines changed: 272 additions & 14 deletions

File tree

‎src/sentry/api/permissions.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,10 @@ def determine_access(
227227
org_context = organization
228228
else:
229229
org_context = organization_service.get_organization_by_id(
230-
id=extract_id_from(organization), user_id=user_id
230+
id=extract_id_from(organization),
231+
user_id=user_id,
232+
include_projects=False,
233+
include_teams=False,
231234
)
232235

233236
if org_context is None:
@@ -393,6 +396,8 @@ def determine_access(
393396
org_context = organization_service.get_organization_by_id(
394397
id=extract_id_from(organization),
395398
user_id=request.user.id if request.user else None,
399+
include_projects=False,
400+
include_teams=False,
396401
)
397402

398403
assert org_context is not None, "Failed to fetch organization in determine_access"

‎src/sentry/auth/access.py‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -827,17 +827,17 @@ def has_project_access(self, project: Project) -> bool:
827827
@cached_property
828828
def accessible_team_ids(self) -> frozenset[int]:
829829
return frozenset(
830-
t.id
831-
for t in self.rpc_user_organization_context.organization.teams
832-
if t.status == TeamStatus.ACTIVE
830+
organization_service.get_active_team_ids(
831+
organization_id=self.rpc_user_organization_context.organization.id
832+
)
833833
)
834834

835835
@cached_property
836836
def accessible_project_ids(self) -> frozenset[int]:
837837
return frozenset(
838-
p.id
839-
for p in self.rpc_user_organization_context.organization.projects
840-
if p.status == ObjectStatus.ACTIVE
838+
organization_service.get_active_project_ids(
839+
organization_id=self.rpc_user_organization_context.organization.id
840+
)
841841
)
842842

843843

@@ -1332,7 +1332,10 @@ def from_rpc_member(
13321332
def from_auth(auth: AuthenticatedToken, organization: Organization) -> Access:
13331333
if isinstance(auth, AuthenticatedToken) and auth.superuser is not None:
13341334
context = organization_service.get_organization_by_id(
1335-
id=organization.id, user_id=auth.user_id
1335+
id=organization.id,
1336+
user_id=auth.user_id,
1337+
include_projects=False,
1338+
include_teams=False,
13361339
)
13371340
return from_superuser_access(auth, context) if context is not None else DEFAULT
13381341
if is_system_auth(auth):

‎tests/sentry/api/bases/test_organization.py‎

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,12 @@
3838
from sentry.models.organization import Organization
3939
from sentry.models.organizationmember import OrganizationMember
4040
from sentry.organizations.services.organization import organization_service
41+
from sentry.organizations.services.organization.impl import DatabaseBackedOrganizationService
4142
from sentry.silo.base import SiloMode
4243
from sentry.testutils.cases import TestCase
4344
from sentry.testutils.helpers.datetime import freeze_time
4445
from sentry.testutils.requests import drf_request_from_request
45-
from sentry.testutils.silo import assume_test_silo_mode
46+
from sentry.testutils.silo import all_silo_test, assume_test_silo_mode
4647
from sentry.users.services.user.serial import serialize_rpc_user
4748
from sentry.users.services.user.service import user_service
4849
from sentry.utils.security.orgauthtoken_token import hash_token
@@ -501,6 +502,62 @@ def build_request(self, user=None, active_superuser=False, **params):
501502
return request
502503

503504

505+
@all_silo_test
506+
class DetermineAccessHydrationTest(TestCase):
507+
def test_member_access_omits_full_organization_resources(self) -> None:
508+
user = self.create_user()
509+
org = self.create_organization(flags=0)
510+
team = self.create_team(organization=org)
511+
project = self.create_project(organization=org, teams=[team])
512+
self.create_member(user=user, organization=org, role="member", teams=[team])
513+
other_team = self.create_team(organization=org)
514+
other_project = self.create_project(organization=org, teams=[other_team])
515+
request = drf_request_from_request(self.make_request(user=user))
516+
517+
with (
518+
mock.patch.object(
519+
DatabaseBackedOrganizationService,
520+
"get_organization_by_id",
521+
autospec=True,
522+
side_effect=DatabaseBackedOrganizationService.get_organization_by_id,
523+
) as get_org,
524+
mock.patch(
525+
"sentry.organizations.services.organization.serial.serialize_project"
526+
) as serialize_project,
527+
mock.patch(
528+
"sentry.organizations.services.organization.serial.serialize_rpc_team"
529+
) as serialize_team,
530+
):
531+
OrganizationPermission().determine_access(request, org)
532+
533+
get_org.assert_called_once()
534+
assert get_org.call_args.kwargs["id"] == org.id
535+
assert get_org.call_args.kwargs["user_id"] == user.id
536+
assert get_org.call_args.kwargs["include_projects"] is False
537+
assert get_org.call_args.kwargs["include_teams"] is False
538+
serialize_project.assert_not_called()
539+
serialize_team.assert_not_called()
540+
assert request.access.accessible_team_ids == frozenset({team.id})
541+
assert request.access.accessible_project_ids == frozenset({project.id})
542+
assert not request.access.has_team_access(other_team)
543+
assert not request.access.has_project_access(other_project)
544+
545+
def test_existing_context_is_reused(self) -> None:
546+
user = self.create_user()
547+
org = self.create_organization(owner=user)
548+
context = organization_service.get_organization_by_id(
549+
id=org.id, user_id=user.id, include_projects=False, include_teams=False
550+
)
551+
assert context is not None
552+
request = drf_request_from_request(self.make_request(user=user))
553+
554+
with mock.patch.object(organization_service, "get_organization_by_id") as get_org:
555+
OrganizationPermission().determine_access(request, context)
556+
557+
get_org.assert_not_called()
558+
assert request.access.has_scope("org:read")
559+
560+
504561
class ControlSiloOrganizationEndpointTest(TestCase):
505562
def setUp(self) -> None:
506563
super().setUp()

‎tests/sentry/api/test_permissions.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from unittest.mock import patch
2+
13
from rest_framework.views import APIView
24

35
from sentry.api.bases.organization import OrganizationPermission
@@ -296,6 +298,28 @@ def test_determine_access_no_demo_users(self) -> None:
296298

297299
assert readonly_rpc_context.member.scopes == list(self.org_member_scopes)
298300

301+
def test_determine_access_omits_full_organization_resources(self) -> None:
302+
team = self.create_team(organization=self.organization)
303+
self.create_project(organization=self.organization, teams=[team])
304+
request = self.make_request(self.readonly_user)
305+
306+
with (
307+
override_options(
308+
{"demo-mode.enabled": True, "demo-mode.users": [self.readonly_user.id]}
309+
),
310+
patch(
311+
"sentry.organizations.services.organization.serial.serialize_project"
312+
) as serialize_project,
313+
patch(
314+
"sentry.organizations.services.organization.serial.serialize_rpc_team"
315+
) as serialize_team,
316+
):
317+
self.user_permission.determine_access(request=request, organization=self.organization)
318+
319+
serialize_project.assert_not_called()
320+
serialize_team.assert_not_called()
321+
assert request.access.scopes == frozenset(READONLY_SCOPES)
322+
299323

300324
class InsufficientScopeResponseTest(APITestCase):
301325
"""End-to-end: a token-scope denial reaches the client as a 403 carrying the RFC 6750

0 commit comments

Comments
 (0)