Skip to content

Commit

Permalink
ref(integration): Extract base class for identity linking (#74744)
Browse files Browse the repository at this point in the history
Extract common code for the "link identity" and "unlink identity"
operations on messaging integrations.
  • Loading branch information
RyanSkonnord authored Aug 26, 2024
1 parent da3b048 commit 5771cda
Show file tree
Hide file tree
Showing 22 changed files with 621 additions and 498 deletions.
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,6 @@ module = [
"sentry.integrations.msteams.actions.form",
"sentry.integrations.msteams.client",
"sentry.integrations.msteams.integration",
"sentry.integrations.msteams.link_identity",
"sentry.integrations.msteams.notifications",
"sentry.integrations.msteams.webhook",
"sentry.integrations.notifications",
Expand Down
2 changes: 0 additions & 2 deletions src/sentry/integrations/discord/views/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +0,0 @@
from .link_identity import * # noqa: F401,F403
from .unlink_identity import * # noqa: F401,F403
64 changes: 15 additions & 49 deletions src/sentry/integrations/discord/views/link_identity.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
from django.core.signing import BadSignature, SignatureExpired
from django.http import HttpRequest, HttpResponse
from collections.abc import Mapping
from typing import Any

from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views.decorators.cache import never_cache

from sentry import analytics
from sentry.integrations.discord.views.linkage import DiscordIdentityLinkageView
from sentry.integrations.messaging import LinkIdentityView
from sentry.integrations.models.integration import Integration
from sentry.integrations.services.integration.model import RpcIntegration
from sentry.integrations.types import ExternalProviders
from sentry.integrations.utils.identities import get_identity_or_404
from sentry.types.actor import ActorType
from sentry.users.models.identity import Identity
from sentry.utils.http import absolute_uri
from sentry.utils.signing import sign, unsign
from sentry.web.frontend.base import BaseView, control_silo_view
from sentry.web.helpers import render_to_response
from sentry.utils.signing import sign

from .constants import SALT

Expand All @@ -27,41 +22,12 @@ def build_linking_url(integration: RpcIntegration, discord_id: str) -> str:
return absolute_uri(reverse(endpoint, kwargs={"signed_params": sign(salt=SALT, **kwargs)}))


@control_silo_view
class DiscordLinkIdentityView(BaseView):
"""
Django view for linking user to Discord account.
"""

@method_decorator(never_cache)
def handle(self, request: HttpRequest, signed_params: str) -> HttpResponse:
try:
params = unsign(signed_params, salt=SALT)
except (SignatureExpired, BadSignature):
return render_to_response("sentry/integrations/discord/expired-link.html")

organization, integration, idp = get_identity_or_404(
ExternalProviders.DISCORD,
request.user,
integration_id=params["integration_id"],
)

if request.method != "POST":
return render_to_response(
"sentry/auth-link-identity.html",
request=request,
context={"organization": organization, "provider": integration.get_provider()},
)

Identity.objects.link_identity(user=request.user, idp=idp, external_id=params["discord_id"]) # type: ignore[arg-type]
class DiscordLinkIdentityView(DiscordIdentityLinkageView, LinkIdentityView):
def get_success_template_and_context(
self, params: Mapping[str, Any], integration: Integration | None
) -> tuple[str, dict[str, Any]]:
return "sentry/integrations/discord/linked.html", {}

analytics.record(
"integrations.discord.identity_linked",
provider="discord",
actor_id=request.user.id,
actor_type=ActorType.USER,
)
return render_to_response(
"sentry/integrations/discord/linked.html",
request=request,
)
@property
def analytics_operation_key(self) -> str | None:
return "identity_linked"
34 changes: 34 additions & 0 deletions src/sentry/integrations/discord/views/linkage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from abc import ABC

from sentry.integrations.messaging import IdentityLinkageView, MessagingIntegrationSpec
from sentry.integrations.types import ExternalProviderEnum, ExternalProviders

from .constants import SALT


class DiscordIdentityLinkageView(IdentityLinkageView, ABC):
@property
def parent_messaging_spec(self) -> MessagingIntegrationSpec:
from sentry.integrations.discord.spec import DiscordMessagingSpec

return DiscordMessagingSpec()

@property
def provider(self) -> ExternalProviders:
return ExternalProviders.DISCORD

@property
def external_provider_enum(self) -> ExternalProviderEnum:
return ExternalProviderEnum.DISCORD

@property
def salt(self) -> str:
return SALT

@property
def external_id_parameter(self) -> str:
return "discord_id"

@property
def expired_link_template(self) -> str:
return "sentry/integrations/discord/expired-link.html"
70 changes: 15 additions & 55 deletions src/sentry/integrations/discord/views/unlink_identity.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
from django.core.signing import BadSignature, SignatureExpired
from django.db import IntegrityError
from django.http import Http404, HttpRequest, HttpResponse
from collections.abc import Mapping
from typing import Any

from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views.decorators.cache import never_cache

from sentry import analytics
from sentry.integrations.discord.views.linkage import DiscordIdentityLinkageView
from sentry.integrations.messaging import UnlinkIdentityView
from sentry.integrations.models.integration import Integration
from sentry.integrations.services.integration.model import RpcIntegration
from sentry.integrations.types import ExternalProviders
from sentry.integrations.utils.identities import get_identity_or_404
from sentry.types.actor import ActorType
from sentry.users.models.identity import Identity
from sentry.utils.http import absolute_uri
from sentry.utils.signing import sign, unsign
from sentry.web.frontend.base import BaseView, control_silo_view
from sentry.web.helpers import render_to_response
from sentry.utils.signing import sign

from ..utils import logger
from .constants import SALT


Expand All @@ -29,45 +22,12 @@ def build_unlinking_url(integration: RpcIntegration, discord_id: str) -> str:
return absolute_uri(reverse(endpoint, kwargs={"signed_params": sign(salt=SALT, **kwargs)}))


@control_silo_view
class DiscordUnlinkIdentityView(BaseView):
"""
Django view for unlinking user from Discord account.
"""

@method_decorator(never_cache)
def handle(self, request: HttpRequest, signed_params: str) -> HttpResponse:
try:
params = unsign(signed_params, salt=SALT)
except (SignatureExpired, BadSignature):
return render_to_response("sentry/integrations/discord/expired-link.html")

organization, integration, idp = get_identity_or_404(
ExternalProviders.DISCORD,
request.user,
integration_id=params["integration_id"],
)

if request.method != "POST":
return render_to_response(
"sentry/auth-unlink-identity.html",
request=request,
context={"organization": organization, "provider": integration.get_provider()},
)

try:
Identity.objects.filter(idp_id=idp.id, external_id=params["discord_id"]).delete()
except IntegrityError:
logger.exception("discord.unlink.integrity-error")
raise Http404
class DiscordUnlinkIdentityView(DiscordIdentityLinkageView, UnlinkIdentityView):
def get_success_template_and_context(
self, params: Mapping[str, Any], integration: Integration | None
) -> tuple[str, dict[str, Any]]:
return "sentry/integrations/discord/unlinked.html", {}

analytics.record(
"integrations.discord.identity_unlinked",
provider="discord",
actor_id=request.user.id,
actor_type=ActorType.USER,
)
return render_to_response(
"sentry/integrations/discord/unlinked.html",
request=request,
)
@property
def analytics_operation_key(self) -> str | None:
return "identity_unlinked"
Loading

0 comments on commit 5771cda

Please sign in to comment.