Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ref(control_silo): Move UserEmail model to users module #76134

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/sentry/api/endpoints/oauth_userinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from sentry.api.base import Endpoint, control_silo_endpoint
from sentry.api.exceptions import ParameterValidationError, ResourceDoesNotExist, SentryAPIException
from sentry.models.apitoken import ApiToken
from sentry.models.useremail import UserEmail
from sentry.users.models.useremail import UserEmail


class InsufficientScopesError(SentryAPIException):
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/api/endpoints/organization_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@
from sentry.models.options.organization_option import OrganizationOption
from sentry.models.organization import Organization, OrganizationStatus
from sentry.models.scheduledeletion import RegionScheduledDeletion
from sentry.models.useremail import UserEmail
from sentry.organizations.services.organization import organization_service
from sentry.organizations.services.organization.model import (
RpcOrganization,
Expand All @@ -88,6 +87,7 @@
OrganizationSlugCollisionException,
organization_provisioning_service,
)
from sentry.users.models.useremail import UserEmail
from sentry.users.services.user.serial import serialize_generic_user
from sentry.utils.audit import create_audit_entry

Expand Down
2 changes: 1 addition & 1 deletion src/sentry/api/endpoints/user_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@
from sentry.models.organization import OrganizationStatus
from sentry.models.organizationmapping import OrganizationMapping
from sentry.models.organizationmembermapping import OrganizationMemberMapping
from sentry.models.useremail import UserEmail
from sentry.organizations.services.organization import organization_service
from sentry.organizations.services.organization.model import RpcOrganizationDeleteState
from sentry.users.models.user import User
from sentry.users.models.useremail import UserEmail
from sentry.users.services.user.serial import serialize_generic_user
from sentry.utils.dates import AVAILABLE_TIMEZONES

Expand Down
2 changes: 1 addition & 1 deletion src/sentry/api/endpoints/user_emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
from sentry.api.serializers import serialize
from sentry.api.validators import AllowedEmailField
from sentry.models.options.user_option import UserOption
from sentry.models.useremail import UserEmail
from sentry.users.models.user import User
from sentry.users.models.useremail import UserEmail

logger = logging.getLogger("sentry.accounts")

Expand Down
2 changes: 1 addition & 1 deletion src/sentry/api/endpoints/user_emails_confirm.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
from sentry.api.base import control_silo_endpoint
from sentry.api.bases.user import UserEndpoint
from sentry.api.validators import AllowedEmailField
from sentry.models.useremail import UserEmail
from sentry.types.ratelimit import RateLimit, RateLimitCategory
from sentry.users.models.useremail import UserEmail

logger = logging.getLogger("sentry.accounts")

Expand Down
2 changes: 1 addition & 1 deletion src/sentry/api/endpoints/user_notification_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from sentry.api.base import control_silo_endpoint
from sentry.api.bases.user import UserEndpoint
from sentry.models.options.user_option import UserOption
from sentry.models.useremail import UserEmail
from sentry.users.models.useremail import UserEmail

