From c66dead21d0a9fe90d5e980f6fa73f79fe2dac11 Mon Sep 17 00:00:00 2001 From: Greg Pstrucha <875316+gricha@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:43:56 -0800 Subject: [PATCH] ref(typing): Let feature and search helpers take a service account is_provider_enabled, the preprod quota checks, investigation action kinds' cannot_apply_reasons, parse_user_value and store_issue_last_defaults now take a RequestPrincipal. They only forward the caller to features.has(), ignore it, or already check has_user(), so this is type-only. --- src/sentry/integrations/base.py | 6 ++---- src/sentry/integrations/mixins/issues.py | 4 ++-- src/sentry/investigations/services/action_kinds/base.py | 6 ++---- .../services/action_kinds/connect_telemetry.py | 6 ++---- .../services/action_kinds/update_detector.py | 6 ++---- src/sentry/preprod/quotas.py | 7 +++---- src/sentry/search/utils.py | 3 ++- tests/sentry/investigations/test_contracts.py | 8 +++----- 8 files changed, 18 insertions(+), 28 deletions(-) diff --git a/src/sentry/integrations/base.py b/src/sentry/integrations/base.py index c853ab69cf62..c9f9a8746258 100644 --- a/src/sentry/integrations/base.py +++ b/src/sentry/integrations/base.py @@ -44,16 +44,14 @@ from sentry.utils.audit import create_audit_entry if TYPE_CHECKING: - from django.contrib.auth.models import AnonymousUser from django.utils.functional import _StrPromise + from sentry.auth.principal import RequestPrincipal from sentry.identity.services.identity.model import RpcIdentity from sentry.integrations.pipeline import IntegrationPipeline # noqa: F401 from sentry.integrations.services.integration import RpcOrganizationIntegration from sentry.integrations.services.integration.model import RpcIntegration from sentry.models.organization import Organization - from sentry.users.models.user import User - from sentry.users.services.user import RpcUser logger = logging.getLogger(__name__) @@ -626,7 +624,7 @@ def get_integration_types(provider: str) -> list[IntegrationDomain]: def is_provider_enabled( provider: IntegrationProvider, organization: Organization | RpcOrganization, - actor: User | RpcUser | AnonymousUser | None = None, + actor: RequestPrincipal | None = None, ) -> bool: from sentry import features diff --git a/src/sentry/integrations/mixins/issues.py b/src/sentry/integrations/mixins/issues.py index 2a2d2cc9ab53..296db7e85ca8 100644 --- a/src/sentry/integrations/mixins/issues.py +++ b/src/sentry/integrations/mixins/issues.py @@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, TypeAlias from sentry import features -from sentry.auth.principal import has_user +from sentry.auth.principal import RequestPrincipal, has_user from sentry.integrations.base import IntegrationInstallation from sentry.integrations.models.external_issue import ExternalIssue from sentry.integrations.models.integration import Integration @@ -232,7 +232,7 @@ def get_persisted_user_default_config_fields(self) -> Sequence[str]: return [] def store_issue_last_defaults( - self, project: Project, user: RpcUser | User, data: IssuePayload + self, project: Project, user: RequestPrincipal, data: IssuePayload ) -> None: """ Stores the last used field defaults on a per-project basis. This diff --git a/src/sentry/investigations/services/action_kinds/base.py b/src/sentry/investigations/services/action_kinds/base.py index ff4c032b4503..6d773f2678b8 100644 --- a/src/sentry/investigations/services/action_kinds/base.py +++ b/src/sentry/investigations/services/action_kinds/base.py @@ -6,15 +6,13 @@ from enum import StrEnum from typing import Any, Literal, NotRequired, TypedDict -from django.contrib.auth.models import AnonymousUser from rest_framework.request import Request from sentry.auth.access import Access +from sentry.auth.principal import RequestPrincipal from sentry.investigations.contracts import ActionPayloadSerializer from sentry.investigations.models import InvestigationOrchestrationRun from sentry.notebooks.models import Notebook, NotebookActionRequest -from sentry.users.models.user import User -from sentry.users.services.user.model import RpcUser class CannotApplyReason(StrEnum): @@ -120,7 +118,7 @@ def cannot_apply_reasons( *, investigations: dict[int, Notebook], access: Access, - user: User | RpcUser | AnonymousUser, + user: RequestPrincipal, ) -> dict[int, CannotApplyReason | None]: """For each suggested request, why the viewer can't apply it, or None if they can.""" return {item.id: CannotApplyReason.UNSUPPORTED_ACTION for item in action_requests} diff --git a/src/sentry/investigations/services/action_kinds/connect_telemetry.py b/src/sentry/investigations/services/action_kinds/connect_telemetry.py index 7770b866c21f..e30fe77cea11 100644 --- a/src/sentry/investigations/services/action_kinds/connect_telemetry.py +++ b/src/sentry/investigations/services/action_kinds/connect_telemetry.py @@ -3,13 +3,13 @@ from collections.abc import Sequence from typing import Literal, TypedDict, cast -from django.contrib.auth.models import AnonymousUser from django.db import router, transaction from django.db.models import QuerySet from rest_framework import serializers from sentry import features from sentry.auth.access import Access +from sentry.auth.principal import RequestPrincipal from sentry.investigations.contracts import ActionPayloadSerializer from sentry.investigations.services.action_kinds.base import ( ActionInvalid, @@ -21,8 +21,6 @@ from sentry.models.organization import Organization from sentry.notebooks.models import Notebook, NotebookActionRequest, NotebookActionRequestState from sentry.seer.agent.monitoring_providers import org_monitoring_provider_registry -from sentry.users.models.user import User -from sentry.users.services.user.model import RpcUser CONNECT_TELEMETRY_ACTION = "connect_telemetry" TelemetryProvider = Literal["datadog", "gcp"] @@ -83,7 +81,7 @@ def cannot_apply_reasons( *, investigations: dict[int, Notebook], access: Access, - user: User | RpcUser | AnonymousUser, + user: RequestPrincipal, ) -> dict[int, CannotApplyReason | None]: can_install = access.has_scope(_INSTALL_SCOPE) reasons: dict[int, CannotApplyReason | None] = {} diff --git a/src/sentry/investigations/services/action_kinds/update_detector.py b/src/sentry/investigations/services/action_kinds/update_detector.py index 2ee52a5e448b..abb144184d15 100644 --- a/src/sentry/investigations/services/action_kinds/update_detector.py +++ b/src/sentry/investigations/services/action_kinds/update_detector.py @@ -5,13 +5,13 @@ from dataclasses import dataclass from typing import Any, Literal, TypedDict, cast -from django.contrib.auth.models import AnonymousUser from django.db import router, transaction from rest_framework import serializers from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.serializers import serialize from sentry.auth.access import Access +from sentry.auth.principal import RequestPrincipal from sentry.db.models.fields.bounded import I32_MAX, I64_MAX from sentry.incidents.endpoints.serializers.query_subscription import SnubaQuerySerializer from sentry.incidents.grouptype import MetricIssue @@ -42,8 +42,6 @@ from sentry.notebooks.contracts import StrictContractSerializer from sentry.notebooks.models import Notebook, NotebookActionRequest from sentry.snuba.models import SnubaQuery -from sentry.users.models.user import User -from sentry.users.services.user.model import RpcUser from sentry.workflow_engine.endpoints.organization_detector_details import ( check_metric_detector_allowed, ) @@ -407,7 +405,7 @@ def cannot_apply_reasons( *, investigations: dict[int, Notebook], access: Access, - user: User | RpcUser | AnonymousUser, + user: RequestPrincipal, ) -> dict[int, CannotApplyReason | None]: source_detector_ids = { investigation_id: source_detector_id(source) diff --git a/src/sentry/preprod/quotas.py b/src/sentry/preprod/quotas.py index 3594aec1d292..35ecb7bef909 100644 --- a/src/sentry/preprod/quotas.py +++ b/src/sentry/preprod/quotas.py @@ -7,16 +7,15 @@ from typing import Any import sentry_sdk -from django.contrib.auth.models import AnonymousUser from django.utils import timezone from sentry import features, quotas +from sentry.auth.principal import RequestPrincipal from sentry.constants import DataCategory from sentry.exceptions import InvalidSearchQuery from sentry.models.organization import Organization from sentry.preprod.artifact_search import artifact_matches_query from sentry.preprod.models import PreprodArtifact -from sentry.users.models.user import User logger = logging.getLogger(__name__) @@ -45,7 +44,7 @@ def get_size_retention_cutoff(organization: Organization) -> datetime: DISTRIBUTION_ENABLED_QUERY_KEY = "sentry:preprod_distribution_enabled_query" -def has_size_quota(organization: Organization, actor: User | AnonymousUser | None = None) -> bool: +def has_size_quota(organization: Organization, actor: RequestPrincipal | None = None) -> bool: result = quotas.backend.has_usage_quota(organization.id, DataCategory.SIZE_ANALYSIS) logger.info( "has_size_quota", @@ -55,7 +54,7 @@ def has_size_quota(organization: Organization, actor: User | AnonymousUser | Non def has_installable_quota( - organization: Organization, actor: User | AnonymousUser | None = None + organization: Organization, actor: RequestPrincipal | None = None ) -> bool: if not features.has( "organizations:preprod-enforce-distribution-quota", organization, actor=actor diff --git a/src/sentry/search/utils.py b/src/sentry/search/utils.py index a18e8471cdde..df54c4effe89 100644 --- a/src/sentry/search/utils.py +++ b/src/sentry/search/utils.py @@ -10,6 +10,7 @@ from django.db import DataError, connections, router from django.utils import timezone as django_timezone +from sentry.auth.principal import RequestPrincipal from sentry.exceptions import InvalidSearchQuery from sentry.models.environment import Environment from sentry.models.group import STATUS_QUERY_CHOICES, Group @@ -355,7 +356,7 @@ def parse_actor_or_none_value( _HACKY_INVALID_USER = RpcUser(id=0) -def parse_user_value(value: str, user: User | RpcUser | AnonymousUser | None) -> RpcUser: +def parse_user_value(value: str, user: RequestPrincipal | None) -> RpcUser: if value == "me": if isinstance(user, User): return serialize_rpc_user(user) diff --git a/tests/sentry/investigations/test_contracts.py b/tests/sentry/investigations/test_contracts.py index 6aaddc012d0d..1f330bc8bff6 100644 --- a/tests/sentry/investigations/test_contracts.py +++ b/tests/sentry/investigations/test_contracts.py @@ -6,9 +6,9 @@ import orjson import pytest -from django.contrib.auth.models import AnonymousUser from sentry.auth.access import Access +from sentry.auth.principal import RequestPrincipal from sentry.db.models.fields.bounded import I32_MAX, I64_MAX from sentry.investigations.contracts import ( MAX_PROJECTION_BYTES, @@ -32,8 +32,6 @@ CannotApplyReason, ) from sentry.notebooks.models import Notebook, NotebookActionRequest -from sentry.users.models.user import User -from sentry.users.services.user.model import RpcUser from sentry.utils import json from tests.sentry.notebooks.test_contracts import golden_payload @@ -536,7 +534,7 @@ def __init__(self) -> None: # The requests after_upsert ran for, in order. self.upserted: list[NotebookActionRequest] = [] # The arguments from each cannot_apply_reasons call. - self.checked: list[tuple[list[int], list[int], Access, User | RpcUser | AnonymousUser]] = [] + self.checked: list[tuple[list[int], list[int], Access, RequestPrincipal]] = [] # Which of check_remote, check and apply ran, in order. self.calls: list[str] = [] # What check and apply raise, if anything. @@ -557,7 +555,7 @@ def cannot_apply_reasons( *, investigations: dict[int, Notebook], access: Access, - user: User | RpcUser | AnonymousUser, + user: RequestPrincipal, ) -> dict[int, CannotApplyReason | None]: self.checked.append( ([item.id for item in action_requests], sorted(investigations), access, user)