diff --git a/src/sentry/auth/helper.py b/src/sentry/auth/helper.py index d08b37727251f3..783e8440ad4cde 100644 --- a/src/sentry/auth/helper.py +++ b/src/sentry/auth/helper.py @@ -35,6 +35,7 @@ from sentry.auth.partnership_configs import ChannelName from sentry.auth.provider import MigratingIdentityId, Provider from sentry.auth.providers.fly.provider import FlyOAuth2Provider +from sentry.auth.store import FLOW_LOGIN, FLOW_SETUP_PROVIDER, AuthHelperSessionStore from sentry.auth.superuser import is_active_superuser from sentry.hybridcloud.models.outbox import outbox_context from sentry.locks import locks @@ -48,7 +49,7 @@ RpcOrganizationMemberFlags, organization_service, ) -from sentry.pipeline import Pipeline, PipelineSessionStore +from sentry.pipeline import Pipeline from sentry.pipeline.provider import PipelineProvider from sentry.signals import sso_enabled, user_signup from sentry.tasks.auth.auth import email_missing_links_control @@ -58,7 +59,6 @@ from sentry.utils.hashlib import md5_text from sentry.utils.http import absolute_uri from sentry.utils.retries import TimedRetryPolicy -from sentry.utils.session_store import redis_property from sentry.utils.urls import add_params_to_url from sentry.web.forms.accounts import AuthenticationForm from sentry.web.helpers import render_to_response @@ -83,27 +83,6 @@ ERR_INVALID_IDENTITY = _("The provider did not return a valid user identity.") -class AuthHelperSessionStore(PipelineSessionStore): - redis_namespace = "auth" - - @property - def session_key(self) -> str: - return "auth_key" - - flow = redis_property("flow") - referrer = redis_property("referrer") - - def mark_session(self) -> None: - super().mark_session() - self.request.session.modified = True - - def is_valid(self) -> bool: - return super().is_valid() and self.flow in ( - AuthHelper.FLOW_LOGIN, - AuthHelper.FLOW_SETUP_PROVIDER, - ) - - @dataclass class AuthIdentityHandler: # SSO auth handler @@ -650,7 +629,7 @@ def handle_new_user(self) -> AuthIdentity: return auth_identity -class AuthHelper(Pipeline[AuthProvider]): +class AuthHelper(Pipeline[AuthProvider, AuthHelperSessionStore]): """ Helper class which is passed into AuthView's. @@ -671,11 +650,6 @@ class AuthHelper(Pipeline[AuthProvider]): it with their account (thus creating a new account). """ - # logging in or registering - FLOW_LOGIN = 1 - # configuring the provider - FLOW_SETUP_PROVIDER = 2 - pipeline_name = "pipeline" provider_manager = manager provider_model_cls = AuthProvider @@ -731,7 +705,7 @@ def __init__( def get_provider( self, provider_key: str | None, *, organization: RpcOrganization | None - ) -> PipelineProvider[AuthProvider]: + ) -> PipelineProvider[AuthProvider, AuthHelperSessionStore]: if self.provider_model: return self.provider_model.get_provider() elif provider_key: @@ -741,15 +715,15 @@ def get_provider( def get_pipeline_views(self) -> Sequence[View]: assert isinstance(self.provider, Provider) - if self.flow == self.FLOW_LOGIN: + if self.flow == FLOW_LOGIN: return self.provider.get_auth_pipeline() - elif self.flow == self.FLOW_SETUP_PROVIDER: + elif self.flow == FLOW_SETUP_PROVIDER: return self.provider.get_setup_pipeline() else: raise NotImplementedError def is_valid(self) -> bool: - return super().is_valid() and self.state.flow in (self.FLOW_LOGIN, self.FLOW_SETUP_PROVIDER) + return super().is_valid() and self.state.flow in (FLOW_LOGIN, FLOW_SETUP_PROVIDER) def get_initial_state(self) -> Mapping[str, Any]: state = dict(super().get_initial_state()) @@ -772,10 +746,10 @@ def finish_pipeline(self) -> HttpResponseBase: except IdentityNotValid as error: return self.error(str(error) or ERR_INVALID_IDENTITY) - if self.state.flow == self.FLOW_LOGIN: + if self.state.flow == FLOW_LOGIN: # create identity and authenticate the user response = self._finish_login_pipeline(identity) - elif self.state.flow == self.FLOW_SETUP_PROVIDER: + elif self.state.flow == FLOW_SETUP_PROVIDER: # Configuring the SSO Auth provider response = self._finish_setup_pipeline(identity) else: @@ -921,11 +895,11 @@ def _finish_setup_pipeline(self, identity: Mapping[str, Any]) -> HttpResponseRed def error(self, message: str | _StrPromise) -> HttpResponseRedirect: redirect_uri = "/" - if self.state.flow == self.FLOW_LOGIN: + if self.state.flow == FLOW_LOGIN: # create identity and authenticate the user redirect_uri = reverse("sentry-auth-organization", args=[self.organization.slug]) - elif self.state.flow == self.FLOW_SETUP_PROVIDER: + elif self.state.flow == FLOW_SETUP_PROVIDER: redirect_uri = reverse( "sentry-organization-auth-settings", args=[self.organization.slug] ) diff --git a/src/sentry/auth/provider.py b/src/sentry/auth/provider.py index f91d3cb57a2361..81af4b02f52c6a 100644 --- a/src/sentry/auth/provider.py +++ b/src/sentry/auth/provider.py @@ -10,6 +10,7 @@ from django.utils.encoding import force_str from sentry.auth.services.auth.model import RpcAuthProvider +from sentry.auth.store import AuthHelperSessionStore from sentry.auth.view import AuthView from sentry.models.authidentity import AuthIdentity from sentry.models.authprovider import AuthProvider @@ -39,7 +40,7 @@ def __str__(self) -> str: return force_str(self.id) -class Provider(PipelineProvider[AuthProvider], abc.ABC): +class Provider(PipelineProvider[AuthProvider, AuthHelperSessionStore], abc.ABC): """ A provider indicates how authenticate should happen for a given service, including its configuration and basic identity management. @@ -84,7 +85,7 @@ def get_pipeline_views(self) -> Sequence[AuthView]: # TODO: state should be Mapping[str, Any]? # Must be reconciled with sentry.pipeline.base.Pipeline.fetch_state - def build_config(self, state: Any) -> Mapping[str, Any]: + def build_config(self, state: Any) -> dict[str, Any]: """ Return a mapping containing provider configuration. diff --git a/src/sentry/auth/providers/saml2/provider.py b/src/sentry/auth/providers/saml2/provider.py index 18db8608c2b73f..44fe9eb28700f7 100644 --- a/src/sentry/auth/providers/saml2/provider.py +++ b/src/sentry/auth/providers/saml2/provider.py @@ -21,6 +21,7 @@ from sentry import features, options from sentry.auth.exceptions import IdentityNotValid from sentry.auth.provider import Provider +from sentry.auth.store import FLOW_LOGIN from sentry.auth.view import AuthView from sentry.models.authidentity import AuthIdentity from sentry.models.authprovider import AuthProvider @@ -118,7 +119,7 @@ def dispatch(self, request: HttpRequest, organization_slug: str) -> HttpResponse request=request, organization=(org_context.organization), auth_provider=auth_provider, - flow=AuthHelper.FLOW_LOGIN, + flow=FLOW_LOGIN, ) helper.initialize() diff --git a/src/sentry/auth/store.py b/src/sentry/auth/store.py new file mode 100644 index 00000000000000..73bd0a887a49c2 --- /dev/null +++ b/src/sentry/auth/store.py @@ -0,0 +1,25 @@ +from sentry.pipeline.store import PipelineSessionStore +from sentry.utils.session_store import redis_property + +# logging in or registering +FLOW_LOGIN = 1 +# configuring the provider +FLOW_SETUP_PROVIDER = 2 + + +class AuthHelperSessionStore(PipelineSessionStore): + redis_namespace = "auth" + + @property + def session_key(self) -> str: + return "auth_key" + + flow = redis_property("flow") + referrer = redis_property("referrer") + + def mark_session(self) -> None: + super().mark_session() + self.request.session.modified = True + + def is_valid(self) -> bool: + return super().is_valid() and self.flow in (FLOW_LOGIN, FLOW_SETUP_PROVIDER) diff --git a/src/sentry/identity/base.py b/src/sentry/identity/base.py index 9a63f7d96515c8..2b497fa32873f7 100644 --- a/src/sentry/identity/base.py +++ b/src/sentry/identity/base.py @@ -2,12 +2,12 @@ import logging from typing import Any +from sentry.identity.pipeline_types import IdentityPipelineProviderT from sentry.identity.services.identity.model import RpcIdentity -from sentry.pipeline import PipelineProvider -from sentry.users.models.identity import Identity, IdentityProvider +from sentry.users.models.identity import Identity -class Provider(PipelineProvider[IdentityProvider], abc.ABC): +class Provider(IdentityPipelineProviderT, abc.ABC): """ A provider indicates how identity authenticate should happen for a given service. """ diff --git a/src/sentry/identity/bitbucket/provider.py b/src/sentry/identity/bitbucket/provider.py index 1fdbe947d77d41..07fc5e4bf0c9da 100644 --- a/src/sentry/identity/bitbucket/provider.py +++ b/src/sentry/identity/bitbucket/provider.py @@ -2,8 +2,7 @@ from django.http.response import HttpResponseBase, HttpResponseRedirect from sentry.identity.base import Provider -from sentry.pipeline import Pipeline, PipelineView -from sentry.users.models.identity import IdentityProvider +from sentry.identity.pipeline_types import IdentityPipelineT, IdentityPipelineViewT from sentry.utils.http import absolute_uri @@ -11,14 +10,12 @@ class BitbucketIdentityProvider(Provider): key = "bitbucket" name = "Bitbucket" - def get_pipeline_views(self) -> list[PipelineView[IdentityProvider]]: + def get_pipeline_views(self) -> list[IdentityPipelineViewT]: return [BitbucketLoginView()] -class BitbucketLoginView(PipelineView[IdentityProvider]): - def dispatch( - self, request: HttpRequest, pipeline: Pipeline[IdentityProvider] - ) -> HttpResponseBase: +class BitbucketLoginView(IdentityPipelineViewT): + def dispatch(self, request: HttpRequest, pipeline: IdentityPipelineT) -> HttpResponseBase: from sentry.integrations.base import IntegrationDomain from sentry.integrations.utils.metrics import ( IntegrationPipelineViewEvent, diff --git a/src/sentry/identity/oauth2.py b/src/sentry/identity/oauth2.py index e86f420c81478a..38f3e63db742a8 100644 --- a/src/sentry/identity/oauth2.py +++ b/src/sentry/identity/oauth2.py @@ -18,6 +18,7 @@ from sentry.auth.exceptions import IdentityNotValid from sentry.exceptions import NotRegistered from sentry.http import safe_urlopen, safe_urlread +from sentry.identity.pipeline_types import IdentityPipelineT, IdentityPipelineViewT from sentry.identity.services.identity.model import RpcIdentity from sentry.integrations.base import IntegrationDomain from sentry.integrations.utils.metrics import ( @@ -26,9 +27,8 @@ IntegrationPipelineViewEvent, IntegrationPipelineViewType, ) -from sentry.pipeline import Pipeline, PipelineView from sentry.shared_integrations.exceptions import ApiError -from sentry.users.models.identity import Identity, IdentityProvider +from sentry.users.models.identity import Identity from sentry.utils.http import absolute_uri from .base import Provider @@ -40,7 +40,7 @@ ERR_TOKEN_RETRIEVAL = "Failed to retrieve token from the upstream service." -def _redirect_url(pipeline: Pipeline[IdentityProvider]) -> str: +def _redirect_url(pipeline: IdentityPipelineT) -> str: associate_url = reverse( "sentry-extension-setup", kwargs={ @@ -119,7 +119,7 @@ def get_oauth_scopes(self): def get_refresh_token_headers(self): return None - def get_pipeline_views(self) -> list[PipelineView[IdentityProvider]]: + def get_pipeline_views(self) -> list[IdentityPipelineViewT]: return [ OAuth2LoginView( authorize_url=self.get_oauth_authorize_url(), @@ -243,7 +243,7 @@ def record_event(event: IntegrationPipelineViewType, provider: str): ) -class OAuth2LoginView(PipelineView[IdentityProvider]): +class OAuth2LoginView(IdentityPipelineViewT): authorize_url: str | None = None client_id: str | None = None scope = "" @@ -273,9 +273,7 @@ def get_authorize_params(self, state, redirect_uri): } @method_decorator(csrf_exempt) - def dispatch( - self, request: HttpRequest, pipeline: Pipeline[IdentityProvider] - ) -> HttpResponseBase: + def dispatch(self, request: HttpRequest, pipeline: IdentityPipelineT) -> HttpResponseBase: with record_event(IntegrationPipelineViewType.OAUTH_LOGIN, pipeline.provider.key).capture(): for param in ("code", "error", "state"): if param in request.GET: @@ -295,7 +293,7 @@ def dispatch( return HttpResponseRedirect(redirect_uri) -class OAuth2CallbackView(PipelineView[IdentityProvider]): +class OAuth2CallbackView(IdentityPipelineViewT): access_token_url: str | None = None client_id: str | None = None client_secret: str | None = None @@ -319,7 +317,7 @@ def get_token_params(self, code, redirect_uri): } def exchange_token( - self, request: HttpRequest, pipeline: Pipeline[IdentityProvider], code: str + self, request: HttpRequest, pipeline: IdentityPipelineT, code: str ) -> dict[str, str]: with record_event( IntegrationPipelineViewType.TOKEN_EXCHANGE, pipeline.provider.key @@ -363,9 +361,7 @@ def exchange_token( "error_description": "We were not able to parse a JSON response, please try again.", } - def dispatch( - self, request: HttpRequest, pipeline: Pipeline[IdentityProvider] - ) -> HttpResponseBase: + def dispatch(self, request: HttpRequest, pipeline: IdentityPipelineT) -> HttpResponseBase: with record_event( IntegrationPipelineViewType.OAUTH_CALLBACK, pipeline.provider.key ).capture() as lifecycle: diff --git a/src/sentry/identity/pipeline.py b/src/sentry/identity/pipeline.py index 35c348187525f8..000a667a1a8f3b 100644 --- a/src/sentry/identity/pipeline.py +++ b/src/sentry/identity/pipeline.py @@ -1,16 +1,18 @@ +from __future__ import annotations + from django.contrib import messages from django.http import HttpResponseRedirect from django.urls import reverse from django.utils.translation import gettext_lazy as _ from sentry import features, options +from sentry.identity.pipeline_types import IdentityPipelineProviderT, IdentityPipelineT from sentry.integrations.base import IntegrationDomain from sentry.integrations.utils.metrics import ( IntegrationPipelineViewEvent, IntegrationPipelineViewType, ) from sentry.organizations.services.organization.model import RpcOrganization -from sentry.pipeline import Pipeline, PipelineProvider from sentry.users.models.identity import Identity, IdentityProvider from sentry.utils import metrics @@ -19,7 +21,7 @@ IDENTITY_LINKED = _("Your {identity_provider} account has been associated with your Sentry account") -class IdentityProviderPipeline(Pipeline[IdentityProvider]): +class IdentityProviderPipeline(IdentityPipelineT): pipeline_name = "identity_provider" provider_manager = default_manager provider_model_cls = IdentityProvider @@ -27,7 +29,7 @@ class IdentityProviderPipeline(Pipeline[IdentityProvider]): # TODO(iamrajjoshi): Delete this after Azure DevOps migration is complete def get_provider( self, provider_key: str, *, organization: RpcOrganization | None - ) -> PipelineProvider[IdentityProvider]: + ) -> IdentityPipelineProviderT: if provider_key == "vsts" and features.has( "organizations:migrate-azure-devops-integration", organization ): diff --git a/src/sentry/identity/pipeline_types.py b/src/sentry/identity/pipeline_types.py new file mode 100644 index 00000000000000..49db26d6a4270c --- /dev/null +++ b/src/sentry/identity/pipeline_types.py @@ -0,0 +1,11 @@ +from typing import TypeAlias + +from sentry.pipeline.base import Pipeline +from sentry.pipeline.provider import PipelineProvider +from sentry.pipeline.store import PipelineSessionStore +from sentry.pipeline.views.base import PipelineView +from sentry.users.models.identity import IdentityProvider + +IdentityPipelineT: TypeAlias = Pipeline[IdentityProvider, PipelineSessionStore] +IdentityPipelineProviderT: TypeAlias = PipelineProvider[IdentityProvider, PipelineSessionStore] +IdentityPipelineViewT: TypeAlias = PipelineView[IdentityProvider, PipelineSessionStore] diff --git a/src/sentry/identity/providers/dummy.py b/src/sentry/identity/providers/dummy.py index 5185f786391d17..8240757c1c68c5 100644 --- a/src/sentry/identity/providers/dummy.py +++ b/src/sentry/identity/providers/dummy.py @@ -5,17 +5,15 @@ from django.http.response import HttpResponseBase from sentry.identity.base import Provider +from sentry.identity.pipeline_types import IdentityPipelineT, IdentityPipelineViewT from sentry.identity.services.identity.model import RpcIdentity -from sentry.pipeline import Pipeline, PipelineView -from sentry.users.models.identity import Identity, IdentityProvider +from sentry.users.models.identity import Identity __all__ = ("DummyProvider",) -class AskEmail(PipelineView[IdentityProvider]): - def dispatch( - self, request: HttpRequest, pipeline: Pipeline[IdentityProvider] - ) -> HttpResponseBase: +class AskEmail(IdentityPipelineViewT): + def dispatch(self, request: HttpRequest, pipeline: IdentityPipelineT) -> HttpResponseBase: if "email" in request.POST: pipeline.bind_state("email", request.POST.get("email")) return pipeline.next_step() @@ -29,7 +27,7 @@ class DummyProvider(Provider): TEMPLATE = '
' - def get_pipeline_views(self) -> list[PipelineView[IdentityProvider]]: + def get_pipeline_views(self) -> list[IdentityPipelineViewT]: return [AskEmail()] def build_identity(self, state): diff --git a/src/sentry/identity/slack/provider.py b/src/sentry/identity/slack/provider.py index 0211a0822d38d3..829a3da8d45f9b 100644 --- a/src/sentry/identity/slack/provider.py +++ b/src/sentry/identity/slack/provider.py @@ -1,7 +1,6 @@ from sentry import options from sentry.identity.oauth2 import OAuth2CallbackView, OAuth2LoginView, OAuth2Provider -from sentry.pipeline.views.base import PipelineView -from sentry.users.models.identity import IdentityProvider +from sentry.identity.pipeline_types import IdentityPipelineViewT class SlackIdentityProvider(OAuth2Provider): @@ -36,7 +35,7 @@ def get_oauth_client_secret(self): def get_user_scopes(self): return self.config.get("user_scopes", self.user_scopes) - def get_pipeline_views(self) -> list[PipelineView[IdentityProvider]]: + def get_pipeline_views(self) -> list[IdentityPipelineViewT]: return [ SlackOAuth2LoginView( authorize_url=self.get_oauth_authorize_url(), diff --git a/src/sentry/identity/vercel/provider.py b/src/sentry/identity/vercel/provider.py index 162349c8400ce2..285aedd7780d40 100644 --- a/src/sentry/identity/vercel/provider.py +++ b/src/sentry/identity/vercel/provider.py @@ -1,7 +1,6 @@ from sentry import options from sentry.identity.oauth2 import OAuth2CallbackView, OAuth2Provider -from sentry.pipeline.views.base import PipelineView -from sentry.users.models.identity import IdentityProvider +from sentry.identity.pipeline_types import IdentityPipelineViewT class VercelIdentityProvider(OAuth2Provider): @@ -20,7 +19,7 @@ def get_oauth_client_secret(self): def get_refresh_token_url(self) -> str: return self.oauth_access_token_url - def get_pipeline_views(self) -> list[PipelineView[IdentityProvider]]: + def get_pipeline_views(self) -> list[IdentityPipelineViewT]: return [ OAuth2CallbackView( access_token_url=self.oauth_access_token_url, diff --git a/src/sentry/identity/vsts/provider.py b/src/sentry/identity/vsts/provider.py index 91d9166a136758..115c719144fb78 100644 --- a/src/sentry/identity/vsts/provider.py +++ b/src/sentry/identity/vsts/provider.py @@ -10,10 +10,9 @@ from sentry import http, options from sentry.http import safe_urlopen, safe_urlread from sentry.identity.oauth2 import OAuth2CallbackView, OAuth2LoginView, OAuth2Provider, record_event +from sentry.identity.pipeline_types import IdentityPipelineT, IdentityPipelineViewT from sentry.integrations.utils.metrics import IntegrationPipelineViewType -from sentry.pipeline.base import Pipeline -from sentry.pipeline.views.base import PipelineView -from sentry.users.models.identity import Identity, IdentityProvider +from sentry.users.models.identity import Identity from sentry.utils.http import absolute_uri @@ -63,7 +62,7 @@ def get_oauth_client_secret(self): def get_refresh_token_url(self) -> str: return self.oauth_access_token_url - def get_pipeline_views(self) -> list[PipelineView[IdentityProvider]]: + def get_pipeline_views(self) -> list[IdentityPipelineViewT]: return [ OAuth2LoginView( authorize_url=self.oauth_authorize_url, @@ -126,7 +125,7 @@ def build_identity(self, data): class VSTSOAuth2CallbackView(OAuth2CallbackView): def exchange_token( - self, request: HttpRequest, pipeline: Pipeline[IdentityProvider], code: str + self, request: HttpRequest, pipeline: IdentityPipelineT, code: str ) -> dict[str, str]: with record_event( IntegrationPipelineViewType.TOKEN_EXCHANGE, pipeline.provider.key @@ -247,7 +246,7 @@ def get_authorize_params(self, state, redirect_uri): class VSTSNewOAuth2CallbackView(OAuth2CallbackView): def exchange_token( - self, request: HttpRequest, pipeline: Pipeline[IdentityProvider], code: str + self, request: HttpRequest, pipeline: IdentityPipelineT, code: str ) -> dict[str, str]: with record_event( IntegrationPipelineViewType.TOKEN_EXCHANGE, pipeline.provider.key diff --git a/src/sentry/integrations/aws_lambda/integration.py b/src/sentry/integrations/aws_lambda/integration.py index efaf65df644a32..a3b0522aeea1e3 100644 --- a/src/sentry/integrations/aws_lambda/integration.py +++ b/src/sentry/integrations/aws_lambda/integration.py @@ -3,7 +3,7 @@ import logging from collections.abc import Mapping from concurrent.futures import ThreadPoolExecutor -from typing import TYPE_CHECKING, Any, Never +from typing import TYPE_CHECKING, Any from botocore.exceptions import ClientError from django.http.request import HttpRequest @@ -22,9 +22,9 @@ from sentry.integrations.mixins import ServerlessMixin from sentry.integrations.models.integration import Integration from sentry.integrations.models.organization_integration import OrganizationIntegration +from sentry.integrations.pipeline_types import IntegrationPipelineT, IntegrationPipelineViewT from sentry.organizations.services.organization import organization_service from sentry.organizations.services.organization.model import RpcOrganization -from sentry.pipeline import Pipeline, PipelineView from sentry.projects.services.project import project_service from sentry.silo.base import control_silo_function from sentry.users.models.user import User @@ -203,7 +203,7 @@ class AwsLambdaIntegrationProvider(IntegrationProvider): integration_cls = AwsLambdaIntegration features = frozenset([IntegrationFeatures.SERVERLESS]) - def get_pipeline_views(self) -> list[PipelineView[Never]]: + def get_pipeline_views(self) -> list[IntegrationPipelineViewT]: return [ AwsLambdaProjectSelectPipelineView(), AwsLambdaCloudFormationPipelineView(), @@ -261,8 +261,8 @@ def post_install( oi.update(config={"default_project_id": default_project_id}) -class AwsLambdaProjectSelectPipelineView(PipelineView[Never]): - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: +class AwsLambdaProjectSelectPipelineView(IntegrationPipelineViewT): + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: # if we have the projectId, go to the next step if "projectId" in request.GET: pipeline.bind_state("project_id", request.GET["projectId"]) @@ -288,8 +288,8 @@ def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpRespo ) -class AwsLambdaCloudFormationPipelineView(PipelineView[Never]): - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: +class AwsLambdaCloudFormationPipelineView(IntegrationPipelineViewT): + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: curr_step = 0 if pipeline.fetch_state("skipped_project_select") else 1 def render_response(error=None): @@ -351,8 +351,8 @@ def render_response(error=None): return render_response() -class AwsLambdaListFunctionsPipelineView(PipelineView[Never]): - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: +class AwsLambdaListFunctionsPipelineView(IntegrationPipelineViewT): + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: if request.method == "POST": raw_data = request.POST data = {} @@ -379,8 +379,8 @@ def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpRespo ) -class AwsLambdaSetupLayerPipelineView(PipelineView[Never]): - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: +class AwsLambdaSetupLayerPipelineView(IntegrationPipelineViewT): + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: if "finish_pipeline" in request.GET: return pipeline.finish_pipeline() diff --git a/src/sentry/integrations/base.py b/src/sentry/integrations/base.py index c89b580efffbcd..83d610f5a387ce 100644 --- a/src/sentry/integrations/base.py +++ b/src/sentry/integrations/base.py @@ -6,7 +6,7 @@ from collections.abc import Callable, Mapping, MutableMapping, Sequence from enum import StrEnum from functools import cached_property -from typing import TYPE_CHECKING, Any, NamedTuple, Never, NoReturn, NotRequired, TypedDict +from typing import TYPE_CHECKING, Any, NamedTuple, NoReturn, NotRequired, TypedDict from rest_framework.exceptions import NotFound from rest_framework.request import Request @@ -20,6 +20,10 @@ from sentry.integrations.models.external_actor import ExternalActor from sentry.integrations.models.integration import Integration from sentry.integrations.notify_disable import notify_disable +from sentry.integrations.pipeline_types import ( + IntegrationPipelineProviderT, + IntegrationPipelineViewT, +) from sentry.integrations.request_buffer import IntegrationRequestBuffer from sentry.integrations.types import IntegrationProviderSlug from sentry.models.team import Team @@ -28,8 +32,6 @@ RpcOrganizationSummary, organization_service, ) -from sentry.pipeline import PipelineProvider -from sentry.pipeline.views.base import PipelineView from sentry.shared_integrations.constants import ( ERR_INTERNAL, ERR_UNAUTHORIZED, @@ -182,7 +184,7 @@ class IntegrationData(TypedDict): provider: NotRequired[str] # maybe unused ??? -class IntegrationProvider(PipelineProvider[Never], abc.ABC): +class IntegrationProvider(IntegrationPipelineProviderT, abc.ABC): """ An integration provider describes a third party that can be registered within Sentry. @@ -297,7 +299,7 @@ def create_audit_log_entry( def get_pipeline_views( self, - ) -> Sequence[PipelineView[Never] | Callable[[], PipelineView[Never]]]: + ) -> Sequence[IntegrationPipelineViewT | Callable[[], IntegrationPipelineViewT]]: """ Return a list of ``View`` instances describing this integration's configuration pipeline. diff --git a/src/sentry/integrations/bitbucket/integration.py b/src/sentry/integrations/bitbucket/integration.py index d4c38507fd87a0..09dae54d1530cc 100644 --- a/src/sentry/integrations/bitbucket/integration.py +++ b/src/sentry/integrations/bitbucket/integration.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Mapping, Sequence -from typing import Any, Never +from typing import Any from django.http.request import HttpRequest from django.http.response import HttpResponseBase @@ -18,6 +18,7 @@ IntegrationProvider, ) from sentry.integrations.models.integration import Integration +from sentry.integrations.pipeline_types import IntegrationPipelineT, IntegrationPipelineViewT from sentry.integrations.services.repository import RpcRepository, repository_service from sentry.integrations.source_code_management.repository import RepositoryIntegration from sentry.integrations.tasks.migrate_repo import migrate_repo @@ -32,7 +33,7 @@ from sentry.models.apitoken import generate_token from sentry.models.repository import Repository from sentry.organizations.services.organization.model import RpcOrganization -from sentry.pipeline import NestedPipelineView, Pipeline, PipelineView +from sentry.pipeline import NestedPipelineView from sentry.shared_integrations.exceptions import ApiError from sentry.utils.http import absolute_uri @@ -197,7 +198,7 @@ class BitbucketIntegrationProvider(IntegrationProvider): ] ) - def get_pipeline_views(self) -> Sequence[PipelineView[Never]]: + def get_pipeline_views(self) -> Sequence[IntegrationPipelineViewT]: return [ NestedPipelineView( bind_key="identity", @@ -273,8 +274,8 @@ def setup(self): ) -class VerifyInstallation(PipelineView[Never]): - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: +class VerifyInstallation(IntegrationPipelineViewT): + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: with IntegrationPipelineViewEvent( IntegrationPipelineViewType.VERIFY_INSTALLATION, IntegrationDomain.SOURCE_CODE_MANAGEMENT, diff --git a/src/sentry/integrations/bitbucket_server/integration.py b/src/sentry/integrations/bitbucket_server/integration.py index 2d8979b49c7a30..d659603d4aee9e 100644 --- a/src/sentry/integrations/bitbucket_server/integration.py +++ b/src/sentry/integrations/bitbucket_server/integration.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Mapping -from typing import Any, Never +from typing import Any from urllib.parse import parse_qs, quote, urlencode, urlparse from cryptography.hazmat.backends import default_backend @@ -24,6 +24,7 @@ IntegrationProvider, ) from sentry.integrations.models.integration import Integration +from sentry.integrations.pipeline_types import IntegrationPipelineT, IntegrationPipelineViewT from sentry.integrations.services.repository import repository_service from sentry.integrations.services.repository.model import RpcRepository from sentry.integrations.source_code_management.repository import RepositoryIntegration @@ -34,7 +35,6 @@ ) from sentry.models.repository import Repository from sentry.organizations.services.organization.model import RpcOrganization -from sentry.pipeline import Pipeline, PipelineView from sentry.shared_integrations.exceptions import ApiError, IntegrationError from sentry.users.models.identity import Identity from sentry.web.helpers import render_to_response @@ -150,12 +150,12 @@ def clean_consumer_key(self): return data -class InstallationConfigView(PipelineView[Never]): +class InstallationConfigView(IntegrationPipelineViewT): """ Collect the OAuth client credentials from the user. """ - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: if request.method == "POST": form = InstallationForm(request.POST) if form.is_valid(): @@ -173,14 +173,14 @@ def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpRespo ) -class OAuthLoginView(PipelineView[Never]): +class OAuthLoginView(IntegrationPipelineViewT): """ Start the OAuth dance by creating a request token and redirecting the user to approve it. """ @method_decorator(csrf_exempt) - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: with IntegrationPipelineViewEvent( IntegrationPipelineViewType.OAUTH_LOGIN, IntegrationDomain.SOURCE_CODE_MANAGEMENT, @@ -214,14 +214,14 @@ def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpRespo return HttpResponseRedirect(authorize_url) -class OAuthCallbackView(PipelineView[Never]): +class OAuthCallbackView(IntegrationPipelineViewT): """ Complete the OAuth dance by exchanging our request token into an access token. """ @method_decorator(csrf_exempt) - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: with IntegrationPipelineViewEvent( IntegrationPipelineViewType.OAUTH_CALLBACK, IntegrationDomain.SOURCE_CODE_MANAGEMENT, @@ -378,7 +378,7 @@ class BitbucketServerIntegrationProvider(IntegrationProvider): ) setup_dialog_config = {"width": 1030, "height": 1000} - def get_pipeline_views(self) -> list[PipelineView[Never]]: + def get_pipeline_views(self) -> list[IntegrationPipelineViewT]: return [InstallationConfigView(), OAuthLoginView(), OAuthCallbackView()] def post_install( diff --git a/src/sentry/integrations/discord/integration.py b/src/sentry/integrations/discord/integration.py index 4f5a31bac92731..9e73230dd1f59b 100644 --- a/src/sentry/integrations/discord/integration.py +++ b/src/sentry/integrations/discord/integration.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Mapping, Sequence -from typing import Any, Never +from typing import Any from urllib.parse import urlencode from django.http import HttpResponseRedirect @@ -22,10 +22,9 @@ from sentry.integrations.discord.client import DiscordClient from sentry.integrations.discord.types import DiscordPermissions from sentry.integrations.models.integration import Integration +from sentry.integrations.pipeline_types import IntegrationPipelineT, IntegrationPipelineViewT from sentry.integrations.types import IntegrationProviderSlug from sentry.organizations.services.organization.model import RpcOrganization -from sentry.pipeline.base import Pipeline -from sentry.pipeline.views.base import PipelineView from sentry.shared_integrations.exceptions import ApiError, IntegrationError from sentry.utils.http import absolute_uri @@ -149,7 +148,7 @@ def __init__(self) -> None: self.configure_url = absolute_uri("extensions/discord/configure/") super().__init__() - def get_pipeline_views(self) -> Sequence[PipelineView[Never]]: + def get_pipeline_views(self) -> Sequence[IntegrationPipelineViewT]: return [DiscordInstallPipeline(self.get_params_for_oauth())] def build_integration(self, state: Mapping[str, Any]) -> IntegrationData: @@ -283,12 +282,12 @@ def _credentials_exist(self) -> bool: return has_credentials -class DiscordInstallPipeline(PipelineView[Never]): +class DiscordInstallPipeline(IntegrationPipelineViewT): def __init__(self, params): self.params = params super().__init__() - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: if "guild_id" not in request.GET or "code" not in request.GET: state = pipeline.fetch_state(key="discord") or {} redirect_uri = ( diff --git a/src/sentry/integrations/example/integration.py b/src/sentry/integrations/example/integration.py index 55fe02d37a8239..d78ef7725bdad8 100644 --- a/src/sentry/integrations/example/integration.py +++ b/src/sentry/integrations/example/integration.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Mapping, Sequence -from typing import Any, Never +from typing import Any from django.http import HttpResponse from django.http.request import HttpRequest @@ -18,20 +18,20 @@ from sentry.integrations.mixins.issues import IssueSyncIntegration from sentry.integrations.models.external_issue import ExternalIssue from sentry.integrations.models.integration import Integration +from sentry.integrations.pipeline_types import IntegrationPipelineT, IntegrationPipelineViewT from sentry.integrations.services.integration.serial import serialize_integration from sentry.integrations.services.repository.model import RpcRepository from sentry.integrations.source_code_management.issues import SourceCodeIssueIntegration from sentry.integrations.source_code_management.repository import RepositoryIntegration from sentry.models.repository import Repository from sentry.organizations.services.organization.model import RpcOrganization -from sentry.pipeline import Pipeline, PipelineView from sentry.plugins.migrator import Migrator from sentry.shared_integrations.exceptions import IntegrationError from sentry.users.services.user import RpcUser from sentry.users.services.user.service import user_service -class ExampleSetupView(PipelineView[Never]): +class ExampleSetupView(IntegrationPipelineViewT): TEMPLATE = """ """ - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: if "name" in request.POST: pipeline.bind_state("name", request.POST["name"]) return pipeline.next_step() @@ -218,7 +218,7 @@ class ExampleIntegrationProvider(IntegrationProvider): ] ) - def get_pipeline_views(self) -> Sequence[PipelineView[Never]]: + def get_pipeline_views(self) -> Sequence[IntegrationPipelineViewT]: return [ExampleSetupView()] def get_config(self): diff --git a/src/sentry/integrations/github/integration.py b/src/sentry/integrations/github/integration.py index e80a3e62d49583..57fa379953b8bf 100644 --- a/src/sentry/integrations/github/integration.py +++ b/src/sentry/integrations/github/integration.py @@ -4,7 +4,7 @@ import re from collections.abc import Callable, Mapping, Sequence from enum import StrEnum -from typing import Any, Never, TypedDict +from typing import Any, TypedDict from urllib.parse import parse_qsl from django.http import HttpResponse @@ -32,6 +32,7 @@ from sentry.integrations.github.tasks.utils import GithubAPIErrorType from sentry.integrations.models.integration import Integration from sentry.integrations.models.organization_integration import OrganizationIntegration +from sentry.integrations.pipeline_types import IntegrationPipelineT, IntegrationPipelineViewT from sentry.integrations.services.integration import integration_service from sentry.integrations.services.repository import RpcRepository, repository_service from sentry.integrations.source_code_management.commit_context import ( @@ -61,7 +62,6 @@ from sentry.organizations.absolute_url import generate_organization_url from sentry.organizations.services.organization import organization_service from sentry.organizations.services.organization.model import RpcOrganization -from sentry.pipeline import Pipeline, PipelineView from sentry.shared_integrations.constants import ERR_INTERNAL, ERR_UNAUTHORIZED from sentry.shared_integrations.exceptions import ApiError, IntegrationError from sentry.snuba.referrer import Referrer @@ -708,7 +708,7 @@ def post_install( def get_pipeline_views( self, - ) -> Sequence[PipelineView[Never] | Callable[[], PipelineView[Never]]]: + ) -> Sequence[IntegrationPipelineViewT | Callable[[], IntegrationPipelineViewT]]: return [OAuthLoginView(), GithubOrganizationSelection(), GitHubInstallation()] def get_installation_info(self, installation_id: str) -> Mapping[str, Any]: @@ -774,10 +774,10 @@ def record_event(event: IntegrationPipelineViewType): ) -class OAuthLoginView(PipelineView[Never]): +class OAuthLoginView(IntegrationPipelineViewT): client: GithubSetupApiClient - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: with record_event(IntegrationPipelineViewType.OAUTH_LOGIN).capture() as lifecycle: self.active_user_organization = determine_active_organization(request) lifecycle.add_extra( @@ -893,8 +893,8 @@ def _get_eligible_multi_org_installations( ] -class GithubOrganizationSelection(PipelineView[Never]): - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: +class GithubOrganizationSelection(IntegrationPipelineViewT): + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: self.active_user_organization = determine_active_organization(request) has_scm_multi_org = ( features.has( @@ -984,12 +984,12 @@ def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpRespo ) -class GitHubInstallation(PipelineView[Never]): +class GitHubInstallation(IntegrationPipelineViewT): def get_app_url(self) -> str: name = options.get("github-app.name") return f"https://github.com/apps/{slugify(name)}" - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: with record_event(IntegrationPipelineViewType.GITHUB_INSTALLATION).capture() as lifecycle: self.active_user_organization = determine_active_organization(request) diff --git a/src/sentry/integrations/github_enterprise/integration.py b/src/sentry/integrations/github_enterprise/integration.py index 8210dbcea0c1e8..0036023af82f79 100644 --- a/src/sentry/integrations/github_enterprise/integration.py +++ b/src/sentry/integrations/github_enterprise/integration.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Callable, Mapping, Sequence -from typing import Any, Never +from typing import Any from urllib.parse import urlparse from django import forms @@ -23,13 +23,14 @@ from sentry.integrations.github.issues import GitHubIssuesSpec from sentry.integrations.github.utils import get_jwt from sentry.integrations.models.integration import Integration +from sentry.integrations.pipeline_types import IntegrationPipelineT, IntegrationPipelineViewT from sentry.integrations.services.repository.model import RpcRepository from sentry.integrations.source_code_management.commit_context import CommitContextIntegration from sentry.integrations.source_code_management.repository import RepositoryIntegration from sentry.integrations.types import IntegrationProviderSlug from sentry.models.repository import Repository from sentry.organizations.services.organization.model import RpcOrganization -from sentry.pipeline import NestedPipelineView, Pipeline, PipelineView +from sentry.pipeline.views.nested import NestedPipelineView from sentry.shared_integrations.constants import ERR_INTERNAL, ERR_UNAUTHORIZED from sentry.shared_integrations.exceptions import ApiError, IntegrationError from sentry.utils import jwt, metrics @@ -333,8 +334,8 @@ def __init__(self, *args, **kwargs): self.fields["verify_ssl"].initial = True -class InstallationConfigView(PipelineView[Never]): - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: +class InstallationConfigView(IntegrationPipelineViewT): + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: if request.method == "POST": form = InstallationForm(request.POST) if form.is_valid(): @@ -385,7 +386,7 @@ class GitHubEnterpriseIntegrationProvider(GitHubIntegrationProvider): ] ) - def _make_identity_pipeline_view(self): + def _make_identity_pipeline_view(self) -> IntegrationPipelineViewT: """ Make the nested identity provider view. It is important that this view is not constructed until we reach this step and the @@ -411,7 +412,7 @@ def _make_identity_pipeline_view(self): def get_pipeline_views( self, - ) -> Sequence[PipelineView[Never] | Callable[[], PipelineView[Never]]]: + ) -> Sequence[IntegrationPipelineViewT | Callable[[], IntegrationPipelineViewT]]: return ( InstallationConfigView(), GitHubEnterpriseInstallationRedirect(), @@ -516,7 +517,7 @@ def setup(self): ) -class GitHubEnterpriseInstallationRedirect(PipelineView[Never]): +class GitHubEnterpriseInstallationRedirect(IntegrationPipelineViewT): def get_app_url(self, installation_data): if installation_data.get("public_link"): return installation_data["public_link"] @@ -525,7 +526,7 @@ def get_app_url(self, installation_data): name = installation_data.get("name") return f"https://{url}/github-apps/{name}" - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: installation_data = pipeline.fetch_state(key="installation_data") if "installation_id" in request.GET: diff --git a/src/sentry/integrations/gitlab/integration.py b/src/sentry/integrations/gitlab/integration.py index 9a985e1e517138..3d104b411da8f7 100644 --- a/src/sentry/integrations/gitlab/integration.py +++ b/src/sentry/integrations/gitlab/integration.py @@ -2,7 +2,7 @@ import logging from collections.abc import Callable, Mapping, Sequence -from typing import Any, Never +from typing import Any from urllib.parse import urlparse from django import forms @@ -20,6 +20,7 @@ IntegrationMetadata, IntegrationProvider, ) +from sentry.integrations.pipeline_types import IntegrationPipelineT, IntegrationPipelineViewT from sentry.integrations.services.repository.model import RpcRepository from sentry.integrations.source_code_management.commit_context import ( OPEN_PR_MAX_FILES_CHANGED, @@ -39,7 +40,7 @@ from sentry.models.organization import Organization from sentry.models.pullrequest import PullRequest from sentry.models.repository import Repository -from sentry.pipeline import NestedPipelineView, Pipeline, PipelineView +from sentry.pipeline import NestedPipelineView from sentry.shared_integrations.exceptions import ( ApiError, IntegrationError, @@ -553,8 +554,8 @@ def clean_url(self): return self.cleaned_data["url"].rstrip("/") -class InstallationConfigView(PipelineView[Never]): - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: +class InstallationConfigView(IntegrationPipelineViewT): + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: if "goback" in request.GET: pipeline.state.step_index = 0 return pipeline.current_step() @@ -595,8 +596,8 @@ def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpRespo ) -class InstallationGuideView(PipelineView[Never]): - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: +class InstallationGuideView(IntegrationPipelineViewT): + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: if "completed_installation_guide" in request.GET: return pipeline.next_step() return render_to_response( @@ -632,7 +633,7 @@ class GitlabIntegrationProvider(IntegrationProvider): setup_dialog_config = {"width": 1030, "height": 1000} - def _make_identity_pipeline_view(self) -> PipelineView[Never]: + def _make_identity_pipeline_view(self) -> IntegrationPipelineViewT: """ Make the nested identity provider view. It is important that this view is not constructed until we reach this step and the @@ -687,7 +688,7 @@ def get_group_info(self, access_token, installation_data): def get_pipeline_views( self, - ) -> Sequence[PipelineView[Never] | Callable[[], PipelineView[Never]]]: + ) -> Sequence[IntegrationPipelineViewT | Callable[[], IntegrationPipelineViewT]]: return ( InstallationGuideView(), InstallationConfigView(), diff --git a/src/sentry/integrations/jira/integration.py b/src/sentry/integrations/jira/integration.py index e178f33d0a37a4..dbb8b6075e077f 100644 --- a/src/sentry/integrations/jira/integration.py +++ b/src/sentry/integrations/jira/integration.py @@ -4,7 +4,7 @@ import re from collections.abc import Mapping, Sequence from operator import attrgetter -from typing import Any, Never, TypedDict +from typing import Any, TypedDict import sentry_sdk from django.conf import settings @@ -28,13 +28,13 @@ from sentry.integrations.mixins.issues import MAX_CHAR, IssueSyncIntegration, ResolveSyncAction from sentry.integrations.models.external_issue import ExternalIssue from sentry.integrations.models.integration_external_project import IntegrationExternalProject +from sentry.integrations.pipeline_types import IntegrationPipelineViewT from sentry.integrations.services.integration import integration_service from sentry.integrations.types import IntegrationProviderSlug from sentry.issues.grouptype import GroupCategory from sentry.issues.issue_occurrence import IssueOccurrence from sentry.models.group import Group from sentry.organizations.services.organization.service import organization_service -from sentry.pipeline.views.base import PipelineView from sentry.shared_integrations.exceptions import ( ApiError, ApiHostError, @@ -1139,7 +1139,7 @@ class JiraIntegrationProvider(IntegrationProvider): can_add = False - def get_pipeline_views(self) -> list[PipelineView[Never]]: + def get_pipeline_views(self) -> list[IntegrationPipelineViewT]: return [] def build_integration(self, state: Mapping[str, Any]) -> IntegrationData: diff --git a/src/sentry/integrations/jira_server/integration.py b/src/sentry/integrations/jira_server/integration.py index 563d34355196b9..877038e7198627 100644 --- a/src/sentry/integrations/jira_server/integration.py +++ b/src/sentry/integrations/jira_server/integration.py @@ -3,7 +3,7 @@ import logging import re from collections.abc import Mapping, Sequence -from typing import Any, Never, NotRequired, TypedDict +from typing import Any, NotRequired, TypedDict from urllib.parse import urlparse from cryptography.hazmat.backends import default_backend @@ -33,11 +33,11 @@ from sentry.integrations.models.external_actor import ExternalActor from sentry.integrations.models.external_issue import ExternalIssue from sentry.integrations.models.integration_external_project import IntegrationExternalProject +from sentry.integrations.pipeline_types import IntegrationPipelineT, IntegrationPipelineViewT from sentry.integrations.services.integration import integration_service from sentry.integrations.types import ExternalProviders, IntegrationProviderSlug from sentry.models.group import Group from sentry.organizations.services.organization.service import organization_service -from sentry.pipeline import Pipeline, PipelineView from sentry.shared_integrations.exceptions import ( ApiError, ApiHostError, @@ -211,12 +211,12 @@ def clean_consumer_key(self): return data -class InstallationConfigView(PipelineView[Never]): +class InstallationConfigView(IntegrationPipelineViewT): """ Collect the OAuth client credentials from the user. """ - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: if request.method == "POST": form = InstallationForm(request.POST) if form.is_valid(): @@ -234,14 +234,14 @@ def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpRespo ) -class OAuthLoginView(PipelineView[Never]): +class OAuthLoginView(IntegrationPipelineViewT): """ Start the OAuth dance by creating a request token and redirecting the user to approve it. """ @method_decorator(csrf_exempt) - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: if "oauth_token" in request.GET: return pipeline.next_step() @@ -277,14 +277,14 @@ def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpRespo return HttpResponseRedirect(authorize_url) -class OAuthCallbackView(PipelineView[Never]): +class OAuthCallbackView(IntegrationPipelineViewT): """ Complete the OAuth dance by exchanging our request token into an access token. """ @method_decorator(csrf_exempt) - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: config = pipeline.fetch_state("installation_data") if config is None: return pipeline.error("Missing installation_data") @@ -1389,7 +1389,7 @@ class JiraServerIntegrationProvider(IntegrationProvider): setup_dialog_config = {"width": 1030, "height": 1000} - def get_pipeline_views(self) -> list[PipelineView[Never]]: + def get_pipeline_views(self) -> list[IntegrationPipelineViewT]: return [InstallationConfigView(), OAuthLoginView(), OAuthCallbackView()] def build_integration(self, state: Mapping[str, Any]) -> IntegrationData: diff --git a/src/sentry/integrations/msteams/integration.py b/src/sentry/integrations/msteams/integration.py index 555a9e82340015..e8d5f6d46cafa5 100644 --- a/src/sentry/integrations/msteams/integration.py +++ b/src/sentry/integrations/msteams/integration.py @@ -2,7 +2,7 @@ import logging from collections.abc import Mapping, Sequence -from typing import Any, Never +from typing import Any from django.http.request import HttpRequest from django.http.response import HttpResponseBase @@ -18,9 +18,9 @@ IntegrationProvider, ) from sentry.integrations.models.integration import Integration +from sentry.integrations.pipeline_types import IntegrationPipelineT, IntegrationPipelineViewT from sentry.integrations.types import IntegrationProviderSlug from sentry.organizations.services.organization.model import RpcOrganization -from sentry.pipeline import Pipeline, PipelineView from .card_builder.installation import ( build_personal_installation_confirmation_message, @@ -87,7 +87,7 @@ class MsTeamsIntegrationProvider(IntegrationProvider): integration_cls = MsTeamsIntegration features = frozenset([IntegrationFeatures.CHAT_UNFURL, IntegrationFeatures.ALERT_RULE]) - def get_pipeline_views(self) -> Sequence[PipelineView[Never]]: + def get_pipeline_views(self) -> Sequence[IntegrationPipelineViewT]: return [MsTeamsPipelineView()] def build_integration(self, state: Mapping[str, Any]) -> IntegrationData: @@ -137,6 +137,6 @@ def post_install( client.send_card(conversation_id, card) -class MsTeamsPipelineView(PipelineView[Never]): - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: +class MsTeamsPipelineView(IntegrationPipelineViewT): + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: return pipeline.next_step() diff --git a/src/sentry/integrations/opsgenie/integration.py b/src/sentry/integrations/opsgenie/integration.py index 2390e613900f88..8523e12dfe76ad 100644 --- a/src/sentry/integrations/opsgenie/integration.py +++ b/src/sentry/integrations/opsgenie/integration.py @@ -2,7 +2,7 @@ import logging from collections.abc import Mapping, MutableMapping, Sequence -from typing import Any, Never +from typing import Any from django import forms from django.http.request import HttpRequest @@ -24,9 +24,9 @@ from sentry.integrations.on_call.metrics import OnCallIntegrationsHaltReason, OnCallInteractionType from sentry.integrations.opsgenie.metrics import record_event from sentry.integrations.opsgenie.tasks import migrate_opsgenie_plugin +from sentry.integrations.pipeline_types import IntegrationPipelineT, IntegrationPipelineViewT from sentry.integrations.types import IntegrationProviderSlug from sentry.organizations.services.organization.model import RpcOrganization -from sentry.pipeline import Pipeline, PipelineView from sentry.shared_integrations.exceptions import ( ApiError, ApiRateLimitedError, @@ -103,8 +103,8 @@ class InstallationForm(forms.Form): ) -class InstallationConfigView(PipelineView[Never]): - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: +class InstallationConfigView(IntegrationPipelineViewT): + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: if request.method == "POST": form = InstallationForm(request.POST) if form.is_valid(): @@ -243,7 +243,7 @@ class OpsgenieIntegrationProvider(IntegrationProvider): ] ) - def get_pipeline_views(self) -> Sequence[PipelineView[Never]]: + def get_pipeline_views(self) -> Sequence[IntegrationPipelineViewT]: return [InstallationConfigView()] def build_integration(self, state: Mapping[str, Any]) -> IntegrationData: diff --git a/src/sentry/integrations/pagerduty/integration.py b/src/sentry/integrations/pagerduty/integration.py index 022ad525aaae58..2528e19075bed8 100644 --- a/src/sentry/integrations/pagerduty/integration.py +++ b/src/sentry/integrations/pagerduty/integration.py @@ -2,7 +2,7 @@ import logging from collections.abc import Mapping, Sequence -from typing import Any, Never +from typing import Any import orjson from django.db import router, transaction @@ -24,9 +24,9 @@ from sentry.integrations.models.organization_integration import OrganizationIntegration from sentry.integrations.on_call.metrics import OnCallInteractionType from sentry.integrations.pagerduty.metrics import record_event +from sentry.integrations.pipeline_types import IntegrationPipelineT, IntegrationPipelineViewT from sentry.integrations.types import IntegrationProviderSlug from sentry.organizations.services.organization.model import RpcOrganization -from sentry.pipeline import Pipeline, PipelineView from sentry.shared_integrations.exceptions import IntegrationError from sentry.utils.http import absolute_uri @@ -176,7 +176,7 @@ class PagerDutyIntegrationProvider(IntegrationProvider): setup_dialog_config = {"width": 600, "height": 900} - def get_pipeline_views(self) -> Sequence[PipelineView[Never]]: + def get_pipeline_views(self) -> Sequence[IntegrationPipelineViewT]: return [PagerDutyInstallationRedirect()] def post_install( @@ -218,7 +218,7 @@ def build_integration(self, state: Mapping[str, Any]) -> IntegrationData: } -class PagerDutyInstallationRedirect(PipelineView[Never]): +class PagerDutyInstallationRedirect(IntegrationPipelineViewT): def get_app_url(self, account_name=None): if not account_name: account_name = "app" @@ -228,7 +228,7 @@ def get_app_url(self, account_name=None): return f"https://{account_name}.pagerduty.com/install/integration?app_id={app_id}&redirect_url={setup_url}&version=2" - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: if "config" in request.GET: pipeline.bind_state("config", request.GET["config"]) return pipeline.next_step() diff --git a/src/sentry/integrations/pipeline.py b/src/sentry/integrations/pipeline.py index a1720777049a6a..f95f9ccb58598b 100644 --- a/src/sentry/integrations/pipeline.py +++ b/src/sentry/integrations/pipeline.py @@ -1,7 +1,7 @@ from __future__ import annotations import logging -from typing import Any, Never, TypedDict +from typing import Any, TypedDict from django.db import IntegrityError from django.http import HttpResponseRedirect @@ -16,10 +16,11 @@ from sentry.integrations.manager import default_manager from sentry.integrations.models.integration import Integration from sentry.integrations.models.organization_integration import OrganizationIntegration +from sentry.integrations.pipeline_types import IntegrationPipelineT from sentry.models.organizationmapping import OrganizationMapping from sentry.organizations.absolute_url import generate_organization_url from sentry.organizations.services.organization import organization_service -from sentry.pipeline import Pipeline, PipelineAnalyticsEntry +from sentry.pipeline import PipelineAnalyticsEntry from sentry.shared_integrations.exceptions import IntegrationError, IntegrationProviderError from sentry.silo.base import SiloMode from sentry.users.models.identity import Identity, IdentityProvider, IdentityStatus @@ -88,7 +89,7 @@ def is_violating_region_restriction(organization_id: int, integration_id: int): return mapping.region_name not in region_names -class IntegrationPipeline(Pipeline[Never]): +class IntegrationPipeline(IntegrationPipelineT): pipeline_name = "integration_pipeline" provider_manager = default_manager diff --git a/src/sentry/integrations/pipeline_types.py b/src/sentry/integrations/pipeline_types.py new file mode 100644 index 00000000000000..99eb7e5276d0ef --- /dev/null +++ b/src/sentry/integrations/pipeline_types.py @@ -0,0 +1,10 @@ +from typing import Never, TypeAlias + +from sentry.pipeline.base import Pipeline +from sentry.pipeline.provider import PipelineProvider +from sentry.pipeline.store import PipelineSessionStore +from sentry.pipeline.views.base import PipelineView + +IntegrationPipelineT: TypeAlias = Pipeline[Never, PipelineSessionStore] +IntegrationPipelineProviderT: TypeAlias = PipelineProvider[Never, PipelineSessionStore] +IntegrationPipelineViewT: TypeAlias = PipelineView[Never, PipelineSessionStore] diff --git a/src/sentry/integrations/slack/integration.py b/src/sentry/integrations/slack/integration.py index 8bee274f06a43f..0a95271537cd53 100644 --- a/src/sentry/integrations/slack/integration.py +++ b/src/sentry/integrations/slack/integration.py @@ -3,7 +3,7 @@ import logging from collections import namedtuple from collections.abc import Mapping, Sequence -from typing import Any, Never +from typing import Any from django.utils.translation import gettext_lazy as _ from slack_sdk import WebClient @@ -20,6 +20,7 @@ ) from sentry.integrations.mixins import NotifyBasicMixin from sentry.integrations.models.integration import Integration +from sentry.integrations.pipeline_types import IntegrationPipelineViewT from sentry.integrations.slack.metrics import ( SLACK_NOTIFY_MIXIN_FAILURE_DATADOG_METRIC, SLACK_NOTIFY_MIXIN_SUCCESS_DATADOG_METRIC, @@ -29,7 +30,6 @@ from sentry.integrations.types import IntegrationProviderSlug from sentry.organizations.services.organization.model import RpcOrganization from sentry.pipeline import NestedPipelineView -from sentry.pipeline.views.base import PipelineView from sentry.shared_integrations.exceptions import IntegrationError from sentry.utils import metrics from sentry.utils.http import absolute_uri @@ -134,21 +134,20 @@ class SlackIntegrationProvider(IntegrationProvider): setup_dialog_config = {"width": 600, "height": 900} - def get_pipeline_views(self) -> Sequence[PipelineView[Never]]: - identity_pipeline_config = { - "oauth_scopes": self.identity_oauth_scopes, - "user_scopes": self.user_scopes, - "redirect_url": absolute_uri("/extensions/slack/setup/"), - } + def _identity_pipeline_view(self) -> IntegrationPipelineViewT: + return NestedPipelineView( + bind_key="identity", + provider_key="slack", + pipeline_cls=IdentityProviderPipeline, + config={ + "oauth_scopes": self.identity_oauth_scopes, + "user_scopes": self.user_scopes, + "redirect_url": absolute_uri("/extensions/slack/setup/"), + }, + ) - return [ - NestedPipelineView( - bind_key="identity", - provider_key="slack", - pipeline_cls=IdentityProviderPipeline, - config=identity_pipeline_config, - ) - ] + def get_pipeline_views(self) -> Sequence[IntegrationPipelineViewT]: + return [self._identity_pipeline_view()] def _get_team_info(self, access_token: str) -> Any: # Manually add authorization since this method is part of slack installation diff --git a/src/sentry/integrations/vercel/integration.py b/src/sentry/integrations/vercel/integration.py index edc972bdd95424..22c7db4c5c3bfb 100644 --- a/src/sentry/integrations/vercel/integration.py +++ b/src/sentry/integrations/vercel/integration.py @@ -2,7 +2,7 @@ import logging from collections.abc import Mapping, Sequence -from typing import Any, Never, TypedDict +from typing import Any, TypedDict from urllib.parse import urlencode import sentry_sdk @@ -21,10 +21,10 @@ IntegrationProvider, ) from sentry.integrations.models.integration import Integration +from sentry.integrations.pipeline_types import IntegrationPipelineViewT from sentry.integrations.services.integration import integration_service from sentry.organizations.services.organization.model import RpcOrganization from sentry.pipeline import NestedPipelineView -from sentry.pipeline.views.base import PipelineView from sentry.projects.services.project.model import RpcProject from sentry.projects.services.project_key import project_key_service from sentry.sentry_apps.logic import SentryAppCreator @@ -366,15 +366,16 @@ class VercelIntegrationProvider(IntegrationProvider): # feature flag handler is in getsentry requires_feature_flag = True - def get_pipeline_views(self) -> Sequence[PipelineView[Never]]: - return [ - NestedPipelineView( - bind_key="identity", - provider_key=self.key, - pipeline_cls=IdentityProviderPipeline, - config={"redirect_url": absolute_uri(self.oauth_redirect_url)}, - ) - ] + def _identity_pipeline_view(self) -> IntegrationPipelineViewT: + return NestedPipelineView( + bind_key="identity", + provider_key=self.key, + pipeline_cls=IdentityProviderPipeline, + config={"redirect_url": absolute_uri(self.oauth_redirect_url)}, + ) + + def get_pipeline_views(self) -> Sequence[IntegrationPipelineViewT]: + return [self._identity_pipeline_view()] def build_integration(self, state: Mapping[str, Any]) -> IntegrationData: data = state["identity"]["data"] diff --git a/src/sentry/integrations/vsts/integration.py b/src/sentry/integrations/vsts/integration.py index 5147eb9ae0dc84..5d12e1f7fcec20 100644 --- a/src/sentry/integrations/vsts/integration.py +++ b/src/sentry/integrations/vsts/integration.py @@ -4,7 +4,7 @@ import re from collections.abc import Mapping, MutableMapping, Sequence from time import time -from typing import Any, Never +from typing import Any from urllib.parse import parse_qs, quote, unquote, urlencode, urlparse from django import forms @@ -29,6 +29,7 @@ from sentry.integrations.models.integration import Integration as IntegrationModel from sentry.integrations.models.integration_external_project import IntegrationExternalProject from sentry.integrations.models.organization_integration import OrganizationIntegration +from sentry.integrations.pipeline_types import IntegrationPipelineT, IntegrationPipelineViewT from sentry.integrations.services.integration import integration_service from sentry.integrations.services.repository import RpcRepository, repository_service from sentry.integrations.source_code_management.repository import RepositoryIntegration @@ -43,7 +44,7 @@ from sentry.models.apitoken import generate_token from sentry.models.repository import Repository from sentry.organizations.services.organization.model import RpcOrganization -from sentry.pipeline import NestedPipelineView, Pipeline, PipelineView +from sentry.pipeline import NestedPipelineView from sentry.shared_integrations.exceptions import ( ApiError, IntegrationError, @@ -465,7 +466,7 @@ def get_scopes(self) -> Sequence[str]: ) return ("vso.code", "vso.graph", "vso.serviceendpoint_manage", "vso.work_write") - def get_pipeline_views(self) -> Sequence[PipelineView[Never]]: + def get_pipeline_views(self) -> Sequence[IntegrationPipelineViewT]: identity_pipeline_config = { "redirect_url": absolute_uri(self.oauth_redirect_url), "oauth_scopes": self.get_scopes(), @@ -671,8 +672,8 @@ def setup(self) -> None: ) -class AccountConfigView(PipelineView[Never]): - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: +class AccountConfigView(IntegrationPipelineViewT): + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: with IntegrationPipelineViewEvent( IntegrationPipelineViewType.ACCOUNT_CONFIG, IntegrationDomain.SOURCE_CODE_MANAGEMENT, diff --git a/src/sentry/integrations/vsts_extension/integration.py b/src/sentry/integrations/vsts_extension/integration.py index e092a3bd21d93b..d552d60e41ad90 100644 --- a/src/sentry/integrations/vsts_extension/integration.py +++ b/src/sentry/integrations/vsts_extension/integration.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Never +from typing import Any from django.contrib import messages from django.http import HttpResponseRedirect @@ -7,8 +7,8 @@ from django.http.response import HttpResponseBase from sentry.integrations.base import IntegrationData +from sentry.integrations.pipeline_types import IntegrationPipelineT, IntegrationPipelineViewT from sentry.integrations.vsts.integration import AccountConfigView, VstsIntegrationProvider -from sentry.pipeline import Pipeline, PipelineView from sentry.utils.http import absolute_uri @@ -20,7 +20,7 @@ class VstsExtensionIntegrationProvider(VstsIntegrationProvider): # want it to actually appear of the Integrations page. visible = False - def get_pipeline_views(self) -> list[PipelineView[Never]]: + def get_pipeline_views(self) -> list[IntegrationPipelineViewT]: views = super().get_pipeline_views() views = [view for view in views if not isinstance(view, AccountConfigView)] views.append(VstsExtensionFinishedView()) @@ -38,8 +38,8 @@ def build_integration(self, state: Mapping[str, Any]) -> IntegrationData: ) -class VstsExtensionFinishedView(PipelineView[Never]): - def dispatch(self, request: HttpRequest, pipeline: Pipeline[Never]) -> HttpResponseBase: +class VstsExtensionFinishedView(IntegrationPipelineViewT): + def dispatch(self, request: HttpRequest, pipeline: IntegrationPipelineT) -> HttpResponseBase: response = pipeline.finish_pipeline() integration = getattr(pipeline, "integration", None) diff --git a/src/sentry/pipeline/base.py b/src/sentry/pipeline/base.py index b0dd6682eee93b..ae7ab11394195c 100644 --- a/src/sentry/pipeline/base.py +++ b/src/sentry/pipeline/base.py @@ -27,7 +27,7 @@ ERR_MISMATCHED_USER = "Current user does not match user that started the pipeline." -class Pipeline[M: Model](abc.ABC): +class Pipeline[M: Model, S: PipelineSessionStore](abc.ABC): """ Pipeline provides a mechanism to guide the user through a request 'pipeline', where each view may be completed by calling the ``next_step`` @@ -56,7 +56,7 @@ class Pipeline[M: Model](abc.ABC): pipeline_name: str provider_manager: Any provider_model_cls: type[M] | None = None - session_store_cls = PipelineSessionStore + session_store_cls: type[S] = PipelineSessionStore # type: ignore[assignment] # python/mypy#18812 @classmethod def get_for_request(cls, request: HttpRequest) -> Self | None: @@ -74,7 +74,7 @@ def get_for_request(cls, request: HttpRequest) -> Self | None: ) @classmethod - def unpack_state(cls, request: HttpRequest) -> PipelineRequestState[M] | None: + def unpack_state(cls, request: HttpRequest) -> PipelineRequestState[M, S] | None: state = cls.session_store_cls(request, cls.pipeline_name, ttl=PIPELINE_STATE_TTL) if not state.is_valid(): return None @@ -98,7 +98,7 @@ def unpack_state(cls, request: HttpRequest) -> PipelineRequestState[M] | None: def get_provider( self, provider_key: str, *, organization: RpcOrganization | None - ) -> PipelineProvider[M]: + ) -> PipelineProvider[M, S]: return self.provider_manager.get(provider_key) def __init__( @@ -106,7 +106,7 @@ def __init__( request: HttpRequest, provider_key: str, organization: Organization | RpcOrganization | None = None, - provider_model: Model | None = None, + provider_model: M | None = None, config: Mapping[str, Any] | None = None, ) -> None: if organization: @@ -134,7 +134,7 @@ def __init__( pipe_ids = [f"{type(v).__module__}.{type(v).__name__}" for v in self.pipeline_views] self.signature = md5_text(*pipe_ids).hexdigest() - def get_pipeline_views(self) -> Sequence[PipelineView[M] | Callable[[], PipelineView[M]]]: + def get_pipeline_views(self) -> Sequence[PipelineView[M, S] | Callable[[], PipelineView[M, S]]]: """ Retrieve the pipeline views from the provider. @@ -189,7 +189,7 @@ def current_step(self) -> HttpResponseBase: return self.dispatch_to(step) - def dispatch_to(self, step: PipelineView[M]) -> HttpResponseBase: + def dispatch_to(self, step: PipelineView[M, S]) -> HttpResponseBase: """ Dispatch to a view expected by this pipeline. diff --git a/src/sentry/pipeline/provider.py b/src/sentry/pipeline/provider.py index 3f28d7ada44139..78cf0c74342beb 100644 --- a/src/sentry/pipeline/provider.py +++ b/src/sentry/pipeline/provider.py @@ -5,13 +5,14 @@ from typing import TYPE_CHECKING, Any from sentry.db.models.base import Model +from sentry.pipeline.store import PipelineSessionStore if TYPE_CHECKING: from sentry.pipeline import Pipeline from sentry.pipeline.views.base import PipelineView -class PipelineProvider[M: Model](abc.ABC): +class PipelineProvider[M: Model, S: PipelineSessionStore](abc.ABC): """ A class implementing the PipelineProvider interface provides the pipeline views that the Pipeline will traverse through. @@ -33,7 +34,7 @@ def name(self) -> str: """A human readable name (e.g. 'Slack').""" @abc.abstractmethod - def get_pipeline_views(self) -> Sequence[PipelineView[M] | Callable[[], PipelineView[M]]]: + def get_pipeline_views(self) -> Sequence[PipelineView[M, S] | Callable[[], PipelineView[M, S]]]: """ Returns a list of instantiated views which implement the PipelineView interface. Each view will be dispatched in order. @@ -48,7 +49,7 @@ def update_config(self, config: Mapping[str, Any]) -> None: """ self.config.update(config) - def set_pipeline(self, pipeline: Pipeline[M]) -> None: + def set_pipeline(self, pipeline: Pipeline[M, S]) -> None: """ Used by the pipeline to give the provider access to the executing pipeline. """ diff --git a/src/sentry/pipeline/types.py b/src/sentry/pipeline/types.py index 544b8090dea964..8a3e86a473c03b 100644 --- a/src/sentry/pipeline/types.py +++ b/src/sentry/pipeline/types.py @@ -9,10 +9,10 @@ @dataclass -class PipelineRequestState[M: Model]: +class PipelineRequestState[M: Model, S: PipelineSessionStore]: """Initial pipeline attributes from a request.""" - state: PipelineSessionStore + state: S provider_model: M | None organization: RpcOrganization | None provider_key: str diff --git a/src/sentry/pipeline/views/base.py b/src/sentry/pipeline/views/base.py index f4adc2eec645c4..9ce4b92ebfca2f 100644 --- a/src/sentry/pipeline/views/base.py +++ b/src/sentry/pipeline/views/base.py @@ -8,6 +8,7 @@ from django.http.response import HttpResponseBase from sentry.db.models.base import Model +from sentry.pipeline.store import PipelineSessionStore from sentry.utils import json from sentry.web.helpers import render_to_response @@ -15,14 +16,14 @@ from sentry.pipeline.base import Pipeline -class PipelineView[M: Model](abc.ABC): +class PipelineView[M: Model, S: PipelineSessionStore](abc.ABC): """ A class implementing the PipelineView may be used in a PipelineProviders get_pipeline_views list. """ @abc.abstractmethod - def dispatch(self, request: HttpRequest, pipeline: Pipeline[M]) -> HttpResponseBase: + def dispatch(self, request: HttpRequest, pipeline: Pipeline[M, S]) -> HttpResponseBase: """ Called on request, the active pipeline is passed in which can and should be used to bind data and traverse the pipeline. diff --git a/src/sentry/pipeline/views/nested.py b/src/sentry/pipeline/views/nested.py index d9df14d97157b0..2ffb707b0687a3 100644 --- a/src/sentry/pipeline/views/nested.py +++ b/src/sentry/pipeline/views/nested.py @@ -7,13 +7,16 @@ from django.http.response import HttpResponseBase from sentry.db.models.base import Model +from sentry.pipeline.store import PipelineSessionStore from sentry.pipeline.views.base import PipelineView if TYPE_CHECKING: from sentry.pipeline import Pipeline -class NestedPipelineView[M: Model, U: Model](PipelineView[M]): +class NestedPipelineView[M: Model, S: PipelineSessionStore, U: Model, V: PipelineSessionStore]( + PipelineView[M, S] +): """ A NestedPipelineView can be used within other pipelines to process another pipeline within a pipeline. Note that the nested pipelines finish_pipeline @@ -26,7 +29,7 @@ class NestedPipelineView[M: Model, U: Model](PipelineView[M]): def __init__( self, bind_key: str, - pipeline_cls: type[Pipeline[U]], + pipeline_cls: type[Pipeline[U, V]], provider_key: str, config: Mapping[str, Any] | None = None, ) -> None: @@ -35,7 +38,7 @@ def __init__( self.config = config or {} class NestedPipeline(pipeline_cls): # type: ignore[misc, valid-type] - def set_parent_pipeline(self, parent_pipeline: Pipeline[M]) -> None: + def set_parent_pipeline(self, parent_pipeline: Pipeline[M, S]) -> None: self.parent_pipeline = parent_pipeline def finish_pipeline(self) -> HttpResponseBase: @@ -46,7 +49,7 @@ def finish_pipeline(self) -> HttpResponseBase: self.pipeline_cls = NestedPipeline - def dispatch(self, request: HttpRequest, pipeline: Pipeline[M]) -> HttpResponseBase: + def dispatch(self, request: HttpRequest, pipeline: Pipeline[M, S]) -> HttpResponseBase: nested_pipeline = self.pipeline_cls( organization=pipeline.organization, request=request, diff --git a/src/sentry/users/api/serializers/user_identity_config.py b/src/sentry/users/api/serializers/user_identity_config.py index aba66d4167c69f..35abb72fa05269 100644 --- a/src/sentry/users/api/serializers/user_identity_config.py +++ b/src/sentry/users/api/serializers/user_identity_config.py @@ -11,7 +11,6 @@ from sentry.api.serializers import Serializer, register, serialize from sentry.api.serializers.models.user_social_auth import get_provider_label -from sentry.db.models.base import Model as SentryModel from sentry.exceptions import NotRegistered from sentry.hybridcloud.services.organization_mapping import organization_mapping_service from sentry.identity import is_login_provider @@ -54,7 +53,7 @@ class UserIdentityProvider: name: str @classmethod - def adapt[M: SentryModel](cls, provider: PipelineProvider[M]) -> Self: + def adapt(cls, provider: PipelineProvider[Any, Any]) -> Self: return cls(provider.key, provider.name) @@ -93,7 +92,7 @@ def base(**kwargs: Any) -> UserIdentityConfig: is_login=False, ) elif isinstance(identity, Identity): - provider: PipelineProvider[Any] + provider: PipelineProvider[Any, Any] try: provider = identity.get_provider() except NotRegistered: diff --git a/src/sentry/web/frontend/auth_organization_login.py b/src/sentry/web/frontend/auth_organization_login.py index 3513e58cf55a59..15d53c77d4c7bb 100644 --- a/src/sentry/web/frontend/auth_organization_login.py +++ b/src/sentry/web/frontend/auth_organization_login.py @@ -9,6 +9,7 @@ from rest_framework.request import Request from sentry.auth.helper import AuthHelper +from sentry.auth.store import FLOW_LOGIN from sentry.constants import WARN_SESSION_EXPIRED from sentry.models.authprovider import AuthProvider from sentry.organizations.services.organization import RpcOrganization, organization_service @@ -28,7 +29,7 @@ def handle_sso(self, request: HttpRequest, organization: RpcOrganization, auth_p request=request, organization=organization, auth_provider=auth_provider, - flow=AuthHelper.FLOW_LOGIN, + flow=FLOW_LOGIN, referrer=request.GET.get( "referrer" ), # TODO: get referrer from the form submit - not the query parms diff --git a/src/sentry/web/frontend/organization_auth_settings.py b/src/sentry/web/frontend/organization_auth_settings.py index 58422564cd976e..c8c88ad4a72bb4 100644 --- a/src/sentry/web/frontend/organization_auth_settings.py +++ b/src/sentry/web/frontend/organization_auth_settings.py @@ -13,6 +13,7 @@ from sentry.auth import manager from sentry.auth.helper import AuthHelper from sentry.auth.services.auth import RpcAuthProvider, auth_service +from sentry.auth.store import FLOW_SETUP_PROVIDER from sentry.models.authprovider import AuthProvider from sentry.models.organization import Organization from sentry.organizations.services.organization import RpcOrganization, organization_service @@ -244,7 +245,7 @@ def handle(self, request: HttpRequest, organization: RpcOrganization) -> HttpRes request=request, # this has all our form data organization=organization, provider_key=provider_key, # okta, google, onelogin, etc - flow=AuthHelper.FLOW_SETUP_PROVIDER, + flow=FLOW_SETUP_PROVIDER, ) feature = helper.provider.required_feature diff --git a/tests/sentry/auth/test_helper.py b/tests/sentry/auth/test_helper.py index a6a216faf94f6e..89c720205e5184 100644 --- a/tests/sentry/auth/test_helper.py +++ b/tests/sentry/auth/test_helper.py @@ -8,13 +8,9 @@ from django.test import Client, RequestFactory from sentry import audit_log -from sentry.auth.helper import ( - OK_LINK_IDENTITY, - AuthHelper, - AuthHelperSessionStore, - AuthIdentityHandler, -) +from sentry.auth.helper import OK_LINK_IDENTITY, AuthHelper, AuthIdentityHandler from sentry.auth.providers.dummy import DummyProvider +from sentry.auth.store import FLOW_LOGIN, FLOW_SETUP_PROVIDER, AuthHelperSessionStore from sentry.hybridcloud.models.outbox import outbox_context from sentry.models.auditlogentry import AuditLogEntry from sentry.models.authidentity import AuthIdentity @@ -490,17 +486,17 @@ def _test_pipeline(self, flow, referrer=None): @mock.patch("sentry.auth.helper.messages") def test_login(self, mock_messages): - final_step = self._test_pipeline(AuthHelper.FLOW_LOGIN) + final_step = self._test_pipeline(FLOW_LOGIN) assert final_step.url == f"/auth/login/{self.organization.slug}/" @mock.patch("sentry.auth.helper.messages") def test_setup_provider(self, mock_messages): - final_step = self._test_pipeline(AuthHelper.FLOW_SETUP_PROVIDER) + final_step = self._test_pipeline(FLOW_SETUP_PROVIDER) assert final_step.url == f"/settings/{self.organization.slug}/auth/" @mock.patch("sentry.auth.helper.messages") def test_referrer_state(self, mock_messages): - final_step = self._test_pipeline(flow=AuthHelper.FLOW_SETUP_PROVIDER, referrer="foobar") + final_step = self._test_pipeline(flow=FLOW_SETUP_PROVIDER, referrer="foobar") assert final_step.url == f"/settings/{self.organization.slug}/auth/" diff --git a/tests/sentry/pipeline/test_pipeline.py b/tests/sentry/pipeline/test_pipeline.py index 7f4050279e8bb6..77dff3f9172132 100644 --- a/tests/sentry/pipeline/test_pipeline.py +++ b/tests/sentry/pipeline/test_pipeline.py @@ -8,27 +8,28 @@ from sentry.organizations.services.organization.serial import serialize_rpc_organization from sentry.pipeline import Pipeline, PipelineProvider, PipelineView from sentry.pipeline.base import ERR_MISMATCHED_USER +from sentry.pipeline.store import PipelineSessionStore from sentry.silo.base import SiloMode from sentry.testutils.cases import TestCase from sentry.testutils.silo import assume_test_silo_mode, control_silo_test -class PipelineStep(PipelineView[Never]): +class PipelineStep(PipelineView[Never, PipelineSessionStore]): def dispatch(self, request, pipeline): pipeline.dispatch_count += 1 pipeline.bind_state("some_state", "value") -class DummyProvider(PipelineProvider[Never]): +class DummyProvider(PipelineProvider[Never, PipelineSessionStore]): key = "dummy" name = "dummy" pipeline_views: list[PipelineStep] = [PipelineStep(), PipelineStep()] - def get_pipeline_views(self) -> Sequence[PipelineView[Never]]: + def get_pipeline_views(self) -> Sequence[PipelineView[Never, PipelineSessionStore]]: return self.pipeline_views -class DummyPipeline(Pipeline[Never]): +class DummyPipeline(Pipeline[Never, PipelineSessionStore]): pipeline_name = "test_pipeline" # Simplify tests, the manager can just be a dict. diff --git a/tests/sentry/web/frontend/test_auth_saml2.py b/tests/sentry/web/frontend/test_auth_saml2.py index 0a73e7987f19f2..15a94537889228 100644 --- a/tests/sentry/web/frontend/test_auth_saml2.py +++ b/tests/sentry/web/frontend/test_auth_saml2.py @@ -10,9 +10,9 @@ from sentry import audit_log from sentry.auth.authenticators.totp import TotpInterface -from sentry.auth.helper import AuthHelperSessionStore from sentry.auth.providers.saml2.generic.provider import GenericSAML2Provider from sentry.auth.providers.saml2.provider import Attributes +from sentry.auth.store import AuthHelperSessionStore from sentry.auth.view import AuthView from sentry.models.auditlogentry import AuditLogEntry from sentry.models.authidentity import AuthIdentity