INVALID_EMAIL_MSG = (
"Invalid email value(s) provided. Email values must be verified emails for the given user."
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/api/endpoints/user_subscriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import control_silo_endpoint
from sentry.api.bases.user import UserEndpoint
from sentry.models.useremail import UserEmail
from sentry.users.models.user import User
from sentry.users.models.useremail import UserEmail


class DefaultNewsletterValidator(serializers.Serializer):
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/api/invite_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
from sentry import audit_log, features
from sentry.models.authidentity import AuthIdentity
from sentry.models.authprovider import AuthProvider
from sentry.models.useremail import UserEmail
from sentry.organizations.services.organization import (
RpcOrganizationMember,
RpcUserInviteContext,
organization_service,
)
from sentry.signals import member_joined
from sentry.users.models.user import User
from sentry.users.models.useremail import UserEmail
from sentry.utils import metrics
from sentry.utils.audit import create_audit_entry

Expand Down
2 changes: 1 addition & 1 deletion src/sentry/api/serializers/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
from sentry.models.organization import OrganizationStatus
from sentry.models.organizationmapping import OrganizationMapping
from sentry.models.organizationmembermapping import OrganizationMemberMapping
from sentry.models.useremail import UserEmail
from sentry.models.userpermission import UserPermission
from sentry.organizations.services.organization import RpcOrganizationSummary
from sentry.users.models.authenticator import Authenticator
from sentry.users.models.user import User
from sentry.users.models.useremail import UserEmail
from sentry.users.models.userrole import UserRoleUser
from sentry.users.services.user import RpcUser
from sentry.utils.avatar import get_gravatar_url
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/api/serializers/models/useremail.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from sentry.api.serializers import Serializer, register
from sentry.models.useremail import UserEmail
from sentry.users.models.useremail import UserEmail


@register(UserEmail)
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/auth/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
from dataclasses import dataclass

from sentry.models.organization import Organization
from sentry.models.useremail import UserEmail
from sentry.organizations.services.organization import organization_service
from sentry.users.models.user import User
from sentry.users.models.useremail import UserEmail
from sentry.utils import metrics


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
from sentry.integrations.slack.utils.users import SlackUserData, get_slack_data_by_user
from sentry.integrations.utils import get_identities_by_user
from sentry.models.identity import Identity, IdentityProvider, IdentityStatus
from sentry.models.useremail import UserEmail
from sentry.organizations.services.organization import organization_service
from sentry.silo.base import SiloMode
from sentry.tasks.base import instrumented_task
from sentry.users.models.user import User
from sentry.users.models.useremail import UserEmail

logger = logging.getLogger("sentry.integrations.slack.tasks")

Expand Down
2 changes: 1 addition & 1 deletion src/sentry/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from sentry.users.models.email import * # NOQA
from sentry.users.models.lostpasswordhash import * # NOQA
from sentry.users.models.user import * # NOQA
from sentry.users.models.useremail import * # NOQA
from sentry.users.models.userrole import * # NOQA

from .activity import * # NOQA
Expand Down Expand Up @@ -121,7 +122,6 @@
from .teamreplica import * # NOQA
from .tombstone import * # NOQA
from .transaction_threshold import * # NOQA
from .useremail import * # NOQA
from .userip import * # NOQA
from .userpermission import * # NOQA
from .userreport import * # NOQA
165 changes: 2 additions & 163 deletions src/sentry/models/useremail.py
Original file line number Diff line number Diff line change
@@ -1,164 +1,3 @@
from __future__ import annotations
from sentry.users.models.useremail import UserEmail

from collections import defaultdict
from collections.abc import Iterable, Mapping
from datetime import timedelta
from typing import TYPE_CHECKING, Any, ClassVar

from django.conf import settings
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _

from sentry.backup.dependencies import (
ImportKind,
NormalizedModelName,
PrimaryKeyMap,
get_model_name,
)
from sentry.backup.helpers import ImportFlags
from sentry.backup.sanitize import SanitizableField, Sanitizer
from sentry.backup.scopes import ImportScope, RelocationScope
from sentry.db.models import FlexibleForeignKey, control_silo_model, sane_repr
from sentry.db.models.manager.base import BaseManager
from sentry.hybridcloud.models.outbox import ControlOutboxBase
from sentry.hybridcloud.outbox.base import ControlOutboxProducingModel
from sentry.hybridcloud.outbox.category import OutboxCategory
from sentry.organizations.services.organization.model import RpcOrganization
from sentry.types.region import find_regions_for_user
from sentry.users.services.user.model import RpcUser
from sentry.utils.security import get_secure_token

if TYPE_CHECKING:
from sentry.users.models.user import User


class UserEmailManager(BaseManager["UserEmail"]):
def get_emails_by_user(self, organization: RpcOrganization) -> Mapping[User, Iterable[str]]:
from sentry.models.organizationmembermapping import OrganizationMemberMapping

emails_by_user = defaultdict(set)
user_emails = self.filter(
user_id__in=OrganizationMemberMapping.objects.filter(
organization_id=organization.id
).values_list("user_id", flat=True)
).select_related("user")
for entry in user_emails:
emails_by_user[entry.user].add(entry.email)
return emails_by_user

def get_primary_email(self, user: RpcUser | User) -> UserEmail:
user_email, _ = self.get_or_create(user_id=user.id, email=user.email)
return user_email


@control_silo_model
class UserEmail(ControlOutboxProducingModel):
__relocation_scope__ = RelocationScope.User
__relocation_dependencies__ = {"sentry.Email"}
__relocation_custom_ordinal__ = ["user", "email"]

user = FlexibleForeignKey(settings.AUTH_USER_MODEL, related_name="emails")
email = models.EmailField(_("email address"), max_length=75)
validation_hash = models.CharField(max_length=32, default=get_secure_token)
date_hash_added = models.DateTimeField(default=timezone.now)
is_verified = models.BooleanField(
_("verified"),
default=False,
help_text=_("Designates whether this user has confirmed their email."),
)

objects: ClassVar[UserEmailManager] = UserEmailManager()

class Meta:
app_label = "sentry"
db_table = "sentry_useremail"
unique_together = (("user", "email"),)

__repr__ = sane_repr("user_id", "email")

def outboxes_for_update(self, shard_identifier: int | None = None) -> list[ControlOutboxBase]:
regions = find_regions_for_user(self.user_id)
return [
outbox
for outbox in OutboxCategory.USER_UPDATE.as_control_outboxes(
region_names=regions,
shard_identifier=self.user_id,
object_identifier=self.user_id,
)
]

def set_hash(self):
self.date_hash_added = timezone.now()
self.validation_hash = get_secure_token()

def hash_is_valid(self):
return self.validation_hash and self.date_hash_added > timezone.now() - timedelta(hours=48)

def is_primary(self):
return self.user.email == self.email

@classmethod
def get_primary_email(cls, user: User) -> UserEmail:
"""@deprecated"""
return cls.objects.get_primary_email(user)

def normalize_before_relocation_import(
self, pk_map: PrimaryKeyMap, scope: ImportScope, flags: ImportFlags
) -> int | None:
from sentry.users.models.user import User

old_user_id = self.user_id
old_pk = super().normalize_before_relocation_import(pk_map, scope, flags)
if old_pk is None:
return None

# If we are merging users, ignore the imported email and use the existing user's email
# instead.
if pk_map.get_kind(get_model_name(User), old_user_id) == ImportKind.Existing:
return None

# Only preserve validation hashes in the backup/restore scope - in all others, have the user
# verify their email again.
if scope != ImportScope.Global:
self.is_verified = False
self.validation_hash = get_secure_token()
self.date_hash_added = timezone.now()

return old_pk

def write_relocation_import(
self, _s: ImportScope, _f: ImportFlags
) -> tuple[int, ImportKind] | None:
# The `UserEmail` was automatically generated `post_save()`, but only if it was the user's
# primary email. We just need to update it with the data being imported. Note that if we've
# reached this point, we cannot be merging into an existing user, and are instead modifying
# the just-created `UserEmail` for a new one.
try:
useremail = self.__class__.objects.get(user=self.user, email=self.email)
for f in self._meta.fields:
if f.name not in ["id", "pk"]:
setattr(useremail, f.name, getattr(self, f.name))
except self.__class__.DoesNotExist:
# This is a non-primary email, so was not auto-created - go ahead and add it in.
useremail = self

useremail.save()

# If we've entered this method at all, we can be sure that the `UserEmail` was created as
# part of the import, since this is a new `User` (the "existing" `User` due to
# `--merge_users=true` case is handled in the `normalize_before_relocation_import()` method
# above).
return (useremail.pk, ImportKind.Inserted)

@classmethod
def sanitize_relocation_json(
cls, json: Any, sanitizer: Sanitizer, model_name: NormalizedModelName | None = None
) -> None:
model_name = get_model_name(cls) if model_name is None else model_name
super().sanitize_relocation_json(json, sanitizer, model_name)

validation_hash = get_secure_token()
sanitizer.set_string(
json, SanitizableField(model_name, "validation_hash"), lambda _: validation_hash
)
__all__ = ("UserEmail",)
2 changes: 1 addition & 1 deletion src/sentry/newsletter/dummy.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def __init__(
unsubscribed_date=None,
**kwargs,
):
from sentry.models.useremail import UserEmail
from sentry.users.models.useremail import UserEmail

self.email = user.email or email
self.list_id = list_id
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/receivers/email.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from django.db import IntegrityError, router, transaction
from django.db.models.signals import post_delete, post_save

from sentry.models.useremail import UserEmail
from sentry.users.models.email import Email
from sentry.users.models.useremail import UserEmail


def create_email(instance, created, **kwargs):
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/receivers/useremail.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from django.db import IntegrityError
from django.db.models.signals import post_save

from sentry.models.useremail import UserEmail
from sentry.users.models.user import User
from sentry.users.models.useremail import UserEmail


def create_user_email(instance, created, **kwargs):
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/tasks/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
from sentry.auth.exceptions import ProviderNotRegistered
from sentry.models.organization import Organization
from sentry.models.organizationmember import OrganizationMember
from sentry.models.useremail import UserEmail
from sentry.organizations.services.organization.service import organization_service
from sentry.silo.base import SiloMode
from sentry.silo.safety import unguarded_write
from sentry.tasks.base import instrumented_task, retry
from sentry.types.region import RegionMappingNotFound
from sentry.users.models.user import User
from sentry.users.models.useremail import UserEmail
from sentry.users.services.user import RpcUser
from sentry.users.services.user.service import user_service
from sentry.utils.audit import create_audit_entry_from_user
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/testutils/cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@
from sentry.models.releasecommit import ReleaseCommit
from sentry.models.repository import Repository
from sentry.models.rule import RuleSource
from sentry.models.useremail import UserEmail
from sentry.monitors.models import Monitor, MonitorEnvironment, MonitorType, ScheduleType
from sentry.notifications.notifications.base import alert_page_needs_org_id
from sentry.notifications.types import FineTuningAPIKey
Expand Down Expand Up @@ -146,6 +145,7 @@
from sentry.testutils.pytest.selenium import Browser
from sentry.types.condition_activity import ConditionActivity, ConditionActivityType
from sentry.users.models.user import User
from sentry.users.models.useremail import UserEmail
from sentry.utils import json
from sentry.utils.auth import SsoSession
from sentry.utils.json import dumps_htmlsafe
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/testutils/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@
from sentry.models.savedsearch import SavedSearch
from sentry.models.servicehook import ServiceHook
from sentry.models.team import Team
from sentry.models.useremail import UserEmail
from sentry.models.userpermission import UserPermission
from sentry.models.userreport import UserReport
from sentry.organizations.services.organization import RpcOrganization, RpcUserOrganizationContext
Expand Down Expand Up @@ -167,6 +166,7 @@
UptimeSubscription,
)
from sentry.users.models.user import User
from sentry.users.models.useremail import UserEmail
from sentry.users.models.userrole import UserRole
from sentry.users.services.user import RpcUser
from sentry.utils import loremipsum
Expand Down
Loading
Loading