diff --git a/bin/mock-user b/bin/mock-user index 432218a30eb3d5..b6f6c18f1c7893 100755 --- a/bin/mock-user +++ b/bin/mock-user @@ -8,7 +8,7 @@ import argparse def main(username, newsletter_consent_prompt=None): - from sentry.models.user import User + from sentry.users.models.user import User user = User.objects.get(username__iexact=username) diff --git a/src/sentry/api/authentication.py b/src/sentry/api/authentication.py index 4f353e3bc1069f..0f66f39f899d22 100644 --- a/src/sentry/api/authentication.py +++ b/src/sentry/api/authentication.py @@ -35,9 +35,9 @@ ) from sentry.models.projectkey import ProjectKey from sentry.models.relay import Relay -from sentry.models.user import User from sentry.relay.utils import get_header_relay_id, get_header_relay_signature from sentry.silo.base import SiloLimit, SiloMode +from sentry.users.models.user import User from sentry.users.services.user import RpcUser from sentry.users.services.user.service import user_service from sentry.utils.linksign import process_signature diff --git a/src/sentry/api/bases/user.py b/src/sentry/api/bases/user.py index 0c1e52bc3a5c84..f9ccdc20ce07be 100644 --- a/src/sentry/api/bases/user.py +++ b/src/sentry/api/bases/user.py @@ -13,8 +13,8 @@ from sentry.models.organization import OrganizationStatus from sentry.models.organizationmapping import OrganizationMapping from sentry.models.organizationmembermapping import OrganizationMemberMapping -from sentry.models.user import User from sentry.organizations.services.organization import organization_service +from sentry.users.models.user import User from sentry.users.services.user import RpcUser from sentry.users.services.user.service import user_service diff --git a/src/sentry/api/endpoints/auth_index.py b/src/sentry/api/endpoints/auth_index.py index ec3b5a2abd36e8..f3c01ad25fe3ff 100644 --- a/src/sentry/api/endpoints/auth_index.py +++ b/src/sentry/api/endpoints/auth_index.py @@ -20,8 +20,8 @@ from sentry.auth.authenticators.u2f import U2fInterface from sentry.auth.services.auth.impl import promote_request_rpc_user from sentry.auth.superuser import SUPERUSER_ORG_ID -from sentry.models.authenticator import Authenticator from sentry.organizations.services.organization import organization_service +from sentry.users.models.authenticator import Authenticator from sentry.utils import auth, json, metrics from sentry.utils.auth import DISABLE_SSO_CHECK_FOR_LOCAL_DEV, has_completed_sso, initiate_login from sentry.utils.settings import is_self_hosted diff --git a/src/sentry/api/endpoints/authenticator_index.py b/src/sentry/api/endpoints/authenticator_index.py index 89a171955c9bce..9894095a0a9208 100644 --- a/src/sentry/api/endpoints/authenticator_index.py +++ b/src/sentry/api/endpoints/authenticator_index.py @@ -7,7 +7,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, control_silo_endpoint -from sentry.models.authenticator import Authenticator +from sentry.users.models.authenticator import Authenticator @control_silo_endpoint diff --git a/src/sentry/api/endpoints/group_ai_autofix.py b/src/sentry/api/endpoints/group_ai_autofix.py index b483915d519428..98c73c16337938 100644 --- a/src/sentry/api/endpoints/group_ai_autofix.py +++ b/src/sentry/api/endpoints/group_ai_autofix.py @@ -18,9 +18,9 @@ from sentry.api.serializers import EventSerializer, serialize from sentry.autofix.utils import get_autofix_repos_from_project_code_mappings from sentry.models.group import Group -from sentry.models.user import User from sentry.seer.signed_seer_api import sign_with_seer_secret from sentry.types.ratelimit import RateLimit, RateLimitCategory +from sentry.users.models.user import User from sentry.users.services.user.service import user_service logger = logging.getLogger(__name__) diff --git a/src/sentry/api/endpoints/group_details.py b/src/sentry/api/endpoints/group_details.py index fcd71f72d2ae57..2ac40eb57a4896 100644 --- a/src/sentry/api/endpoints/group_details.py +++ b/src/sentry/api/endpoints/group_details.py @@ -40,10 +40,10 @@ from sentry.models.integrations.external_issue import ExternalIssue from sentry.models.platformexternalissue import PlatformExternalIssue from sentry.models.team import Team -from sentry.models.userreport import UserReport from sentry.plugins.base import plugins from sentry.tasks.post_process import fetch_buffered_group_stats from sentry.types.ratelimit import RateLimit, RateLimitCategory +from sentry.users.models.userreport import UserReport from sentry.users.services.user.service import user_service from sentry.utils import metrics diff --git a/src/sentry/api/endpoints/group_event_details.py b/src/sentry/api/endpoints/group_event_details.py index 746c58ae751210..20714e66140149 100644 --- a/src/sentry/api/endpoints/group_event_details.py +++ b/src/sentry/api/endpoints/group_event_details.py @@ -21,10 +21,10 @@ from sentry.api.serializers import EventSerializer, serialize from sentry.issues.grouptype import GroupCategory from sentry.models.environment import Environment -from sentry.models.user import User from sentry.search.events.filter import convert_search_filter_to_snuba_query, format_search_filter from sentry.snuba.dataset import Dataset from sentry.types.ratelimit import RateLimit, RateLimitCategory +from sentry.users.models.user import User from sentry.utils import metrics if TYPE_CHECKING: diff --git a/src/sentry/api/endpoints/group_integration_details.py b/src/sentry/api/endpoints/group_integration_details.py index 3c8bb2d05741b3..3224c2ff830922 100644 --- a/src/sentry/api/endpoints/group_integration_details.py +++ b/src/sentry/api/endpoints/group_integration_details.py @@ -17,10 +17,10 @@ from sentry.models.group import Group from sentry.models.grouplink import GroupLink from sentry.models.integrations.external_issue import ExternalIssue -from sentry.models.user import User from sentry.shared_integrations.exceptions import IntegrationError, IntegrationFormError from sentry.signals import integration_issue_created, integration_issue_linked from sentry.types.activity import ActivityType +from sentry.users.models.user import User MISSING_FEATURE_MESSAGE = "Your organization does not have access to this feature." diff --git a/src/sentry/api/endpoints/group_integrations.py b/src/sentry/api/endpoints/group_integrations.py index 37a1e677c9c6a1..166ec6711e0ecb 100644 --- a/src/sentry/api/endpoints/group_integrations.py +++ b/src/sentry/api/endpoints/group_integrations.py @@ -19,7 +19,7 @@ from sentry.models.group import Group from sentry.models.grouplink import GroupLink from sentry.models.integrations.external_issue import ExternalIssue -from sentry.models.user import User +from sentry.users.models.user import User class IntegrationIssueSerializer(IntegrationSerializer): diff --git a/src/sentry/api/endpoints/group_similar_issues_embeddings.py b/src/sentry/api/endpoints/group_similar_issues_embeddings.py index dd4707d3426941..a9b27006e8b0c3 100644 --- a/src/sentry/api/endpoints/group_similar_issues_embeddings.py +++ b/src/sentry/api/endpoints/group_similar_issues_embeddings.py @@ -14,10 +14,10 @@ from sentry.api.serializers import serialize from sentry.grouping.grouping_info import get_grouping_info from sentry.models.group import Group -from sentry.models.user import User from sentry.seer.similarity.similar_issues import get_similarity_data_from_seer from sentry.seer.similarity.types import SeerSimilarIssueData, SimilarIssuesEmbeddingsRequest from sentry.seer.similarity.utils import get_stacktrace_string +from sentry.users.models.user import User from sentry.utils.safe import get_path logger = logging.getLogger(__name__) diff --git a/src/sentry/api/endpoints/group_user_reports.py b/src/sentry/api/endpoints/group_user_reports.py index 9714821b6fd235..5b37cd22c9a0f3 100644 --- a/src/sentry/api/endpoints/group_user_reports.py +++ b/src/sentry/api/endpoints/group_user_reports.py @@ -7,7 +7,7 @@ from sentry.api.paginator import DateTimePaginator from sentry.api.serializers import serialize from sentry.models.environment import Environment -from sentry.models.userreport import UserReport +from sentry.users.models.userreport import UserReport @region_silo_endpoint diff --git a/src/sentry/api/endpoints/index.py b/src/sentry/api/endpoints/index.py index 60a6f8918e127d..580a739778be37 100644 --- a/src/sentry/api/endpoints/index.py +++ b/src/sentry/api/endpoints/index.py @@ -4,7 +4,7 @@ from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, control_silo_endpoint from sentry.api.serializers import serialize -from sentry.models.user import User +from sentry.users.models.user import User @control_silo_endpoint diff --git a/src/sentry/api/endpoints/integrations/sentry_apps/installation/external_issue/actions.py b/src/sentry/api/endpoints/integrations/sentry_apps/installation/external_issue/actions.py index 918812b5747cae..cca110d734d7f2 100644 --- a/src/sentry/api/endpoints/integrations/sentry_apps/installation/external_issue/actions.py +++ b/src/sentry/api/endpoints/integrations/sentry_apps/installation/external_issue/actions.py @@ -10,7 +10,7 @@ from sentry.mediators.external_issues.issue_link_creator import IssueLinkCreator from sentry.models.group import Group from sentry.models.project import Project -from sentry.models.user import User +from sentry.users.models.user import User from sentry.users.services.user.serial import serialize_rpc_user diff --git a/src/sentry/api/endpoints/oauth_userinfo.py b/src/sentry/api/endpoints/oauth_userinfo.py index 80c4c8ed2d3987..67b4bde537057e 100644 --- a/src/sentry/api/endpoints/oauth_userinfo.py +++ b/src/sentry/api/endpoints/oauth_userinfo.py @@ -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): diff --git a/src/sentry/api/endpoints/org_auth_tokens.py b/src/sentry/api/endpoints/org_auth_tokens.py index aed1cea5d51de4..8e80bb35ebb374 100644 --- a/src/sentry/api/endpoints/org_auth_tokens.py +++ b/src/sentry/api/endpoints/org_auth_tokens.py @@ -19,12 +19,12 @@ from sentry.models.organizationmapping import OrganizationMapping from sentry.models.organizationmembermapping import OrganizationMemberMapping from sentry.models.orgauthtoken import MAX_NAME_LENGTH, OrgAuthToken -from sentry.models.user import User from sentry.organizations.services.organization.model import ( RpcOrganization, RpcUserOrganizationContext, ) from sentry.security.utils import capture_security_activity +from sentry.users.models.user import User from sentry.utils.security.orgauthtoken_token import ( SystemUrlPrefixMissingException, generate_token, diff --git a/src/sentry/api/endpoints/organization_details.py b/src/sentry/api/endpoints/organization_details.py index d7e70dfbaecd5d..c5d6f49b6b3f73 100644 --- a/src/sentry/api/endpoints/organization_details.py +++ b/src/sentry/api/endpoints/organization_details.py @@ -66,7 +66,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, @@ -77,6 +76,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 diff --git a/src/sentry/api/endpoints/organization_onboarding_continuation_email.py b/src/sentry/api/endpoints/organization_onboarding_continuation_email.py index 31770646b414d3..9696b7ad14cc95 100644 --- a/src/sentry/api/endpoints/organization_onboarding_continuation_email.py +++ b/src/sentry/api/endpoints/organization_onboarding_continuation_email.py @@ -8,7 +8,7 @@ from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.serializers.rest_framework.base import CamelSnakeSerializer from sentry.models.organization import Organization -from sentry.models.user import User +from sentry.users.models.user import User from sentry.utils.email import MessageBuilder from sentry.utils.strings import oxfordize_list diff --git a/src/sentry/api/endpoints/organization_user_reports.py b/src/sentry/api/endpoints/organization_user_reports.py index a083d32db550c7..9e75da1ace3f63 100644 --- a/src/sentry/api/endpoints/organization_user_reports.py +++ b/src/sentry/api/endpoints/organization_user_reports.py @@ -12,7 +12,7 @@ from sentry.api.paginator import DateTimePaginator from sentry.api.serializers import serialize from sentry.api.serializers.models import UserReportWithGroupSerializer -from sentry.models.userreport import UserReport +from sentry.users.models.userreport import UserReport class _PaginateKwargs(TypedDict): diff --git a/src/sentry/api/endpoints/project_user_reports.py b/src/sentry/api/endpoints/project_user_reports.py index 36dda69e763ff7..d3d4a824c8ef38 100644 --- a/src/sentry/api/endpoints/project_user_reports.py +++ b/src/sentry/api/endpoints/project_user_reports.py @@ -16,7 +16,7 @@ from sentry.ingest.userreport import Conflict, save_userreport from sentry.models.environment import Environment from sentry.models.projectkey import ProjectKey -from sentry.models.userreport import UserReport +from sentry.users.models.userreport import UserReport class UserReportSerializer(serializers.ModelSerializer): diff --git a/src/sentry/api/endpoints/relocations/index.py b/src/sentry/api/endpoints/relocations/index.py index f7f790c61162c7..13e4b8f0582122 100644 --- a/src/sentry/api/endpoints/relocations/index.py +++ b/src/sentry/api/endpoints/relocations/index.py @@ -24,12 +24,12 @@ from sentry.auth.elevated_mode import has_elevated_mode from sentry.models.files.file import File from sentry.models.relocation import Relocation, RelocationFile -from sentry.models.user import MAX_USERNAME_LENGTH from sentry.options import get from sentry.search.utils import tokenize_query from sentry.signals import relocation_link_promo_code from sentry.slug.patterns import ORG_SLUG_PATTERN from sentry.tasks.relocation import uploading_complete +from sentry.users.models.user import MAX_USERNAME_LENGTH from sentry.users.services.user.model import RpcUser from sentry.users.services.user.service import user_service from sentry.utils.db import atomic_transaction diff --git a/src/sentry/api/endpoints/user_authenticator_details.py b/src/sentry/api/endpoints/user_authenticator_details.py index 14f45fffc9f141..30f4d700e52802 100644 --- a/src/sentry/api/endpoints/user_authenticator_details.py +++ b/src/sentry/api/endpoints/user_authenticator_details.py @@ -13,9 +13,9 @@ from sentry.auth.authenticators.u2f import decode_credential_id from sentry.auth.staff import has_staff_option, is_active_staff from sentry.auth.superuser import is_active_superuser -from sentry.models.authenticator import Authenticator -from sentry.models.user import User from sentry.security.utils import capture_security_activity +from sentry.users.models.authenticator import Authenticator +from sentry.users.models.user import User from sentry.utils.auth import MFA_SESSION_KEY diff --git a/src/sentry/api/endpoints/user_authenticator_enroll.py b/src/sentry/api/endpoints/user_authenticator_enroll.py index 5213e81f1a6bf8..fccb770de0b155 100644 --- a/src/sentry/api/endpoints/user_authenticator_enroll.py +++ b/src/sentry/api/endpoints/user_authenticator_enroll.py @@ -18,10 +18,10 @@ from sentry.api.serializers import serialize from sentry.auth.authenticators.base import EnrollmentStatus, NewEnrollmentDisallowed from sentry.auth.authenticators.sms import SMSRateLimitExceeded -from sentry.models.authenticator import Authenticator -from sentry.models.user import User from sentry.organizations.services.organization import organization_service from sentry.security.utils import capture_security_activity +from sentry.users.models.authenticator import Authenticator +from sentry.users.models.user import User from sentry.utils.auth import MFA_SESSION_KEY logger = logging.getLogger(__name__) diff --git a/src/sentry/api/endpoints/user_authenticator_index.py b/src/sentry/api/endpoints/user_authenticator_index.py index e441ff584fefc1..f2d972d04aca57 100644 --- a/src/sentry/api/endpoints/user_authenticator_index.py +++ b/src/sentry/api/endpoints/user_authenticator_index.py @@ -6,7 +6,7 @@ from sentry.api.base import control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.api.serializers import serialize -from sentry.models.authenticator import Authenticator +from sentry.users.models.authenticator import Authenticator @control_silo_endpoint diff --git a/src/sentry/api/endpoints/user_details.py b/src/sentry/api/endpoints/user_details.py index 02361826077bda..e381e93361070f 100644 --- a/src/sentry/api/endpoints/user_details.py +++ b/src/sentry/api/endpoints/user_details.py @@ -21,14 +21,14 @@ from sentry.api.serializers.rest_framework import CamelSnakeModelSerializer from sentry.auth.elevated_mode import has_elevated_mode from sentry.constants import LANGUAGES -from sentry.models.options.user_option import UserOption from sentry.models.organization import OrganizationStatus from sentry.models.organizationmapping import OrganizationMapping from sentry.models.organizationmembermapping import OrganizationMemberMapping -from sentry.models.user import User -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.user_option import UserOption +from sentry.users.models.useremail import UserEmail from sentry.users.services.user.serial import serialize_generic_user from sentry.utils.dates import AVAILABLE_TIMEZONES diff --git a/src/sentry/api/endpoints/user_emails.py b/src/sentry/api/endpoints/user_emails.py index 7e01cf8b96473e..4bc724576bbe42 100644 --- a/src/sentry/api/endpoints/user_emails.py +++ b/src/sentry/api/endpoints/user_emails.py @@ -10,9 +10,9 @@ from sentry.api.decorators import sudo_required from sentry.api.serializers import serialize from sentry.api.validators import AllowedEmailField -from sentry.models.options.user_option import UserOption -from sentry.models.user import User -from sentry.models.useremail import UserEmail +from sentry.users.models.user import User +from sentry.users.models.user_option import UserOption +from sentry.users.models.useremail import UserEmail logger = logging.getLogger("sentry.accounts") diff --git a/src/sentry/api/endpoints/user_emails_confirm.py b/src/sentry/api/endpoints/user_emails_confirm.py index 4acbc3adee9a90..16c51e206093e1 100644 --- a/src/sentry/api/endpoints/user_emails_confirm.py +++ b/src/sentry/api/endpoints/user_emails_confirm.py @@ -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") diff --git a/src/sentry/api/endpoints/user_identity_config.py b/src/sentry/api/endpoints/user_identity_config.py index 31e7f6c7d26cc9..f43f2b3a5ccb8d 100644 --- a/src/sentry/api/endpoints/user_identity_config.py +++ b/src/sentry/api/endpoints/user_identity_config.py @@ -17,7 +17,7 @@ ) from sentry.models.authidentity import AuthIdentity from sentry.models.identity import Identity -from sentry.models.user import User +from sentry.users.models.user import User from social_auth.models import UserSocialAuth diff --git a/src/sentry/api/endpoints/user_index.py b/src/sentry/api/endpoints/user_index.py index e184071339e5cb..58b632d23b4908 100644 --- a/src/sentry/api/endpoints/user_index.py +++ b/src/sentry/api/endpoints/user_index.py @@ -8,8 +8,8 @@ from sentry.api.permissions import SuperuserOrStaffFeatureFlaggedPermission from sentry.api.serializers import serialize from sentry.db.models.query import in_iexact -from sentry.models.user import User from sentry.search.utils import tokenize_query +from sentry.users.models.user import User @control_silo_endpoint diff --git a/src/sentry/api/endpoints/user_ips.py b/src/sentry/api/endpoints/user_ips.py index 2efb14283ce567..ccbd3b4d5c0e3e 100644 --- a/src/sentry/api/endpoints/user_ips.py +++ b/src/sentry/api/endpoints/user_ips.py @@ -7,7 +7,7 @@ from sentry.api.decorators import sudo_required from sentry.api.paginator import DateTimePaginator from sentry.api.serializers import serialize -from sentry.models.userip import UserIP +from sentry.users.models.userip import UserIP @control_silo_endpoint diff --git a/src/sentry/api/endpoints/user_notification_details.py b/src/sentry/api/endpoints/user_notification_details.py index f6998d890c78fc..95afd419922d57 100644 --- a/src/sentry/api/endpoints/user_notification_details.py +++ b/src/sentry/api/endpoints/user_notification_details.py @@ -9,8 +9,8 @@ from sentry.api.base import control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.api.serializers import Serializer, serialize -from sentry.models.options.user_option import UserOption from sentry.notifications.types import UserOptionsSettingsKey +from sentry.users.models.user_option import UserOption USER_OPTION_SETTINGS = { UserOptionsSettingsKey.SELF_ACTIVITY: { diff --git a/src/sentry/api/endpoints/user_notification_email.py b/src/sentry/api/endpoints/user_notification_email.py index 6307c6564d1211..8d72e8817bc26b 100644 --- a/src/sentry/api/endpoints/user_notification_email.py +++ b/src/sentry/api/endpoints/user_notification_email.py @@ -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.options.user_option import UserOption -from sentry.models.useremail import UserEmail +from sentry.users.models.user_option import UserOption +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." diff --git a/src/sentry/api/endpoints/user_notification_settings_options.py b/src/sentry/api/endpoints/user_notification_settings_options.py index 0e74a43ec35027..c646a3d61232ac 100644 --- a/src/sentry/api/endpoints/user_notification_settings_options.py +++ b/src/sentry/api/endpoints/user_notification_settings_options.py @@ -10,9 +10,9 @@ from sentry.api.serializers import serialize from sentry.api.validators.notifications import validate_type from sentry.models.notificationsettingoption import NotificationSettingOption -from sentry.models.user import User from sentry.notifications.serializers import NotificationSettingsOptionSerializer from sentry.notifications.validators import UserNotificationSettingOptionWithValueSerializer +from sentry.users.models.user import User @control_silo_endpoint diff --git a/src/sentry/api/endpoints/user_notification_settings_options_detail.py b/src/sentry/api/endpoints/user_notification_settings_options_detail.py index 3d2d9715e6751a..a35285814ba3f3 100644 --- a/src/sentry/api/endpoints/user_notification_settings_options_detail.py +++ b/src/sentry/api/endpoints/user_notification_settings_options_detail.py @@ -7,7 +7,7 @@ from sentry.api.base import control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.models.notificationsettingoption import NotificationSettingOption -from sentry.models.user import User +from sentry.users.models.user import User @control_silo_endpoint diff --git a/src/sentry/api/endpoints/user_notification_settings_providers.py b/src/sentry/api/endpoints/user_notification_settings_providers.py index 77ea107ee78d8e..f3455e2aa1cb06 100644 --- a/src/sentry/api/endpoints/user_notification_settings_providers.py +++ b/src/sentry/api/endpoints/user_notification_settings_providers.py @@ -12,10 +12,10 @@ from sentry.api.validators.notifications import validate_type from sentry.integrations.types import PERSONAL_NOTIFICATION_PROVIDERS from sentry.models.notificationsettingprovider import NotificationSettingProvider -from sentry.models.user import User from sentry.notifications.serializers import NotificationSettingsProviderSerializer from sentry.notifications.types import NotificationSettingsOptionEnum from sentry.notifications.validators import UserNotificationSettingsProvidersDetailsSerializer +from sentry.users.models.user import User @control_silo_endpoint diff --git a/src/sentry/api/endpoints/user_permission_details.py b/src/sentry/api/endpoints/user_permission_details.py index 44a96401e8ba60..b9bc471d232447 100644 --- a/src/sentry/api/endpoints/user_permission_details.py +++ b/src/sentry/api/endpoints/user_permission_details.py @@ -12,7 +12,7 @@ from sentry.api.bases.user import UserEndpoint from sentry.api.decorators import sudo_required from sentry.api.permissions import SuperuserOrStaffFeatureFlaggedPermission -from sentry.models.userpermission import UserPermission +from sentry.users.models.userpermission import UserPermission audit_logger = logging.getLogger("sentry.audit.user") diff --git a/src/sentry/api/endpoints/user_permissions.py b/src/sentry/api/endpoints/user_permissions.py index e33544d446ea1e..09c6ae67f3b658 100644 --- a/src/sentry/api/endpoints/user_permissions.py +++ b/src/sentry/api/endpoints/user_permissions.py @@ -6,7 +6,7 @@ from sentry.api.base import control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.api.permissions import SuperuserOrStaffFeatureFlaggedPermission -from sentry.models.userpermission import UserPermission +from sentry.users.models.userpermission import UserPermission @control_silo_endpoint diff --git a/src/sentry/api/endpoints/user_regions.py b/src/sentry/api/endpoints/user_regions.py index 1b683b983ef56d..2ff45118a675f8 100644 --- a/src/sentry/api/endpoints/user_regions.py +++ b/src/sentry/api/endpoints/user_regions.py @@ -11,8 +11,8 @@ from sentry.auth.system import is_system_auth from sentry.models.organizationmapping import OrganizationMapping from sentry.models.organizationmembermapping import OrganizationMemberMapping -from sentry.models.user import User from sentry.types.region import get_region_by_name +from sentry.users.models.user import User from sentry.users.services.user import RpcUser diff --git a/src/sentry/api/endpoints/user_role_details.py b/src/sentry/api/endpoints/user_role_details.py index 654880869391ce..2eb9cc1f966de0 100644 --- a/src/sentry/api/endpoints/user_role_details.py +++ b/src/sentry/api/endpoints/user_role_details.py @@ -11,7 +11,7 @@ from sentry.api.decorators import sudo_required from sentry.api.permissions import SuperuserPermission from sentry.api.serializers import serialize -from sentry.models.userrole import UserRole, UserRoleUser +from sentry.users.models.userrole import UserRole, UserRoleUser audit_logger = logging.getLogger("sentry.audit.user") diff --git a/src/sentry/api/endpoints/user_roles.py b/src/sentry/api/endpoints/user_roles.py index c99a9a02664ef5..4a10a1bcabd0ab 100644 --- a/src/sentry/api/endpoints/user_roles.py +++ b/src/sentry/api/endpoints/user_roles.py @@ -6,7 +6,7 @@ from sentry.api.bases.user import UserEndpoint from sentry.api.permissions import SuperuserPermission from sentry.api.serializers import serialize -from sentry.models.userrole import UserRole +from sentry.users.models.userrole import UserRole @control_silo_endpoint diff --git a/src/sentry/api/endpoints/user_subscriptions.py b/src/sentry/api/endpoints/user_subscriptions.py index 5e81551ff0ace4..5b501ebeae1799 100644 --- a/src/sentry/api/endpoints/user_subscriptions.py +++ b/src/sentry/api/endpoints/user_subscriptions.py @@ -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.user import User -from sentry.models.useremail import UserEmail +from sentry.users.models.user import User +from sentry.users.models.useremail import UserEmail class DefaultNewsletterValidator(serializers.Serializer): diff --git a/src/sentry/api/endpoints/userroles_details.py b/src/sentry/api/endpoints/userroles_details.py index 4c76a763be81ef..9d907ffc7d39dd 100644 --- a/src/sentry/api/endpoints/userroles_details.py +++ b/src/sentry/api/endpoints/userroles_details.py @@ -10,7 +10,7 @@ from sentry.api.permissions import SuperuserPermission from sentry.api.serializers import serialize from sentry.api.validators.userrole import UserRoleValidator -from sentry.models.userrole import UserRole +from sentry.users.models.userrole import UserRole audit_logger = logging.getLogger("sentry.audit.user") diff --git a/src/sentry/api/endpoints/userroles_index.py b/src/sentry/api/endpoints/userroles_index.py index bedbd2e0be2f74..6c821fb5399453 100644 --- a/src/sentry/api/endpoints/userroles_index.py +++ b/src/sentry/api/endpoints/userroles_index.py @@ -10,7 +10,7 @@ from sentry.api.permissions import SuperuserPermission from sentry.api.serializers import serialize from sentry.api.validators.userrole import UserRoleValidator -from sentry.models.userrole import UserRole +from sentry.users.models.userrole import UserRole audit_logger = logging.getLogger("sentry.audit.user") diff --git a/src/sentry/api/fields/user.py b/src/sentry/api/fields/user.py index 1fd71a54c20756..6ce4150d4ddac9 100644 --- a/src/sentry/api/fields/user.py +++ b/src/sentry/api/fields/user.py @@ -4,7 +4,7 @@ from rest_framework import serializers -from sentry.models.user import User +from sentry.users.models.user import User from sentry.users.services.user import RpcUser from sentry.users.services.user.service import user_service diff --git a/src/sentry/api/helpers/group_index/index.py b/src/sentry/api/helpers/group_index/index.py index 9052d095753bd4..5f4a30f82bf841 100644 --- a/src/sentry/api/helpers/group_index/index.py +++ b/src/sentry/api/helpers/group_index/index.py @@ -22,8 +22,8 @@ from sentry.models.project import Project from sentry.models.release import Release from sentry.models.savedsearch import SavedSearch, Visibility -from sentry.models.user import User from sentry.signals import advanced_search_feature_gated +from sentry.users.models.user import User from sentry.utils import metrics from sentry.utils.cursors import Cursor, CursorResult diff --git a/src/sentry/api/helpers/group_index/update.py b/src/sentry/api/helpers/group_index/update.py index 4c651f3a71511f..be3b04f7f7e8f5 100644 --- a/src/sentry/api/helpers/group_index/update.py +++ b/src/sentry/api/helpers/group_index/update.py @@ -44,13 +44,13 @@ from sentry.models.grouptombstone import TOMBSTONE_FIELDS_FROM_GROUP, GroupTombstone from sentry.models.project import Project from sentry.models.release import Release, follows_semver_versioning_scheme -from sentry.models.user import User from sentry.notifications.types import SUBSCRIPTION_REASON_MAP, GroupSubscriptionReason from sentry.signals import issue_resolved from sentry.tasks.integrations import kick_off_status_syncs from sentry.types.activity import ActivityType from sentry.types.actor import Actor, ActorType from sentry.types.group import SUBSTATUS_UPDATE_CHOICES, GroupSubStatus, PriorityLevel +from sentry.users.models.user import User from sentry.users.services.user import RpcUser from sentry.users.services.user.service import user_service from sentry.users.services.user_option import user_option_service diff --git a/src/sentry/api/invite_helper.py b/src/sentry/api/invite_helper.py index 4cbc6c91c625c0..2dd8e65e5e8de5 100644 --- a/src/sentry/api/invite_helper.py +++ b/src/sentry/api/invite_helper.py @@ -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.user import User -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 diff --git a/src/sentry/api/issue_search.py b/src/sentry/api/issue_search.py index 0a46b1bc944599..d8c480267a79e2 100644 --- a/src/sentry/api/issue_search.py +++ b/src/sentry/api/issue_search.py @@ -25,7 +25,6 @@ from sentry.models.organization import Organization from sentry.models.project import Project from sentry.models.team import Team -from sentry.models.user import User from sentry.search.events.constants import EQUALITY_OPERATORS, INEQUALITY_OPERATORS from sentry.search.events.filter import ParsedTerms, to_list from sentry.search.utils import ( @@ -38,6 +37,7 @@ parse_user_value, ) from sentry.types.group import SUBSTATUS_UPDATE_CHOICES, GroupSubStatus, PriorityLevel +from sentry.users.models.user import User from sentry.users.services.user import RpcUser is_filter_translation = { diff --git a/src/sentry/api/serializers/models/doc_integration.py b/src/sentry/api/serializers/models/doc_integration.py index c4e55457e642af..a7f7e8218faf0a 100644 --- a/src/sentry/api/serializers/models/doc_integration.py +++ b/src/sentry/api/serializers/models/doc_integration.py @@ -5,7 +5,7 @@ from sentry.models.avatars.doc_integration_avatar import DocIntegrationAvatar from sentry.models.integrations.doc_integration import DocIntegration from sentry.models.integrations.integration_feature import IntegrationFeature, IntegrationTypes -from sentry.models.user import User +from sentry.users.models.user import User @register(DocIntegration) diff --git a/src/sentry/api/serializers/models/event.py b/src/sentry/api/serializers/models/event.py index 2b351ee0209ecc..785ba387ea2cb9 100644 --- a/src/sentry/api/serializers/models/event.py +++ b/src/sentry/api/serializers/models/event.py @@ -16,11 +16,11 @@ from sentry.models.eventattachment import EventAttachment from sentry.models.eventerror import EventError from sentry.models.release import Release -from sentry.models.user import User -from sentry.models.userreport import UserReport from sentry.sdk_updates import SdkSetupState, get_suggested_updates from sentry.search.utils import convert_user_tag_to_query, map_device_class_level from sentry.stacktraces.processing import find_stacktraces_in_data +from sentry.users.models.user import User +from sentry.users.models.userreport import UserReport from sentry.utils.json import prune_empty_keys from sentry.utils.safe import get_path diff --git a/src/sentry/api/serializers/models/external_actor.py b/src/sentry/api/serializers/models/external_actor.py index 22e4d1ad75f681..b38076010dbd59 100644 --- a/src/sentry/api/serializers/models/external_actor.py +++ b/src/sentry/api/serializers/models/external_actor.py @@ -4,7 +4,7 @@ from sentry.api.serializers import Serializer, register from sentry.integrations.utils.providers import get_provider_string from sentry.models.integrations.external_actor import ExternalActor -from sentry.models.user import User +from sentry.users.models.user import User class ExternalActorResponseOptional(TypedDict, total=False): diff --git a/src/sentry/api/serializers/models/external_issue.py b/src/sentry/api/serializers/models/external_issue.py index c5eba1ee82877c..91edefb16e4528 100644 --- a/src/sentry/api/serializers/models/external_issue.py +++ b/src/sentry/api/serializers/models/external_issue.py @@ -6,7 +6,7 @@ from sentry.api.serializers.base import Serializer from sentry.integrations.services.integration.service import integration_service from sentry.models.integrations.external_issue import ExternalIssue -from sentry.models.user import User +from sentry.users.models.user import User # Serializer for External Issues Model diff --git a/src/sentry/api/serializers/models/group.py b/src/sentry/api/serializers/models/group.py index fd8b946aea19a5..ddc1d80a5fe0c6 100644 --- a/src/sentry/api/serializers/models/group.py +++ b/src/sentry/api/serializers/models/group.py @@ -40,7 +40,6 @@ from sentry.models.organizationmember import OrganizationMember from sentry.models.orgauthtoken import is_org_auth_token_auth from sentry.models.team import Team -from sentry.models.user import User from sentry.notifications.helpers import collect_groups_by_project, get_subscription_from_attributes from sentry.notifications.services import notifications_service from sentry.notifications.types import NotificationSettingEnum @@ -52,6 +51,7 @@ from sentry.tagstore.types import GroupTagValue from sentry.tsdb.snuba import SnubaTSDB from sentry.types.group import SUBSTATUS_TO_STR, PriorityLevel +from sentry.users.models.user import User from sentry.users.services.user.serial import serialize_generic_user from sentry.users.services.user.service import user_service from sentry.utils.cache import cache diff --git a/src/sentry/api/serializers/models/integration.py b/src/sentry/api/serializers/models/integration.py index 7095030570f5fb..529ea05854888f 100644 --- a/src/sentry/api/serializers/models/integration.py +++ b/src/sentry/api/serializers/models/integration.py @@ -13,8 +13,8 @@ ) from sentry.models.integrations.integration import Integration from sentry.models.integrations.organization_integration import OrganizationIntegration -from sentry.models.user import User from sentry.shared_integrations.exceptions import ApiError +from sentry.users.models.user import User logger = logging.getLogger(__name__) diff --git a/src/sentry/api/serializers/models/integration_feature.py b/src/sentry/api/serializers/models/integration_feature.py index 19b4e31d72013f..35ad5f1f46f0c5 100644 --- a/src/sentry/api/serializers/models/integration_feature.py +++ b/src/sentry/api/serializers/models/integration_feature.py @@ -3,7 +3,7 @@ from sentry.api.serializers import Serializer, register from sentry.models.integrations.integration_feature import IntegrationFeature -from sentry.models.user import User +from sentry.users.models.user import User @register(IntegrationFeature) diff --git a/src/sentry/api/serializers/models/organization.py b/src/sentry/api/serializers/models/organization.py index 6ac500ae801806..b1d815b1c5cd1f 100644 --- a/src/sentry/api/serializers/models/organization.py +++ b/src/sentry/api/serializers/models/organization.py @@ -62,8 +62,8 @@ from sentry.models.organizationonboardingtask import OrganizationOnboardingTask from sentry.models.project import Project from sentry.models.team import Team, TeamStatus -from sentry.models.user import User from sentry.organizations.services.organization import RpcOrganizationSummary +from sentry.users.models.user import User from sentry.users.services.user.service import user_service _ORGANIZATION_SCOPE_PREFIX = "organizations:" diff --git a/src/sentry/api/serializers/models/organization_member/base.py b/src/sentry/api/serializers/models/organization_member/base.py index ebd2318e7f0b38..9cf415716ed49c 100644 --- a/src/sentry/api/serializers/models/organization_member/base.py +++ b/src/sentry/api/serializers/models/organization_member/base.py @@ -6,7 +6,7 @@ from sentry.api.serializers import Serializer, register, serialize from sentry.models.integrations.external_actor import ExternalActor from sentry.models.organizationmember import OrganizationMember -from sentry.models.user import User +from sentry.users.models.user import User from sentry.users.services.user import RpcUser from sentry.users.services.user.service import user_service diff --git a/src/sentry/api/serializers/models/organization_member/expand/projects.py b/src/sentry/api/serializers/models/organization_member/expand/projects.py index 89d88ce8cdb756..65e4ad8c411196 100644 --- a/src/sentry/api/serializers/models/organization_member/expand/projects.py +++ b/src/sentry/api/serializers/models/organization_member/expand/projects.py @@ -6,7 +6,7 @@ from sentry.models.organizationmemberteam import OrganizationMemberTeam from sentry.models.projectteam import ProjectTeam from sentry.models.team import TeamStatus -from sentry.models.user import User +from sentry.users.models.user import User from ..base import OrganizationMemberSerializer from ..response import OrganizationMemberWithProjectsResponse diff --git a/src/sentry/api/serializers/models/organization_member/expand/roles.py b/src/sentry/api/serializers/models/organization_member/expand/roles.py index cfdae6b2990817..5cb2ac03545c3f 100644 --- a/src/sentry/api/serializers/models/organization_member/expand/roles.py +++ b/src/sentry/api/serializers/models/organization_member/expand/roles.py @@ -5,9 +5,9 @@ from sentry.api.serializers import serialize from sentry.models.organizationmember import OrganizationMember -from sentry.models.user import User from sentry.roles import organization_roles, team_roles from sentry.roles.manager import OrganizationRole, Role +from sentry.users.models.user import User from sentry.users.services.user import UserSerializeType from sentry.users.services.user.service import user_service diff --git a/src/sentry/api/serializers/models/organization_member/expand/teams.py b/src/sentry/api/serializers/models/organization_member/expand/teams.py index c75dba8ff27d56..b3e018a79fd865 100644 --- a/src/sentry/api/serializers/models/organization_member/expand/teams.py +++ b/src/sentry/api/serializers/models/organization_member/expand/teams.py @@ -2,7 +2,7 @@ from typing import Any, cast from sentry.models.organizationmember import OrganizationMember -from sentry.models.user import User +from sentry.users.models.user import User from ..base import OrganizationMemberSerializer from ..response import OrganizationMemberWithTeamsResponse diff --git a/src/sentry/api/serializers/models/project.py b/src/sentry/api/serializers/models/project.py index c6e7cab9377062..5744115afe28da 100644 --- a/src/sentry/api/serializers/models/project.py +++ b/src/sentry/api/serializers/models/project.py @@ -35,11 +35,11 @@ from sentry.models.projectplatform import ProjectPlatform from sentry.models.projectteam import ProjectTeam from sentry.models.release import Release -from sentry.models.user import User -from sentry.models.userreport import UserReport from sentry.release_health.base import CurrentAndPreviousCrashFreeRate from sentry.roles import organization_roles from sentry.snuba import discover +from sentry.users.models.user import User +from sentry.users.models.userreport import UserReport STATUS_LABELS = { ObjectStatus.ACTIVE: "active", diff --git a/src/sentry/api/serializers/models/relocation.py b/src/sentry/api/serializers/models/relocation.py index afb989bb4e3df4..16ea9f58363fb0 100644 --- a/src/sentry/api/serializers/models/relocation.py +++ b/src/sentry/api/serializers/models/relocation.py @@ -7,7 +7,7 @@ from sentry.api.serializers import Serializer, register from sentry.models.importchunk import BaseImportChunk, ControlImportChunkReplica, RegionImportChunk from sentry.models.relocation import Relocation -from sentry.models.user import User +from sentry.users.models.user import User from sentry.users.services.user.model import RpcUser from sentry.users.services.user.service import user_service diff --git a/src/sentry/api/serializers/models/role.py b/src/sentry/api/serializers/models/role.py index 4577b8f337e782..b283a7a65dcadf 100644 --- a/src/sentry/api/serializers/models/role.py +++ b/src/sentry/api/serializers/models/role.py @@ -3,8 +3,8 @@ from sentry import features from sentry.api.serializers import Serializer -from sentry.models.user import User from sentry.roles.manager import OrganizationRole, Role, TeamRole +from sentry.users.models.user import User class BaseRoleSerializerResponse(TypedDict): diff --git a/src/sentry/api/serializers/models/sentry_app.py b/src/sentry/api/serializers/models/sentry_app.py index d32190acc903a1..2e65155450e0a5 100644 --- a/src/sentry/api/serializers/models/sentry_app.py +++ b/src/sentry/api/serializers/models/sentry_app.py @@ -14,8 +14,8 @@ from sentry.models.avatars.sentry_app_avatar import SentryAppAvatar from sentry.models.integrations.integration_feature import IntegrationFeature, IntegrationTypes from sentry.models.integrations.sentry_app import MASKED_VALUE, SentryApp -from sentry.models.user import User from sentry.organizations.services.organization import organization_service +from sentry.users.models.user import User from sentry.users.services.user.service import user_service diff --git a/src/sentry/api/serializers/models/sentry_app_installation.py b/src/sentry/api/serializers/models/sentry_app_installation.py index 9f71348f2c1a75..b43111c1808579 100644 --- a/src/sentry/api/serializers/models/sentry_app_installation.py +++ b/src/sentry/api/serializers/models/sentry_app_installation.py @@ -8,7 +8,7 @@ from sentry.hybridcloud.services.organization_mapping import organization_mapping_service from sentry.models.integrations.sentry_app import SentryApp from sentry.models.integrations.sentry_app_installation import SentryAppInstallation -from sentry.models.user import User +from sentry.users.models.user import User from sentry.users.services.user import RpcUser diff --git a/src/sentry/api/serializers/models/team.py b/src/sentry/api/serializers/models/team.py index 2fa68ab6f0e571..54ff62675b6e8e 100644 --- a/src/sentry/api/serializers/models/team.py +++ b/src/sentry/api/serializers/models/team.py @@ -25,9 +25,9 @@ from sentry.models.organizationmemberteam import OrganizationMemberTeam from sentry.models.projectteam import ProjectTeam from sentry.models.team import Team -from sentry.models.user import User from sentry.roles import organization_roles, team_roles from sentry.scim.endpoints.constants import SCIM_SCHEMA_GROUP +from sentry.users.models.user import User from sentry.utils.query import RangeQuerySetWrapper if TYPE_CHECKING: diff --git a/src/sentry/api/serializers/models/user.py b/src/sentry/api/serializers/models/user.py index 11387cea215aaf..3d120ec4871a08 100644 --- a/src/sentry/api/serializers/models/user.py +++ b/src/sentry/api/serializers/models/user.py @@ -17,18 +17,18 @@ from sentry.app import env from sentry.auth.elevated_mode import has_elevated_mode from sentry.hybridcloud.services.organization_mapping import organization_mapping_service -from sentry.models.authenticator import Authenticator from sentry.models.authidentity import AuthIdentity from sentry.models.avatars.user_avatar import UserAvatar -from sentry.models.options.user_option import UserOption from sentry.models.organization import OrganizationStatus from sentry.models.organizationmapping import OrganizationMapping from sentry.models.organizationmembermapping import OrganizationMemberMapping -from sentry.models.user import User -from sentry.models.useremail import UserEmail -from sentry.models.userpermission import UserPermission -from sentry.models.userrole import UserRoleUser 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.user_option import UserOption +from sentry.users.models.useremail import UserEmail +from sentry.users.models.userpermission import UserPermission +from sentry.users.models.userrole import UserRoleUser from sentry.users.services.user import RpcUser from sentry.utils.avatar import get_gravatar_url diff --git a/src/sentry/api/serializers/models/useremail.py b/src/sentry/api/serializers/models/useremail.py index 087b36745aae1a..cba7547c87299f 100644 --- a/src/sentry/api/serializers/models/useremail.py +++ b/src/sentry/api/serializers/models/useremail.py @@ -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) diff --git a/src/sentry/api/serializers/models/userip.py b/src/sentry/api/serializers/models/userip.py index f765cae6da2487..4cb9570db5b0ea 100644 --- a/src/sentry/api/serializers/models/userip.py +++ b/src/sentry/api/serializers/models/userip.py @@ -1,5 +1,5 @@ from sentry.api.serializers import Serializer, register -from sentry.models.userip import UserIP +from sentry.users.models.userip import UserIP @register(UserIP) diff --git a/src/sentry/api/serializers/models/userreport.py b/src/sentry/api/serializers/models/userreport.py index 85be18bd7c0b15..ca326220968cb8 100644 --- a/src/sentry/api/serializers/models/userreport.py +++ b/src/sentry/api/serializers/models/userreport.py @@ -3,8 +3,8 @@ from sentry.eventstore.models import Event from sentry.models.group import Group from sentry.models.project import Project -from sentry.models.userreport import UserReport from sentry.snuba.dataset import Dataset +from sentry.users.models.userreport import UserReport from sentry.utils.eventuser import EventUser @@ -28,9 +28,11 @@ def get_attrs(self, item_list, user, **kwargs): events_dict: dict[str, Event] = {event.event_id: event for event in events} for item in item_list: attrs[item] = { - "event_user": EventUser.from_event(events_dict[item.event_id]) - if events_dict.get(item.event_id) - else {} + "event_user": ( + EventUser.from_event(events_dict[item.event_id]) + if events_dict.get(item.event_id) + else {} + ) } return attrs diff --git a/src/sentry/api/serializers/models/userrole.py b/src/sentry/api/serializers/models/userrole.py index 8d27330b90e68c..53379252a88429 100644 --- a/src/sentry/api/serializers/models/userrole.py +++ b/src/sentry/api/serializers/models/userrole.py @@ -1,5 +1,5 @@ from sentry.api.serializers import Serializer, register -from sentry.models.userrole import UserRole +from sentry.users.models.userrole import UserRole @register(UserRole) diff --git a/src/sentry/audit_log/services/log/impl.py b/src/sentry/audit_log/services/log/impl.py index 3e3aa199a57928..d7465640527e20 100644 --- a/src/sentry/audit_log/services/log/impl.py +++ b/src/sentry/audit_log/services/log/impl.py @@ -8,9 +8,9 @@ from sentry.db.postgres.transactions import enforce_constraints from sentry.models.auditlogentry import AuditLogEntry from sentry.models.outbox import OutboxCategory, OutboxScope, RegionOutbox -from sentry.models.user import User -from sentry.models.userip import UserIP from sentry.silo.safety import unguarded_write +from sentry.users.models.user import User +from sentry.users.models.userip import UserIP class DatabaseBackedLogService(LogService): diff --git a/src/sentry/auth/access.py b/src/sentry/auth/access.py index 8ac472ddfb8fb3..59808f32db8569 100644 --- a/src/sentry/auth/access.py +++ b/src/sentry/auth/access.py @@ -35,11 +35,11 @@ from sentry.models.organizationmemberteam import OrganizationMemberTeam from sentry.models.project import Project from sentry.models.team import Team, TeamStatus -from sentry.models.user import User from sentry.organizations.services.organization import RpcTeamMember, RpcUserOrganizationContext from sentry.organizations.services.organization.serial import summarize_member from sentry.roles import organization_roles from sentry.roles.manager import OrganizationRole, TeamRole +from sentry.users.models.user import User from sentry.users.services.user import RpcUser from sentry.utils import metrics diff --git a/src/sentry/auth/authenticators/base.py b/src/sentry/auth/authenticators/base.py index d292c6bf0c46c3..d9b9aa42bbd15b 100644 --- a/src/sentry/auth/authenticators/base.py +++ b/src/sentry/auth/authenticators/base.py @@ -14,8 +14,8 @@ if TYPE_CHECKING: from django.utils.functional import _StrPromise - from sentry.models.authenticator import Authenticator - from sentry.models.user import User + from sentry.users.models.authenticator import Authenticator + from sentry.users.models.user import User class ActivationResult: @@ -150,7 +150,7 @@ def enroll(self, user: User) -> None: If `disallow_new_enrollment` is `True`, raises exception: `NewEnrollmentDisallowed`. """ - from sentry.models.authenticator import Authenticator + from sentry.users.models.authenticator import Authenticator if self.disallow_new_enrollment: raise NewEnrollmentDisallowed diff --git a/src/sentry/auth/email.py b/src/sentry/auth/email.py index 0b95304c33d2c8..dff490a775fcf8 100644 --- a/src/sentry/auth/email.py +++ b/src/sentry/auth/email.py @@ -5,9 +5,9 @@ from dataclasses import dataclass from sentry.models.organization import Organization -from sentry.models.user import User -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 diff --git a/src/sentry/auth/helper.py b/src/sentry/auth/helper.py index b6ec5696417cf5..e85c5b0f48c682 100644 --- a/src/sentry/auth/helper.py +++ b/src/sentry/auth/helper.py @@ -41,7 +41,6 @@ from sentry.models.authidentity import AuthIdentity from sentry.models.authprovider import AuthProvider from sentry.models.outbox import outbox_context -from sentry.models.user import User from sentry.organizations.services.organization import ( RpcOrganization, RpcOrganizationFlagsUpdate, @@ -53,6 +52,7 @@ from sentry.pipeline.provider import PipelineProvider from sentry.signals import sso_enabled, user_signup from sentry.tasks.auth import email_missing_links_control +from sentry.users.models.user import User from sentry.utils import auth, metrics from sentry.utils.audit import create_audit_entry from sentry.utils.hashlib import md5_text diff --git a/src/sentry/auth/idpmigration.py b/src/sentry/auth/idpmigration.py index 3799f406627036..a31beb5adfb28b 100644 --- a/src/sentry/auth/idpmigration.py +++ b/src/sentry/auth/idpmigration.py @@ -10,8 +10,8 @@ from sentry import options from sentry.models.authprovider import AuthProvider -from sentry.models.user import User from sentry.organizations.services.organization import RpcOrganization, organization_service +from sentry.users.models.user import User from sentry.utils import metrics, redis from sentry.utils.email import MessageBuilder from sentry.utils.http import absolute_uri diff --git a/src/sentry/auth/provider.py b/src/sentry/auth/provider.py index d757926df890eb..245ce9371440fb 100644 --- a/src/sentry/auth/provider.py +++ b/src/sentry/auth/provider.py @@ -12,9 +12,9 @@ from sentry.models.authidentity import AuthIdentity from sentry.models.authprovider import AuthProvider from sentry.models.organization import Organization -from sentry.models.user import User from sentry.organizations.services.organization.model import RpcOrganization from sentry.pipeline import PipelineProvider +from sentry.users.models.user import User from .view import AuthView, ConfigureView diff --git a/src/sentry/auth/services/auth/impl.py b/src/sentry/auth/services/auth/impl.py index 1efa0685085d6a..c0e2e6aecd38f2 100644 --- a/src/sentry/auth/services/auth/impl.py +++ b/src/sentry/auth/services/auth/impl.py @@ -21,10 +21,10 @@ from sentry.models.authprovider import AuthProvider from sentry.models.organizationmembermapping import OrganizationMemberMapping from sentry.models.outbox import outbox_context -from sentry.models.user import User from sentry.organizations.services.organization.service import organization_service from sentry.signals import sso_enabled from sentry.silo.safety import unguarded_write +from sentry.users.models.user import User class DatabaseBackedAuthService(AuthService): diff --git a/src/sentry/backup/dependencies.py b/src/sentry/backup/dependencies.py index fce212e17de8da..a768f93afa7c6e 100644 --- a/src/sentry/backup/dependencies.py +++ b/src/sentry/backup/dependencies.py @@ -712,7 +712,7 @@ def merge_users_for_model_in_org( """ from sentry.models.organization import Organization - from sentry.models.user import User + from sentry.users.models.user import User model_relations = dependencies()[get_model_name(model)] user_refs = {k for k, v in model_relations.foreign_keys.items() if v.model == User} diff --git a/src/sentry/backup/exports.py b/src/sentry/backup/exports.py index d07da83e16f779..ab7c10d93a6402 100644 --- a/src/sentry/backup/exports.py +++ b/src/sentry/backup/exports.py @@ -61,7 +61,7 @@ def _export( # Import here to prevent circular module resolutions. from sentry.models.organization import Organization from sentry.models.organizationmember import OrganizationMember - from sentry.models.user import User + from sentry.users.models.user import User if SiloMode.get_current_mode() == SiloMode.CONTROL: errText = "Exports must be run in REGION or MONOLITH instances only" @@ -165,7 +165,7 @@ def export_in_user_scope( """ # Import here to prevent circular module resolutions. - from sentry.models.user import User + from sentry.users.models.user import User return _export( dest, @@ -217,7 +217,7 @@ def export_in_config_scope( """ # Import here to prevent circular module resolutions. - from sentry.models.user import User + from sentry.users.models.user import User return _export( dest, diff --git a/src/sentry/backup/imports.py b/src/sentry/backup/imports.py index c479ccf0d73acd..d1af85d5332626 100644 --- a/src/sentry/backup/imports.py +++ b/src/sentry/backup/imports.py @@ -104,7 +104,7 @@ def _import( from sentry.models.email import Email from sentry.models.organization import Organization from sentry.models.organizationmember import OrganizationMember - from sentry.models.user import User + from sentry.users.models.user import User if SiloMode.get_current_mode() == SiloMode.CONTROL: errText = "Imports must be run in REGION or MONOLITH instances only" @@ -505,7 +505,7 @@ def import_in_user_scope( """ # Import here to prevent circular module resolutions. - from sentry.models.user import User + from sentry.users.models.user import User return _import( src, @@ -568,7 +568,7 @@ def import_in_config_scope( """ # Import here to prevent circular module resolutions. - from sentry.models.user import User + from sentry.users.models.user import User return _import( src, diff --git a/src/sentry/backup/services/import_export/impl.py b/src/sentry/backup/services/import_export/impl.py index d24f35bb770f48..b2a0211c85accf 100644 --- a/src/sentry/backup/services/import_export/impl.py +++ b/src/sentry/backup/services/import_export/impl.py @@ -47,10 +47,10 @@ from sentry.models.importchunk import ControlImportChunk, RegionImportChunk from sentry.models.organizationmember import OrganizationMember from sentry.models.outbox import outbox_context -from sentry.models.user import User -from sentry.models.userpermission import UserPermission -from sentry.models.userrole import UserRoleUser from sentry.silo.base import SiloMode +from sentry.users.models.user import User +from sentry.users.models.userpermission import UserPermission +from sentry.users.models.userrole import UserRoleUser logger = logging.getLogger(__name__) diff --git a/src/sentry/backup/validate.py b/src/sentry/backup/validate.py index d4bb7f76ae4692..55114cdcd076c7 100644 --- a/src/sentry/backup/validate.py +++ b/src/sentry/backup/validate.py @@ -91,7 +91,7 @@ def build_model_map( """Does two things in tandem: builds a map of InstanceID -> JSON model, and simultaneously builds a map of model name -> number of ordinals assigned.""" from sentry.db.models import BaseModel - from sentry.models.user import User + from sentry.users.models.user import User model_map: ModelMap = defaultdict(ordereddict) ordinal_counters: OrdinalCounters = defaultdict(OrdinalCounter) diff --git a/src/sentry/deletions/defaults/project.py b/src/sentry/deletions/defaults/project.py index 9d34ce5b533c1c..1811c180e9e82d 100644 --- a/src/sentry/deletions/defaults/project.py +++ b/src/sentry/deletions/defaults/project.py @@ -36,10 +36,10 @@ def get_child_relations(self, instance): from sentry.models.releases.release_project import ReleaseProject from sentry.models.servicehook import ServiceHook, ServiceHookProject from sentry.models.transaction_threshold import ProjectTransactionThreshold - from sentry.models.userreport import UserReport from sentry.monitors.models import Monitor from sentry.replays.models import ReplayRecordingSegment from sentry.snuba.models import QuerySubscription + from sentry.users.models.userreport import UserReport relations = [ # ProjectKey gets revoked immediately, in bulk diff --git a/src/sentry/deletions/defaults/sentry_app.py b/src/sentry/deletions/defaults/sentry_app.py index 3a1169e4f02474..c3a6f7e66c22e3 100644 --- a/src/sentry/deletions/defaults/sentry_app.py +++ b/src/sentry/deletions/defaults/sentry_app.py @@ -5,7 +5,7 @@ class SentryAppDeletionTask(ModelDeletionTask): def get_child_relations(self, instance): from sentry.models.apiapplication import ApiApplication from sentry.models.integrations.sentry_app_installation import SentryAppInstallation - from sentry.models.user import User + from sentry.users.models.user import User return [ ModelRelation(SentryAppInstallation, {"sentry_app_id": instance.id}), diff --git a/src/sentry/features/base.py b/src/sentry/features/base.py index 85b5d57cdb5701..13b0063fe8f9bf 100644 --- a/src/sentry/features/base.py +++ b/src/sentry/features/base.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: from sentry.models.organization import Organization from sentry.models.project import Project - from sentry.models.user import User + from sentry.users.models.user import User class Feature: diff --git a/src/sentry/features/flagpole_context.py b/src/sentry/features/flagpole_context.py index 89664058b5d40e..e32694b9b3e71c 100644 --- a/src/sentry/features/flagpole_context.py +++ b/src/sentry/features/flagpole_context.py @@ -6,10 +6,10 @@ from sentry.hybridcloud.services.organization_mapping.model import RpcOrganizationMapping from sentry.models.organization import Organization from sentry.models.project import Project -from sentry.models.user import User from sentry.organizations.services.organization import RpcOrganization from sentry.organizations.services.organization.model import RpcOrganizationSummary from sentry.projects.services.project import RpcProject +from sentry.users.models.user import User from sentry.users.services.user import RpcUser @@ -20,9 +20,9 @@ class InvalidContextDataException(Exception): @dataclass() class SentryContextData: actor: User | RpcUser | AnonymousUser | None = None - organization: Organization | RpcOrganization | RpcOrganizationSummary | RpcOrganizationMapping | None = ( - None - ) + organization: ( + Organization | RpcOrganization | RpcOrganizationSummary | RpcOrganizationMapping | None + ) = None project: Project | RpcProject | None = None diff --git a/src/sentry/features/handler.py b/src/sentry/features/handler.py index 182b8104a92e1d..9b0935f087018c 100644 --- a/src/sentry/features/handler.py +++ b/src/sentry/features/handler.py @@ -13,7 +13,7 @@ from sentry.features.manager import FeatureCheckBatch from sentry.models.organization import Organization from sentry.models.project import Project - from sentry.models.user import User + from sentry.users.models.user import User class FeatureHandler: diff --git a/src/sentry/features/manager.py b/src/sentry/features/manager.py index 33df24d31a3045..9d414307e949a2 100644 --- a/src/sentry/features/manager.py +++ b/src/sentry/features/manager.py @@ -25,7 +25,7 @@ from sentry.features.handler import FeatureHandler from sentry.models.organization import Organization from sentry.models.project import Project - from sentry.models.user import User + from sentry.users.models.user import User logger = logging.getLogger(__name__) diff --git a/src/sentry/hybridcloud/rpc/__init__.py b/src/sentry/hybridcloud/rpc/__init__.py index a417251e9a93be..4be99bd2586623 100644 --- a/src/sentry/hybridcloud/rpc/__init__.py +++ b/src/sentry/hybridcloud/rpc/__init__.py @@ -219,7 +219,7 @@ def get_delegated_constructors( def delegator() -> ServiceInterface: from sentry.models.organization import Organization - from sentry.models.user import User + from sentry.users.models.user import User return cast( ServiceInterface, diff --git a/src/sentry/hybridcloud/services/organizationmember_mapping/impl.py b/src/sentry/hybridcloud/services/organizationmember_mapping/impl.py index 1d5ddee9377cc5..be329c8b342ae4 100644 --- a/src/sentry/hybridcloud/services/organizationmember_mapping/impl.py +++ b/src/sentry/hybridcloud/services/organizationmember_mapping/impl.py @@ -16,8 +16,8 @@ ) from sentry.models.organizationmembermapping import OrganizationMemberMapping from sentry.models.outbox import outbox_context -from sentry.models.user import User from sentry.silo.safety import unguarded_write +from sentry.users.models.user import User class DatabaseBackedOrganizationMemberMappingService(OrganizationMemberMappingService): diff --git a/src/sentry/hybridcloud/services/replica/impl.py b/src/sentry/hybridcloud/services/replica/impl.py index 1db29ecd184055..722697214b9b1f 100644 --- a/src/sentry/hybridcloud/services/replica/impl.py +++ b/src/sentry/hybridcloud/services/replica/impl.py @@ -36,9 +36,9 @@ from sentry.models.outbox import OutboxCategory from sentry.models.team import Team from sentry.models.teamreplica import TeamReplica -from sentry.models.user import User from sentry.notifications.services import RpcExternalActor from sentry.organizations.services.organization import RpcOrganizationMemberTeam, RpcTeam +from sentry.users.models.user import User def get_foreign_key_columns( diff --git a/src/sentry/incidents/action_handlers.py b/src/sentry/incidents/action_handlers.py index 68f10256215cfb..c7ddc9aa02ab72 100644 --- a/src/sentry/incidents/action_handlers.py +++ b/src/sentry/incidents/action_handlers.py @@ -28,12 +28,12 @@ ) from sentry.integrations.types import ExternalProviders from sentry.models.rulesnooze import RuleSnooze -from sentry.models.user import User from sentry.notifications.types import NotificationSettingEnum from sentry.notifications.utils.participants import get_notification_recipients from sentry.snuba.metrics import format_mri_field, is_mri_field from sentry.snuba.utils import build_query_strings from sentry.types.actor import Actor, ActorType +from sentry.users.models.user import User from sentry.users.services.user import RpcUser from sentry.users.services.user.service import user_service from sentry.users.services.user_option import RpcUserOption, user_option_service diff --git a/src/sentry/incidents/charts.py b/src/sentry/incidents/charts.py index 44e561ce81884b..369f3e13c91586 100644 --- a/src/sentry/incidents/charts.py +++ b/src/sentry/incidents/charts.py @@ -19,12 +19,12 @@ from sentry.incidents.models.incident import Incident from sentry.models.apikey import ApiKey from sentry.models.organization import Organization -from sentry.models.user import User from sentry.snuba.dataset import Dataset from sentry.snuba.entity_subscription import apply_dataset_query_conditions from sentry.snuba.models import QuerySubscription, SnubaQuery from sentry.snuba.referrer import Referrer from sentry.snuba.utils import build_query_strings +from sentry.users.models.user import User CRASH_FREE_SESSIONS = "percentage(sessions_crashed, sessions) AS _crash_rate_alert_aggregate" CRASH_FREE_USERS = "percentage(users_crashed, users) AS _crash_rate_alert_aggregate" diff --git a/src/sentry/incidents/endpoints/serializers/alert_rule.py b/src/sentry/incidents/endpoints/serializers/alert_rule.py index b3a7fbbeab92cd..1e84937068069c 100644 --- a/src/sentry/incidents/endpoints/serializers/alert_rule.py +++ b/src/sentry/incidents/endpoints/serializers/alert_rule.py @@ -26,10 +26,10 @@ from sentry.models.integrations.sentry_app_installation import prepare_ui_component from sentry.models.rule import Rule from sentry.models.rulesnooze import RuleSnooze -from sentry.models.user import User from sentry.sentry_apps.services.app import app_service from sentry.sentry_apps.services.app.model import RpcSentryAppComponentContext from sentry.snuba.models import SnubaQueryEventType +from sentry.users.models.user import User from sentry.users.services.user import RpcUser from sentry.users.services.user.service import user_service diff --git a/src/sentry/ingest/userreport.py b/src/sentry/ingest/userreport.py index 5dacd47dc02a57..be82dfbd3c0758 100644 --- a/src/sentry/ingest/userreport.py +++ b/src/sentry/ingest/userreport.py @@ -12,8 +12,8 @@ UNREAL_FEEDBACK_UNATTENDED_MESSAGE, shim_to_feedback, ) -from sentry.models.userreport import UserReport from sentry.signals import user_feedback_received +from sentry.users.models.userreport import UserReport from sentry.utils import metrics from sentry.utils.db import atomic_transaction from sentry.utils.eventuser import EventUser diff --git a/src/sentry/integrations/aws_lambda/integration.py b/src/sentry/integrations/aws_lambda/integration.py index 270374ddf17747..94f6bfe0b6d5e7 100644 --- a/src/sentry/integrations/aws_lambda/integration.py +++ b/src/sentry/integrations/aws_lambda/integration.py @@ -21,11 +21,11 @@ from sentry.integrations.mixins import ServerlessMixin from sentry.models.integrations.integration import Integration from sentry.models.integrations.organization_integration import OrganizationIntegration -from sentry.models.user import User from sentry.organizations.services.organization import RpcOrganizationSummary, organization_service from sentry.pipeline import PipelineView from sentry.projects.services.project import project_service from sentry.silo.base import control_silo_function +from sentry.users.models.user import User from sentry.users.services.user.serial import serialize_rpc_user from sentry.utils.sdk import capture_exception @@ -289,9 +289,9 @@ def dispatch(self, request: Request, pipeline) -> Response: def render_response(error=None): serialized_organization = organization_service.serialize_organization( id=pipeline.organization.id, - as_user=serialize_rpc_user(request.user) - if isinstance(request.user, User) - else None, + as_user=( + serialize_rpc_user(request.user) if isinstance(request.user, User) else None + ), ) template_url = options.get("aws-lambda.cloudformation-url") context = { diff --git a/src/sentry/integrations/bitbucket/issues.py b/src/sentry/integrations/bitbucket/issues.py index 3cf89d202c721b..cb548a5031e1c1 100644 --- a/src/sentry/integrations/bitbucket/issues.py +++ b/src/sentry/integrations/bitbucket/issues.py @@ -7,9 +7,9 @@ from sentry.integrations.mixins import IssueBasicMixin from sentry.models.group import Group -from sentry.models.user import User from sentry.shared_integrations.exceptions import ApiError, IntegrationFormError from sentry.silo.base import all_silo_function +from sentry.users.models.user import User ISSUE_TYPES = ( ("bug", "Bug"), diff --git a/src/sentry/integrations/github/issues.py b/src/sentry/integrations/github/issues.py index 9366e95dccaa5a..69e46abd6d62ef 100644 --- a/src/sentry/integrations/github/issues.py +++ b/src/sentry/integrations/github/issues.py @@ -12,10 +12,10 @@ from sentry.issues.grouptype import GroupCategory from sentry.models.group import Group from sentry.models.integrations.external_issue import ExternalIssue -from sentry.models.user import User from sentry.organizations.services.organization.service import organization_service from sentry.shared_integrations.exceptions import ApiError, IntegrationError from sentry.silo.base import all_silo_function +from sentry.users.models.user import User from sentry.utils.http import absolute_uri from sentry.utils.strings import truncatechars diff --git a/src/sentry/integrations/gitlab/issues.py b/src/sentry/integrations/gitlab/issues.py index 9266a2f964f07e..186a75bbf559f9 100644 --- a/src/sentry/integrations/gitlab/issues.py +++ b/src/sentry/integrations/gitlab/issues.py @@ -8,9 +8,9 @@ from sentry.integrations.mixins import IssueBasicMixin from sentry.models.group import Group -from sentry.models.user import User from sentry.shared_integrations.exceptions import ApiError, ApiUnauthorized, IntegrationError from sentry.silo.base import all_silo_function +from sentry.users.models.user import User from sentry.utils.http import absolute_uri ISSUE_EXTERNAL_KEY_FORMAT = re.compile(r".+:(.+)#(.+)") diff --git a/src/sentry/integrations/mixins/issues.py b/src/sentry/integrations/mixins/issues.py index 4001864210380a..c0476d354c121e 100644 --- a/src/sentry/integrations/mixins/issues.py +++ b/src/sentry/integrations/mixins/issues.py @@ -14,11 +14,11 @@ from sentry.models.grouplink import GroupLink from sentry.models.integrations.external_issue import ExternalIssue from sentry.models.project import Project -from sentry.models.user import User from sentry.notifications.utils import get_notification_group_title from sentry.shared_integrations.exceptions import ApiError, IntegrationError from sentry.silo.base import all_silo_function from sentry.tasks.integrations import sync_status_inbound as sync_status_inbound_task +from sentry.users.models.user import User from sentry.users.services.user import RpcUser from sentry.users.services.user_option import get_option_from_list, user_option_service from sentry.utils.http import absolute_uri diff --git a/src/sentry/integrations/msteams/notifications.py b/src/sentry/integrations/msteams/notifications.py index 328049488fbfce..fca1a6152a81c9 100644 --- a/src/sentry/integrations/msteams/notifications.py +++ b/src/sentry/integrations/msteams/notifications.py @@ -11,7 +11,6 @@ from sentry.integrations.notifications import get_context, get_integrations_by_channel_by_recipient from sentry.integrations.types import ExternalProviders from sentry.models.team import Team -from sentry.models.user import User from sentry.notifications.notifications.activity.assigned import AssignedActivityNotification from sentry.notifications.notifications.activity.escalating import EscalatingActivityNotification from sentry.notifications.notifications.activity.note import NoteActivityNotification @@ -26,6 +25,7 @@ from sentry.notifications.notifications.rules import AlertRuleNotification from sentry.notifications.notify import register_notification_provider from sentry.types.actor import Actor +from sentry.users.models.user import User from sentry.utils import metrics from .card_builder.notifications import ( diff --git a/src/sentry/integrations/slack/unfurl/__init__.py b/src/sentry/integrations/slack/unfurl/__init__.py index 3df9e18a58cb5f..12746fb43e85f4 100644 --- a/src/sentry/integrations/slack/unfurl/__init__.py +++ b/src/sentry/integrations/slack/unfurl/__init__.py @@ -8,7 +8,7 @@ from django.http.request import HttpRequest from sentry.models.integrations.integration import Integration -from sentry.models.user import User +from sentry.users.models.user import User UnfurledUrl = Mapping[Any, Any] ArgsMapper = Callable[[str, Mapping[str, Optional[str]]], Mapping[str, Any]] diff --git a/src/sentry/integrations/slack/unfurl/discover.py b/src/sentry/integrations/slack/unfurl/discover.py index fe8f7a57a56c18..5489f59e7dcbaa 100644 --- a/src/sentry/integrations/slack/unfurl/discover.py +++ b/src/sentry/integrations/slack/unfurl/discover.py @@ -19,8 +19,8 @@ from sentry.models.apikey import ApiKey from sentry.models.integrations.integration import Integration from sentry.models.organization import Organization -from sentry.models.user import User from sentry.search.events.filter import to_list +from sentry.users.models.user import User from sentry.utils.dates import ( get_interval_from_range, parse_stats_period, diff --git a/src/sentry/integrations/slack/unfurl/issues.py b/src/sentry/integrations/slack/unfurl/issues.py index 8f223ece3199c3..f0b32ba8323568 100644 --- a/src/sentry/integrations/slack/unfurl/issues.py +++ b/src/sentry/integrations/slack/unfurl/issues.py @@ -10,7 +10,7 @@ from sentry.models.group import Group from sentry.models.integrations.integration import Integration from sentry.models.project import Project -from sentry.models.user import User +from sentry.users.models.user import User from . import Handler, UnfurlableUrl, UnfurledUrl, make_type_coercer diff --git a/src/sentry/integrations/slack/unfurl/metric_alerts.py b/src/sentry/integrations/slack/unfurl/metric_alerts.py index ab62a8e1cfd31e..663f10a479caf7 100644 --- a/src/sentry/integrations/slack/unfurl/metric_alerts.py +++ b/src/sentry/integrations/slack/unfurl/metric_alerts.py @@ -18,7 +18,7 @@ from sentry.integrations.slack.message_builder.metric_alerts import SlackMetricAlertMessageBuilder from sentry.models.integrations.integration import Integration from sentry.models.organization import Organization -from sentry.models.user import User +from sentry.users.models.user import User from . import Handler, UnfurlableUrl, UnfurledUrl, make_type_coercer diff --git a/src/sentry/integrations/slack/utils/users.py b/src/sentry/integrations/slack/utils/users.py index c1fc26f523a259..5707c24e8a4ae2 100644 --- a/src/sentry/integrations/slack/utils/users.py +++ b/src/sentry/integrations/slack/utils/users.py @@ -10,8 +10,8 @@ from sentry.integrations.slack.sdk_client import SlackSdkClient from sentry.models.integrations.integration import Integration from sentry.models.organization import Organization -from sentry.models.user import User from sentry.organizations.services.organization import RpcOrganization +from sentry.users.models.user import User from ..utils import logger diff --git a/src/sentry/integrations/utils/identities.py b/src/sentry/integrations/utils/identities.py index a94e52217fcd14..add6eb5253dea4 100644 --- a/src/sentry/integrations/utils/identities.py +++ b/src/sentry/integrations/utils/identities.py @@ -8,9 +8,9 @@ from sentry.models.identity import Identity, IdentityProvider, IdentityStatus from sentry.models.integrations.integration import Integration from sentry.models.integrations.organization_integration import OrganizationIntegration -from sentry.models.user import User from sentry.organizations.services.organization import RpcOrganization, organization_service from sentry.silo.base import control_silo_function +from sentry.users.models.user import User from sentry.users.services.user.service import user_service _logger = logging.getLogger(__name__) diff --git a/src/sentry/integrations/vercel/integration.py b/src/sentry/integrations/vercel/integration.py index 51b645fd7d056d..5b691843e99b35 100644 --- a/src/sentry/integrations/vercel/integration.py +++ b/src/sentry/integrations/vercel/integration.py @@ -24,11 +24,11 @@ SentryAppInstallationForProvider, ) from sentry.models.integrations.sentry_app_installation_token import SentryAppInstallationToken -from sentry.models.user import User from sentry.organizations.services.organization import RpcOrganizationSummary from sentry.pipeline import NestedPipelineView from sentry.projects.services.project_key import project_key_service from sentry.shared_integrations.exceptions import ApiError, IntegrationError +from sentry.users.models.user import User from sentry.utils.http import absolute_uri from ...sentry_apps.apps import SentryAppCreator diff --git a/src/sentry/issues/grouptype.py b/src/sentry/issues/grouptype.py index 48e3934e919fac..c9e6d548cc5c45 100644 --- a/src/sentry/issues/grouptype.py +++ b/src/sentry/issues/grouptype.py @@ -19,7 +19,7 @@ if TYPE_CHECKING: from sentry.models.organization import Organization from sentry.models.project import Project - from sentry.models.user import User + from sentry.users.models.user import User class GroupCategory(Enum): diff --git a/src/sentry/issues/ignored.py b/src/sentry/issues/ignored.py index 6b81124d8b3d47..f1ef37421d60b0 100644 --- a/src/sentry/issues/ignored.py +++ b/src/sentry/issues/ignored.py @@ -14,10 +14,10 @@ from sentry.models.groupinbox import GroupInboxRemoveAction, remove_group_from_inbox from sentry.models.groupsnooze import GroupSnooze from sentry.models.project import Project -from sentry.models.user import User from sentry.signals import issue_archived from sentry.snuba.referrer import Referrer from sentry.types.group import GroupSubStatus +from sentry.users.models.user import User from sentry.users.services.user import RpcUser from sentry.users.services.user.serial import serialize_generic_user from sentry.users.services.user.service import user_service diff --git a/src/sentry/issues/merge.py b/src/sentry/issues/merge.py index 02ddb293326761..9d79c9f3e35e42 100644 --- a/src/sentry/issues/merge.py +++ b/src/sentry/issues/merge.py @@ -11,9 +11,9 @@ from sentry.models.activity import Activity from sentry.models.group import Group, GroupStatus from sentry.models.project import Project -from sentry.models.user import User from sentry.tasks.merge import merge_groups from sentry.types.activity import ActivityType +from sentry.users.models.user import User class MergedGroup(TypedDict): diff --git a/src/sentry/issues/priority.py b/src/sentry/issues/priority.py index ea9646e5cf2913..c42aef02b43656 100644 --- a/src/sentry/issues/priority.py +++ b/src/sentry/issues/priority.py @@ -7,10 +7,10 @@ from sentry.models.activity import Activity from sentry.models.grouphistory import GroupHistoryStatus, record_group_history from sentry.models.project import Project -from sentry.models.user import User from sentry.signals import issue_update_priority from sentry.types.activity import ActivityType from sentry.types.group import PriorityLevel +from sentry.users.models.user import User from sentry.users.services.user.model import RpcUser if TYPE_CHECKING: diff --git a/src/sentry/issues/status_change.py b/src/sentry/issues/status_change.py index 7841668c4acfb5..d915cadbb58aa4 100644 --- a/src/sentry/issues/status_change.py +++ b/src/sentry/issues/status_change.py @@ -12,11 +12,11 @@ from sentry.models.grouphistory import record_group_history_from_activity_type from sentry.models.groupsubscription import GroupSubscription from sentry.models.project import Project -from sentry.models.user import User from sentry.notifications.types import GroupSubscriptionReason from sentry.signals import issue_ignored, issue_unignored, issue_unresolved from sentry.tasks.integrations import kick_off_status_syncs from sentry.types.activity import ActivityType +from sentry.users.models.user import User from sentry.utils import json ActivityInfo = namedtuple("ActivityInfo", ("activity_type", "activity_data")) diff --git a/src/sentry/issues/update_inbox.py b/src/sentry/issues/update_inbox.py index 195ab864ca3869..18421e9458e79a 100644 --- a/src/sentry/issues/update_inbox.py +++ b/src/sentry/issues/update_inbox.py @@ -11,9 +11,9 @@ remove_group_from_inbox, ) from sentry.models.project import Project -from sentry.models.user import User from sentry.signals import issue_mark_reviewed from sentry.types.group import GroupSubStatus +from sentry.users.models.user import User def update_inbox( diff --git a/src/sentry/management/commands/merge_users.py b/src/sentry/management/commands/merge_users.py index 76fc4e2e039d1d..c810dd2f3e58e0 100644 --- a/src/sentry/management/commands/merge_users.py +++ b/src/sentry/management/commands/merge_users.py @@ -6,7 +6,7 @@ from django.db.models import Q from sentry.models.organization import Organization -from sentry.models.user import User +from sentry.users.models.user import User class Command(BaseCommand): diff --git a/src/sentry/mediators/token_exchange/grant_exchanger.py b/src/sentry/mediators/token_exchange/grant_exchanger.py index c9c59a4d11a9e9..3519b19187e6d4 100644 --- a/src/sentry/mediators/token_exchange/grant_exchanger.py +++ b/src/sentry/mediators/token_exchange/grant_exchanger.py @@ -14,9 +14,9 @@ from sentry.models.apitoken import ApiToken from sentry.models.integrations.sentry_app import SentryApp from sentry.models.integrations.sentry_app_installation import SentryAppInstallation -from sentry.models.user import User from sentry.sentry_apps.services.app import RpcSentryAppInstallation from sentry.silo.safety import unguarded_write +from sentry.users.models.user import User class GrantExchanger(Mediator): diff --git a/src/sentry/mediators/token_exchange/refresher.py b/src/sentry/mediators/token_exchange/refresher.py index dbc704adc16bab..6f731f39876200 100644 --- a/src/sentry/mediators/token_exchange/refresher.py +++ b/src/sentry/mediators/token_exchange/refresher.py @@ -11,8 +11,8 @@ from sentry.models.apitoken import ApiToken from sentry.models.integrations.sentry_app import SentryApp from sentry.models.integrations.sentry_app_installation import SentryAppInstallation -from sentry.models.user import User from sentry.sentry_apps.services.app import RpcSentryAppInstallation +from sentry.users.models.user import User class Refresher(Mediator): diff --git a/src/sentry/mediators/token_exchange/validator.py b/src/sentry/mediators/token_exchange/validator.py index b86374cea19624..c6c0c1dc1d9fc0 100644 --- a/src/sentry/mediators/token_exchange/validator.py +++ b/src/sentry/mediators/token_exchange/validator.py @@ -6,8 +6,8 @@ from sentry.mediators.param import Param from sentry.models.apiapplication import ApiApplication from sentry.models.integrations.sentry_app import SentryApp -from sentry.models.user import User from sentry.sentry_apps.services.app import RpcSentryAppInstallation +from sentry.users.models.user import User class Validator(Mediator): diff --git a/src/sentry/middleware/auth.py b/src/sentry/middleware/auth.py index bf7dc3a78486c1..e6e9f74ab1c28c 100644 --- a/src/sentry/middleware/auth.py +++ b/src/sentry/middleware/auth.py @@ -14,7 +14,7 @@ OrgAuthTokenAuthentication, UserAuthTokenAuthentication, ) -from sentry.models.userip import UserIP +from sentry.users.models.userip import UserIP from sentry.utils.auth import AuthUserPasswordExpired, logger diff --git a/src/sentry/migrations/0001_squashed_0484_break_org_member_user_fk.py b/src/sentry/migrations/0001_squashed_0484_break_org_member_user_fk.py index 3b9a0967c7abcc..d69f090c82b520 100644 --- a/src/sentry/migrations/0001_squashed_0484_break_org_member_user_fk.py +++ b/src/sentry/migrations/0001_squashed_0484_break_org_member_user_fk.py @@ -28,14 +28,14 @@ import sentry.models.apiapplication import sentry.models.apigrant import sentry.models.apitoken -import sentry.models.authenticator import sentry.models.broadcast import sentry.models.groupshare import sentry.models.integrations.sentry_app import sentry.models.integrations.sentry_app_installation import sentry.models.scheduledeletion import sentry.models.servicehook -import sentry.models.user +import sentry.users.models.authenticator +import sentry.users.models.user import sentry.utils.security.hash from sentry.new_migrations.migrations import CheckedMigration @@ -390,7 +390,7 @@ class Migration(CheckedMigration): "verbose_name_plural": "users", }, managers=[ - ("objects", sentry.models.user.UserManager(cache_fields=["pk"])), + ("objects", sentry.users.models.user.UserManager(cache_fields=["pk"])), ], ), migrations.CreateModel( @@ -9149,7 +9149,7 @@ class Migration(CheckedMigration): migrations.AlterField( model_name="authenticator", name="config", - field=sentry.models.authenticator.AuthenticatorConfig(editable=False), + field=sentry.users.models.authenticator.AuthenticatorConfig(editable=False), ), migrations.CreateModel( name="MonitorEnvironment", diff --git a/src/sentry/models/activity.py b/src/sentry/models/activity.py index cfe925edc3ee89..96fe1131969112 100644 --- a/src/sentry/models/activity.py +++ b/src/sentry/models/activity.py @@ -29,7 +29,7 @@ if TYPE_CHECKING: from sentry.models.group import Group - from sentry.models.user import User + from sentry.users.models.user import User from sentry.users.services.user import RpcUser diff --git a/src/sentry/models/auditlogentry.py b/src/sentry/models/auditlogentry.py index de014d8fdd9e09..1f45b39ead3675 100644 --- a/src/sentry/models/auditlogentry.py +++ b/src/sentry/models/auditlogentry.py @@ -130,7 +130,7 @@ def from_event(cls, event: AuditLogEvent) -> AuditLogEntry: could have been created from previous code versions -- the events are stored on an async queue for indefinite delivery and from possibly older code versions. """ - from sentry.models.user import User + from sentry.users.models.user import User if event.actor_label: label = event.actor_label[:MAX_ACTOR_LABEL_LENGTH] diff --git a/src/sentry/models/authenticator.py b/src/sentry/models/authenticator.py index df0f03ff4cbcc8..3b23fcc7752405 100644 --- a/src/sentry/models/authenticator.py +++ b/src/sentry/models/authenticator.py @@ -1,204 +1,3 @@ -from __future__ import annotations +from sentry.users.models.authenticator import Authenticator, AuthenticatorConfig -import base64 -import copy -from typing import Any, ClassVar - -from django.db import models -from django.utils import timezone -from django.utils.functional import cached_property -from django.utils.translation import gettext_lazy as _ -from fido2.ctap2 import AuthenticatorData - -from sentry.auth.authenticators import ( - AUTHENTICATOR_CHOICES, - AUTHENTICATOR_INTERFACES, - AUTHENTICATOR_INTERFACES_BY_TYPE, - available_authenticators, -) -from sentry.auth.authenticators.base import EnrollmentStatus -from sentry.backup.dependencies import NormalizedModelName, get_model_name -from sentry.backup.sanitize import SanitizableField, Sanitizer -from sentry.backup.scopes import RelocationScope -from sentry.db.models import ( - BoundedAutoField, - BoundedPositiveIntegerField, - FlexibleForeignKey, - control_silo_model, -) -from sentry.db.models.fields.picklefield import PickledObjectField -from sentry.db.models.manager.base import BaseManager -from sentry.db.models.outboxes import ControlOutboxProducingModel -from sentry.models.outbox import ControlOutboxBase, OutboxCategory -from sentry.types.region import find_regions_for_user - - -class AuthenticatorManager(BaseManager["Authenticator"]): - def all_interfaces_for_user(self, user, return_missing=False, ignore_backup=False): - """Returns a correctly sorted list of all interfaces the user - has enabled. If `return_missing` is set to `True` then all - interfaces are returned even if not enabled. - """ - - def _sort(x): - return sorted(x, key=lambda x: (x.type == 0, x.type)) - - # Collect interfaces user is enrolled in - ifaces = [ - x.interface - for x in Authenticator.objects.filter( - user_id=user.id, - type__in=[a.type for a in available_authenticators(ignore_backup=ignore_backup)], - ) - ] - - if return_missing: - # Collect additional interfaces that the user - # is not enrolled in - rvm = dict(AUTHENTICATOR_INTERFACES) - for iface in ifaces: - rvm.pop(iface.interface_id, None) - for iface_cls in rvm.values(): - if iface_cls.is_available: - ifaces.append(iface_cls()) - - return _sort(ifaces) - - def auto_add_recovery_codes(self, user, force=False): - """This automatically adds the recovery code backup interface in - case no backup interface is currently set for the user. Returns - the interface that was added. - """ - from sentry.auth.authenticators.recovery_code import RecoveryCodeInterface - - has_authenticators = False - - # If we're not forcing, check for a backup interface already setup - # or if it's missing, we'll need to set it. - if not force: - for authenticator in Authenticator.objects.filter( - user_id=user.id, type__in=[a.type for a in available_authenticators()] - ): - iface = authenticator.interface - if iface.is_backup_interface: - return - has_authenticators = True - - if has_authenticators or force: - interface = RecoveryCodeInterface() - interface.enroll(user) - return interface - - def get_interface(self, user, interface_id): - """Looks up an interface by interface ID for a user. If the - interface is not available but configured a - `Authenticator.DoesNotExist` will be raised just as if the - authenticator was not configured at all. - """ - interface = AUTHENTICATOR_INTERFACES.get(interface_id) - if interface is None or not interface.is_available: - raise LookupError("No such interface %r" % interface_id) - try: - return Authenticator.objects.get(user_id=user.id, type=interface.type).interface - except Authenticator.DoesNotExist: - return interface.generate(EnrollmentStatus.NEW) - - def bulk_users_have_2fa(self, user_ids): - """Checks if a list of user ids have 2FA configured. - Returns a dict of {: } - """ - authenticators = set( - Authenticator.objects.filter( - user__in=user_ids, - type__in=[a.type for a in available_authenticators(ignore_backup=True)], - ) - .distinct() - .values_list("user_id", flat=True) - ) - return {id: id in authenticators for id in user_ids} - - -class AuthenticatorConfig(PickledObjectField): - def _is_devices_config(self, value: Any) -> bool: - return isinstance(value, dict) and "devices" in value - - def get_db_prep_value(self, value, *args, **kwargs): - if self._is_devices_config(value): - # avoid mutating the original object - value = copy.deepcopy(value) - for device in value["devices"]: - # AuthenticatorData is a non-json-serializable bytes subclass - if isinstance(device["binding"], AuthenticatorData): - device["binding"] = base64.b64encode(device["binding"]).decode() - - return super().get_db_prep_value(value, *args, **kwargs) - - def to_python(self, value): - ret = super().to_python(value) - if self._is_devices_config(ret): - for device in ret["devices"]: - if isinstance(device["binding"], str): - device["binding"] = AuthenticatorData(base64.b64decode(device["binding"])) - return ret - - -@control_silo_model -class Authenticator(ControlOutboxProducingModel): - # It only makes sense to import/export this data when doing a full global backup/restore, so it - # lives in the `Global` scope, even though it only depends on the `User` model. - __relocation_scope__ = RelocationScope.Global - - id = BoundedAutoField(primary_key=True) - user = FlexibleForeignKey("sentry.User", db_index=True) - created_at = models.DateTimeField(_("created at"), default=timezone.now) - last_used_at = models.DateTimeField(_("last used at"), null=True) - type = BoundedPositiveIntegerField(choices=AUTHENTICATOR_CHOICES) - - config = AuthenticatorConfig() - - objects: ClassVar[AuthenticatorManager] = AuthenticatorManager() - - class AlreadyEnrolled(Exception): - pass - - class Meta: - app_label = "sentry" - db_table = "auth_authenticator" - verbose_name = _("authenticator") - verbose_name_plural = _("authenticators") - unique_together = (("user", "type"),) - - def outboxes_for_update(self, shard_identifier: int | None = None) -> list[ControlOutboxBase]: - regions = find_regions_for_user(self.user_id) - return OutboxCategory.USER_UPDATE.as_control_outboxes( - region_names=regions, - shard_identifier=self.user_id, - object_identifier=self.user_id, - ) - - @cached_property - def interface(self): - return AUTHENTICATOR_INTERFACES_BY_TYPE[self.type](self) - - def mark_used(self, save=True): - self.last_used_at = timezone.now() - if save: - self.save() - - def reset_fields(self, save=True): - self.created_at = timezone.now() - self.last_used_at = None - if save: - self.save() - - def __repr__(self): - return f"" - - @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) - - sanitizer.set_string(json, SanitizableField(model_name, "config"), lambda _: '""') +__all__ = ("Authenticator", "AuthenticatorConfig") diff --git a/src/sentry/models/avatars/user_avatar.py b/src/sentry/models/avatars/user_avatar.py index 2d106a3ddaec77..7703a6b8dd3871 100644 --- a/src/sentry/models/avatars/user_avatar.py +++ b/src/sentry/models/avatars/user_avatar.py @@ -8,10 +8,9 @@ from sentry.db.models import FlexibleForeignKey, control_silo_model from sentry.db.models.manager.base import BaseManager - -from ...types.region import find_regions_for_user -from ..outbox import ControlOutboxBase, OutboxCategory -from . import ControlAvatarBase +from sentry.models.avatars import ControlAvatarBase +from sentry.models.outbox import ControlOutboxBase, OutboxCategory +from sentry.types.region import find_regions_for_user class UserAvatarType(IntEnum): diff --git a/src/sentry/models/email.py b/src/sentry/models/email.py index f947c3096ee083..e57f2e298de173 100644 --- a/src/sentry/models/email.py +++ b/src/sentry/models/email.py @@ -33,8 +33,8 @@ class Meta: @classmethod def query_for_relocation_export(cls, q: models.Q, pk_map: PrimaryKeyMap) -> models.Q: - from sentry.models.user import User - from sentry.models.useremail import UserEmail + from sentry.users.models.user import User + from sentry.users.models.useremail import UserEmail # `Sentry.Email` models don't have any explicit dependencies on `Sentry.User`, so we need to # find them manually via `UserEmail`. diff --git a/src/sentry/models/groupassignee.py b/src/sentry/models/groupassignee.py index 600b1f68bc72b0..e3c979eb3eb562 100644 --- a/src/sentry/models/groupassignee.py +++ b/src/sentry/models/groupassignee.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: from sentry.models.group import Group from sentry.models.team import Team - from sentry.models.user import User + from sentry.users.models.user import User from sentry.users.services.user import RpcUser logger = logging.getLogger(__name__) @@ -46,7 +46,7 @@ def get_assigned_to_data( def get_assignee_data(self, assigned_to: Team | RpcUser) -> tuple[str, str, str]: from sentry.models.team import Team - from sentry.models.user import User + from sentry.users.models.user import User from sentry.users.services.user import RpcUser if isinstance(assigned_to, (User, RpcUser)): diff --git a/src/sentry/models/grouphistory.py b/src/sentry/models/grouphistory.py index 88129637081e94..96e04922a032ac 100644 --- a/src/sentry/models/grouphistory.py +++ b/src/sentry/models/grouphistory.py @@ -24,7 +24,7 @@ from sentry.models.group import Group from sentry.models.release import Release from sentry.models.team import Team - from sentry.models.user import User + from sentry.users.models.user import User from sentry.users.services.user import RpcUser @@ -284,7 +284,7 @@ def record_group_history( release: Optional["Release"] = None, ): from sentry.models.team import Team - from sentry.models.user import User + from sentry.users.models.user import User from sentry.users.services.user import RpcUser prev_history = get_prev_history(group, status) @@ -318,7 +318,7 @@ def bulk_record_group_history( release: Optional["Release"] = None, ): from sentry.models.team import Team - from sentry.models.user import User + from sentry.users.models.user import User from sentry.users.services.user import RpcUser def get_prev_history_date(group, status): diff --git a/src/sentry/models/groupsubscription.py b/src/sentry/models/groupsubscription.py index b195fdac4c3a4d..8a81951188ecc5 100644 --- a/src/sentry/models/groupsubscription.py +++ b/src/sentry/models/groupsubscription.py @@ -30,8 +30,8 @@ if TYPE_CHECKING: from sentry.models.group import Group from sentry.models.team import Team - from sentry.models.user import User from sentry.notifications.utils.participants import ParticipantMap + from sentry.users.models.user import User class GroupSubscriptionManager(BaseManager["GroupSubscription"]): @@ -46,7 +46,7 @@ def subscribe( unsubscribed. """ from sentry.models.team import Team - from sentry.models.user import User + from sentry.users.models.user import User try: with transaction.atomic(router.db_for_write(GroupSubscription)): @@ -78,7 +78,7 @@ def subscribe_actor( ) -> bool | None: from sentry import features from sentry.models.team import Team - from sentry.models.user import User + from sentry.users.models.user import User if isinstance(actor, (RpcUser, User)): return self.subscribe(group, actor, reason) diff --git a/src/sentry/models/identity.py b/src/sentry/models/identity.py index 4c7fe96d177f27..977164bee781d3 100644 --- a/src/sentry/models/identity.py +++ b/src/sentry/models/identity.py @@ -26,7 +26,7 @@ if TYPE_CHECKING: from sentry.identity.base import Provider from sentry.identity.services.identity import RpcIdentityProvider - from sentry.models.user import User + from sentry.users.models.user import User logger = logging.getLogger(__name__) diff --git a/src/sentry/models/integrations/integration.py b/src/sentry/models/integrations/integration.py index d7c886a7405cbd..09fa8571fc9beb 100644 --- a/src/sentry/models/integrations/integration.py +++ b/src/sentry/models/integrations/integration.py @@ -25,7 +25,7 @@ IntegrationProvider, ) from sentry.models.organization import Organization - from sentry.models.user import User + from sentry.users.models.user import User from sentry.users.services.user import RpcUser logger = logging.getLogger(__name__) diff --git a/src/sentry/models/options/user_option.py b/src/sentry/models/options/user_option.py index 951d9db896b1e7..8a74caf125abd6 100644 --- a/src/sentry/models/options/user_option.py +++ b/src/sentry/models/options/user_option.py @@ -1,264 +1,3 @@ -from __future__ import annotations +from sentry.users.models.user_option import UserOption -from collections.abc import Mapping -from typing import TYPE_CHECKING, Any, ClassVar - -from django.conf import settings -from django.db import models - -from sentry.backup.dependencies import ImportKind, PrimaryKeyMap, get_model_name -from sentry.backup.helpers import ImportFlags -from sentry.backup.scopes import ImportScope, RelocationScope -from sentry.db.models import FlexibleForeignKey, Model, control_silo_model, sane_repr -from sentry.db.models.fields import PickledObjectField -from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey -from sentry.db.models.manager.option import OptionManager - -if TYPE_CHECKING: - from sentry.models.organization import Organization - from sentry.models.project import Project - from sentry.models.user import User - from sentry.users.services.user import RpcUser - -option_scope_error = "this is not a supported use case, scope to project OR organization" - - -class UserOptionManager(OptionManager["UserOption"]): - def _make_key( # type: ignore[override] - self, - user: User | RpcUser | int, - project: Project | int | None = None, - organization: Organization | int | None = None, - ) -> str: - uid = user.id if user and not isinstance(user, int) else user - org_id: int | None = organization.id if isinstance(organization, Model) else organization - proj_id: int | None = project.id if isinstance(project, Model) else project - if project: - metakey = f"{uid}:{proj_id}:project" - elif organization: - metakey = f"{uid}:{org_id}:organization" - else: - metakey = f"{uid}:user" - - return super()._make_key(metakey) - - def get_value( - self, user: User | RpcUser, key: str, default: Any | None = None, **kwargs: Any - ) -> Any: - project = kwargs.get("project") - organization = kwargs.get("organization") - - if organization and project: - raise NotImplementedError(option_scope_error) - if organization: - result = self.get_all_values(user, None, organization) - else: - result = self.get_all_values(user, project) - return result.get(key, default) - - def unset_value(self, user: User, project: Project, key: str) -> None: - """ - This isn't implemented for user-organization scoped options yet, because it hasn't been needed. - """ - self.filter(user=user, project=project, key=key).delete() - - if not hasattr(self, "_metadata"): - return - - metakey = self._make_key(user, project=project) - - if metakey not in self._option_cache: - return - self._option_cache[metakey].pop(key, None) - - def set_value(self, user: User | int, key: str, value: Any, **kwargs: Any) -> None: - project = kwargs.get("project") - organization = kwargs.get("organization") - project_id = kwargs.get("project_id", None) - organization_id = kwargs.get("organization_id", None) - if project is not None: - project_id = project.id - if organization is not None: - organization_id = organization.id - - if organization and project: - raise NotImplementedError(option_scope_error) - - inst, created = self.get_or_create( - user_id=user.id if user and not isinstance(user, int) else user, - project_id=project_id, - organization_id=organization_id, - key=key, - defaults={"value": value}, - ) - if not created and inst.value != value: - inst.update(value=value) - - metakey = self._make_key(user, project=project, organization=organization) - - if metakey not in self._option_cache: - return - self._option_cache[metakey][key] = value - - def get_all_values( - self, - user: User | RpcUser | int, - project: Project | int | None = None, - organization: Organization | int | None = None, - force_reload: bool = False, - ) -> Mapping[str, Any]: - if organization and project: - raise NotImplementedError(option_scope_error) - - uid = user.id if user and not isinstance(user, int) else user - metakey = self._make_key(user, project=project, organization=organization) - project_id: int | None = project.id if isinstance(project, Model) else project - organization_id: int | None = ( - organization.id if isinstance(organization, Model) else organization - ) - - if metakey not in self._option_cache or force_reload: - result = { - i.key: i.value - for i in self.filter( - user_id=uid, project_id=project_id, organization_id=organization_id - ) - } - self._option_cache[metakey] = result - - return self._option_cache.get(metakey, {}) - - def post_save(self, instance: UserOption, **kwargs: Any) -> None: - self.get_all_values( - instance.user, instance.project_id, instance.organization_id, force_reload=True - ) - - def post_delete(self, instance: UserOption, **kwargs: Any) -> None: - self.get_all_values( - instance.user, instance.project_id, instance.organization_id, force_reload=True - ) - - -# TODO(dcramer): the NULL UNIQUE constraint here isn't valid, and instead has to -# be manually replaced in the database. We should restructure this model. -@control_silo_model -class UserOption(Model): - """ - User options apply only to a user, and optionally a project OR an organization. - - Options which are specific to a plugin should namespace - their key. e.g. key='myplugin:optname' - - Keeping user feature state - key: "feature:assignment" - value: { updated: datetime, state: bool } - - where key is one of: - (please add to this list if adding new keys) - - clock_24_hours - - 12hr vs. 24hr - - issue:defaults - - only used in Jira, set default reporter field - - issues:defaults:jira - - unused - - issues:defaults:jira_server - - unused - - issue_details_new_experience_q4_2023 - - Whether the user has opted into the new issue details experience (boolean) - - language - - which language to display the app in - - mail:email - - which email address to send an email to - - reports:disabled-organizations - - which orgs to not send weekly reports to - - seen_release_broadcast - - unused - - self_assign_issue - - "Claim Unassigned Issues I've Resolved" - - self_notifications - - "Notify Me About My Own Activity" - - stacktrace_order - - default, most recent first, most recent last - - subscribe_by_default - - "Only On Issues I Subscribe To", "Only On Deploys With My Commits" - - subscribe_notes - - unused - - timezone - - user's timezone to display timestamps - - theme - - dark, light, or default - - twilio:alert - - unused - - workflow_notifications - - unused - """ - - __relocation_scope__ = RelocationScope.User - - user = FlexibleForeignKey(settings.AUTH_USER_MODEL) - project_id = HybridCloudForeignKey("sentry.Project", null=True, on_delete="CASCADE") - organization_id = HybridCloudForeignKey("sentry.Organization", null=True, on_delete="CASCADE") - key = models.CharField(max_length=64) - value = PickledObjectField() - - objects: ClassVar[UserOptionManager] = UserOptionManager() - - class Meta: - app_label = "sentry" - db_table = "sentry_useroption" - unique_together = (("user", "project_id", "key"), ("user", "organization_id", "key")) - - __repr__ = sane_repr("user_id", "project_id", "organization_id", "key", "value") - - @classmethod - def get_relocation_ordinal_fields(self, json_model: Any) -> list[str] | None: - # "global" user options (those with no organization and/or project scope) get a custom - # ordinal; non-global ones use the default ordering. - org_id = json_model["fields"].get("organization_id", None) - project_id = json_model["fields"].get("project_id", None) - if org_id is None and project_id is None: - return ["user", "key"] - - return None - - def normalize_before_relocation_import( - self, pk_map: PrimaryKeyMap, scope: ImportScope, flags: ImportFlags - ) -> int | None: - from sentry.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 options and use the existing user's - # options instead. - if pk_map.get_kind(get_model_name(User), old_user_id) == ImportKind.Existing: - return None - - return old_pk - - def write_relocation_import( - self, scope: ImportScope, flags: ImportFlags - ) -> tuple[int, ImportKind] | None: - # TODO(getsentry/team-ospo#190): This circular import is a bit gross. See if we can't find a - # better place for this logic to live. - from sentry.api.endpoints.user_details import UserOptionsSerializer - - serializer_options = UserOptionsSerializer(data={self.key: self.value}, partial=True) - serializer_options.is_valid(raise_exception=True) - - # TODO(getsentry/team-ospo#190): Find a more general solution to one-off indices such as - # this. We currently have this constraint on prod, but not in Django, probably from legacy - # SQL manipulation. - # - # Ensure that global (ie: `organization_id` and `project_id` both `NULL`) constraints are - # not duplicated on import. - if self.organization_id is None and self.project_id is None: - colliding_global_user_option = self.objects.filter( - user=self.user, key=self.key, organization_id__isnull=True, project_id__isnull=True - ).first() - if colliding_global_user_option is not None: - return None - - return super().write_relocation_import(scope, flags) +__all__ = ("UserOption",) diff --git a/src/sentry/models/outbox.py b/src/sentry/models/outbox.py index 92e70e0ae321d4..161a2596a48004 100644 --- a/src/sentry/models/outbox.py +++ b/src/sentry/models/outbox.py @@ -224,7 +224,7 @@ def infer_identifiers( from sentry.models.apiapplication import ApiApplication from sentry.models.integrations import Integration from sentry.models.organization import Organization - from sentry.models.user import User + from sentry.users.models.user import User assert (model is not None) ^ ( object_identifier is not None @@ -628,13 +628,16 @@ def _set_span_data_for_coalesced_message(self, span: Span, message: OutboxBase): def process(self, is_synchronous_flush: bool) -> bool: with self.process_coalesced(is_synchronous_flush=is_synchronous_flush) as coalesced: if coalesced is not None and not self.should_skip_shard(): - with metrics.timer( - "outbox.send_signal.duration", - tags={ - "category": OutboxCategory(coalesced.category).name, - "synchronous": int(is_synchronous_flush), - }, - ), sentry_sdk.start_span(op="outbox.process") as span: + with ( + metrics.timer( + "outbox.send_signal.duration", + tags={ + "category": OutboxCategory(coalesced.category).name, + "synchronous": int(is_synchronous_flush), + }, + ), + sentry_sdk.start_span(op="outbox.process") as span, + ): self._set_span_data_for_coalesced_message(span=span, message=coalesced) try: coalesced.send_signal() diff --git a/src/sentry/models/project.py b/src/sentry/models/project.py index a7f0c06ddd19df..9fd0a5a1120ac7 100644 --- a/src/sentry/models/project.py +++ b/src/sentry/models/project.py @@ -51,7 +51,7 @@ if TYPE_CHECKING: from sentry.models.options.project_option import ProjectOptionManager from sentry.models.options.project_template_option import ProjectTemplateOptionManager - from sentry.models.user import User + from sentry.users.models.user import User SENTRY_USE_SNOWFLAKE = getattr(settings, "SENTRY_USE_SNOWFLAKE", False) diff --git a/src/sentry/models/projectownership.py b/src/sentry/models/projectownership.py index 4dafe566821ac0..783dc2414250f2 100644 --- a/src/sentry/models/projectownership.py +++ b/src/sentry/models/projectownership.py @@ -247,7 +247,7 @@ def handle_auto_assignment( from sentry.models.groupassignee import GroupAssignee from sentry.models.groupowner import GroupOwner, GroupOwnerType from sentry.models.team import Team - from sentry.models.user import User + from sentry.users.models.user import User from sentry.users.services.user import RpcUser if logging_extra is None: diff --git a/src/sentry/models/team.py b/src/sentry/models/team.py index 05c0157dd1d85e..f1148214a25954 100644 --- a/src/sentry/models/team.py +++ b/src/sentry/models/team.py @@ -30,7 +30,7 @@ if TYPE_CHECKING: from sentry.models.organization import Organization from sentry.models.project import Project - from sentry.models.user import User + from sentry.users.models.user import User from sentry.users.services.user import RpcUser diff --git a/src/sentry/models/user.py b/src/sentry/models/user.py index a0e92080b6d29c..581d3df59fe2ea 100644 --- a/src/sentry/models/user.py +++ b/src/sentry/models/user.py @@ -1,598 +1,3 @@ -from __future__ import annotations +from sentry.users.models.user import User -import logging -import secrets -import warnings -from collections.abc import Mapping -from string import ascii_letters, digits -from typing import Any, ClassVar - -from django.contrib.auth.models import AbstractBaseUser -from django.contrib.auth.models import UserManager as DjangoUserManager -from django.contrib.auth.signals import user_logged_out -from django.db import IntegrityError, models, router, transaction -from django.db.models import Count, Subquery -from django.db.models.query import QuerySet -from django.dispatch import receiver -from django.forms import model_to_dict -from django.urls import reverse -from django.utils import timezone -from django.utils.translation import gettext_lazy as _ - -from bitfield import TypedClassBitField -from sentry.auth.authenticators import available_authenticators -from sentry.backup.dependencies import ( - ImportKind, - NormalizedModelName, - PrimaryKeyMap, - get_model_name, - merge_users_for_model_in_org, -) -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 Model, control_silo_model, sane_repr -from sentry.db.models.manager.base import BaseManager -from sentry.db.models.utils import unique_db_instance -from sentry.db.postgres.transactions import enforce_constraints -from sentry.integrations.types import EXTERNAL_PROVIDERS, ExternalProviders -from sentry.locks import locks -from sentry.models.authenticator import Authenticator -from sentry.models.avatars import UserAvatar -from sentry.models.lostpasswordhash import LostPasswordHash -from sentry.models.organizationmapping import OrganizationMapping -from sentry.models.organizationmembermapping import OrganizationMemberMapping -from sentry.models.orgauthtoken import OrgAuthToken -from sentry.models.outbox import ControlOutboxBase, OutboxCategory, outbox_context -from sentry.organizations.services.organization import RpcRegionUser, organization_service -from sentry.types.region import find_all_region_names, find_regions_for_user -from sentry.users.services.user import RpcUser -from sentry.utils.http import absolute_uri -from sentry.utils.retries import TimedRetryPolicy - -audit_logger = logging.getLogger("sentry.audit.user") - -MAX_USERNAME_LENGTH = 128 -RANDOM_PASSWORD_ALPHABET = ascii_letters + digits -RANDOM_PASSWORD_LENGTH = 32 - - -class UserManager(BaseManager["User"], DjangoUserManager["User"]): - def get_users_with_only_one_integration_for_provider( - self, provider: ExternalProviders, organization_id: int - ) -> QuerySet[User]: - """ - For a given organization, get the list of members that are only - connected to a single integration. - """ - from sentry.models.integrations.organization_integration import OrganizationIntegration - from sentry.models.organizationmembermapping import OrganizationMemberMapping - - org_user_ids = OrganizationMemberMapping.objects.filter( - organization_id=organization_id - ).values("user_id") - org_members_with_provider = ( - OrganizationMemberMapping.objects.values("user_id") - .annotate(org_counts=Count("organization_id")) - .filter( - user_id__in=Subquery(org_user_ids), - organization_id__in=Subquery( - OrganizationIntegration.objects.filter( - integration__provider=EXTERNAL_PROVIDERS[provider] - ).values("organization_id") - ), - org_counts=1, - ) - .values("user_id") - ) - return self.filter(id__in=Subquery(org_members_with_provider)) - - -@control_silo_model -class User(Model, AbstractBaseUser): - __relocation_scope__ = RelocationScope.User - __relocation_custom_ordinal__ = ["username"] - - replication_version: int = 2 - - username = models.CharField(_("username"), max_length=MAX_USERNAME_LENGTH, unique=True) - # this column is called first_name for legacy reasons, but it is the entire - # display name - name = models.CharField(_("name"), max_length=200, blank=True, db_column="first_name") - email = models.EmailField(_("email address"), blank=True, max_length=75) - is_staff = models.BooleanField( - _("staff status"), - default=False, - help_text=_("Designates whether the user can log into this admin site."), - ) - is_active = models.BooleanField( - _("active"), - default=True, - help_text=_( - "Designates whether this user should be treated as " - "active. Unselect this instead of deleting accounts." - ), - ) - is_unclaimed = models.BooleanField( - _("unclaimed"), - default=False, - help_text=_( - "Designates that this user was imported via the relocation tool, but has not yet been " - "claimed by the owner of the associated email. Users in this state have randomized " - "passwords - when email owners claim the account, they are prompted to reset their " - "password and do a one-time update to their username." - ), - ) - is_superuser = models.BooleanField( - _("superuser status"), - default=False, - help_text=_( - "Designates that this user has all permissions without explicitly assigning them." - ), - ) - is_managed = models.BooleanField( - _("managed"), - default=False, - help_text=_( - "Designates whether this user should be treated as " - "managed. Select this to disallow the user from " - "modifying their account (username, password, etc)." - ), - ) - is_sentry_app = models.BooleanField( - _("is sentry app"), - null=True, - default=None, - help_text=_( - "Designates whether this user is the entity used for Permissions" - "on behalf of a Sentry App. Cannot login or use Sentry like a" - "normal User would." - ), - ) - is_password_expired = models.BooleanField( - _("password expired"), - default=False, - help_text=_( - "If set to true then the user needs to change the " "password on next sign in." - ), - ) - last_password_change = models.DateTimeField( - _("date of last password change"), - null=True, - help_text=_("The date the password was changed last."), - ) - - class flags(TypedClassBitField): - # WARNING: Only add flags to the bottom of this list - # bitfield flags are dependent on their order and inserting/removing - # flags from the middle of the list will cause bits to shift corrupting - # existing data. - - # Do we need to ask this user for newsletter consent? - newsletter_consent_prompt: bool - - bitfield_default = 0 - bitfield_null = True - - session_nonce = models.CharField(max_length=12, null=True) - - date_joined = models.DateTimeField(_("date joined"), default=timezone.now) - last_active = models.DateTimeField(_("last active"), default=timezone.now, null=True) - - avatar_type = models.PositiveSmallIntegerField(default=0, choices=UserAvatar.AVATAR_TYPES) - avatar_url = models.CharField(_("avatar url"), max_length=120, null=True) - - objects: ClassVar[UserManager] = UserManager(cache_fields=["pk"]) - - USERNAME_FIELD = "username" - REQUIRED_FIELDS = ["email"] - - class Meta: - app_label = "sentry" - db_table = "auth_user" - verbose_name = _("user") - verbose_name_plural = _("users") - - __repr__ = sane_repr("id") - - def class_name(self): - return "User" - - def delete(self): - if self.username == "sentry": - raise Exception('You cannot delete the "sentry" user as it is required by Sentry.') - with outbox_context(transaction.atomic(using=router.db_for_write(User))): - avatar = self.avatar.first() - if avatar: - avatar.delete() - for outbox in self.outboxes_for_update(is_user_delete=True): - outbox.save() - return super().delete() - - def update(self, *args, **kwds): - with outbox_context(transaction.atomic(using=router.db_for_write(User))): - for outbox in self.outboxes_for_update(): - outbox.save() - return super().update(*args, **kwds) - - def save(self, *args, **kwargs): - with outbox_context(transaction.atomic(using=router.db_for_write(User))): - if not self.username: - self.username = self.email - result = super().save(*args, **kwargs) - for outbox in self.outboxes_for_update(): - outbox.save() - return result - - def has_perm(self, perm_name): - warnings.warn("User.has_perm is deprecated", DeprecationWarning) - return self.is_superuser - - def has_module_perms(self, app_label): - warnings.warn("User.has_module_perms is deprecated", DeprecationWarning) - return self.is_superuser - - def has_2fa(self): - return Authenticator.objects.filter( - user_id=self.id, type__in=[a.type for a in available_authenticators(ignore_backup=True)] - ).exists() - - def get_unverified_emails(self): - return self.emails.filter(is_verified=False) - - def get_verified_emails(self): - return self.emails.filter(is_verified=True) - - def has_verified_emails(self): - return self.get_verified_emails().exists() - - def has_unverified_emails(self): - return self.get_unverified_emails().exists() - - def has_usable_password(self): - if self.password == "" or self.password is None: - # This is the behavior we've been relying on from Django 1.6 - 2.0. - # In 2.1, a "" or None password is considered usable. - # Removing this override requires identifying all the places - # to put set_unusable_password and a migration. - return False - return super().has_usable_password() - - def get_label(self): - return self.email or self.username or self.id - - def get_display_name(self): - return self.name or self.email or self.username - - def get_full_name(self): - return self.name - - def get_salutation_name(self) -> str: - name = self.name or self.username.split("@", 1)[0].split(".", 1)[0] - first_name = name.split(" ", 1)[0] - return first_name.capitalize() - - def get_avatar_type(self): - return self.get_avatar_type_display() - - def get_actor_identifier(self): - return f"user:{self.id}" - - def send_confirm_email_singular(self, email, is_new_user=False): - from sentry import options - from sentry.utils.email import MessageBuilder - - if not email.hash_is_valid(): - email.set_hash() - email.save() - - context = { - "user": self, - "url": absolute_uri( - reverse("sentry-account-confirm-email", args=[self.id, email.validation_hash]) - ), - "confirm_email": email.email, - "is_new_user": is_new_user, - } - msg = MessageBuilder( - subject="{}Confirm Email".format(options.get("mail.subject-prefix")), - template="sentry/emails/confirm_email.txt", - html_template="sentry/emails/confirm_email.html", - type="user.confirm_email", - context=context, - ) - msg.send_async([email.email]) - - def send_confirm_emails(self, is_new_user=False): - email_list = self.get_unverified_emails() - for email in email_list: - self.send_confirm_email_singular(email, is_new_user) - - def outboxes_for_update(self, is_user_delete: bool = False) -> list[ControlOutboxBase]: - return User.outboxes_for_user_update(self.id, is_user_delete=is_user_delete) - - @staticmethod - def outboxes_for_user_update( - identifier: int, is_user_delete: bool = False - ) -> list[ControlOutboxBase]: - # User deletions must fan out to all regions to ensure cascade behavior - # of anything with a HybridCloudForeignKey, even if the user is no longer - # a member of any organizations in that region. - if is_user_delete: - user_regions = set(find_all_region_names()) - else: - user_regions = find_regions_for_user(identifier) - - return OutboxCategory.USER_UPDATE.as_control_outboxes( - region_names=user_regions, - object_identifier=identifier, - shard_identifier=identifier, - ) - - def merge_to(from_user: User, to_user: User) -> None: - # TODO: we could discover relations automatically and make this useful - from sentry.models.auditlogentry import AuditLogEntry - from sentry.models.authenticator import Authenticator - from sentry.models.authidentity import AuthIdentity - from sentry.models.avatars.user_avatar import UserAvatar - from sentry.models.identity import Identity - from sentry.models.options.user_option import UserOption - from sentry.models.organizationmembermapping import OrganizationMemberMapping - from sentry.models.useremail import UserEmail - - from_user_id = from_user.id - to_user_id = to_user.id - - audit_logger.info( - "user.merge", extra={"from_user_id": from_user_id, "to_user_id": to_user_id} - ) - - organization_ids = OrganizationMemberMapping.objects.filter( - user_id=from_user_id - ).values_list("organization_id", flat=True) - - for organization_id in organization_ids: - organization_service.merge_users( - organization_id=organization_id, from_user_id=from_user_id, to_user_id=to_user_id - ) - - # Update all organization control models to only use the new user id. - # - # TODO: in the future, proactively update `OrganizationMemberTeamReplica` as well. - with enforce_constraints( - transaction.atomic(using=router.db_for_write(OrganizationMemberMapping)) - ): - control_side_org_models: tuple[type[Model], ...] = ( - OrgAuthToken, - OrganizationMemberMapping, - ) - for model in control_side_org_models: - merge_users_for_model_in_org( - model, - organization_id=organization_id, - from_user_id=from_user_id, - to_user_id=to_user_id, - ) - - # While it would be nice to make the following changes in a transaction, there are too many - # unique constraints to make this feasible. Instead, we just do it sequentially and ignore - # the `IntegrityError`s. - user_related_models: tuple[type[Model], ...] = ( - Authenticator, - Identity, - UserAvatar, - UserEmail, - UserOption, - ) - for model in user_related_models: - for obj in model.objects.filter(user_id=from_user_id): - try: - with transaction.atomic(using=router.db_for_write(User)): - obj.update(user_id=to_user_id) - except IntegrityError: - pass - - # users can be either the subject or the object of actions which get logged - AuditLogEntry.objects.filter(actor=from_user).update(actor=to_user) - AuditLogEntry.objects.filter(target_user=from_user).update(target_user=to_user) - - with outbox_context(flush=False): - # remove any SSO identities that exist on from_user that might conflict - # with to_user's existing identities (only applies if both users have - # SSO identities in the same org), then pass the rest on to to_user - # NOTE: This could, become calls to identity_service.delete_ide - for ai in AuthIdentity.objects.filter( - user=from_user, - auth_provider__organization_id__in=AuthIdentity.objects.filter( - user_id=to_user_id - ).values("auth_provider__organization_id"), - ): - ai.delete() - for ai in AuthIdentity.objects.filter(user_id=from_user.id): - ai.update(user=to_user) - - def set_password(self, raw_password): - super().set_password(raw_password) - self.last_password_change = timezone.now() - self.is_password_expired = False - - def refresh_session_nonce(self, request=None): - from django.utils.crypto import get_random_string - - self.session_nonce = get_random_string(12) - if request is not None: - request.session["_nonce"] = self.session_nonce - - def has_org_requiring_2fa(self) -> bool: - from sentry.models.organization import OrganizationStatus - - return OrganizationMemberMapping.objects.filter( - user_id=self.id, - organization_id__in=Subquery( - OrganizationMapping.objects.filter( - require_2fa=True, - status=OrganizationStatus.ACTIVE, - ).values("organization_id") - ), - ).exists() - - def clear_lost_passwords(self): - LostPasswordHash.objects.filter(user=self).delete() - - def normalize_before_relocation_import( - self, pk_map: PrimaryKeyMap, scope: ImportScope, flags: ImportFlags - ) -> int | None: - old_pk = super().normalize_before_relocation_import(pk_map, scope, flags) - if old_pk is None: - return None - - # Importing in any scope besides `Global` (which does a naive, blanket restore of all data) - # and `Config` (which is explicitly meant to import admin accounts) should strip all - # incoming users of their admin privileges. - if scope not in {ImportScope.Config, ImportScope.Global}: - self.is_staff = False - self.is_superuser = False - self.is_managed = False - - # No need to mark users newly "unclaimed" when doing a global backup/restore. - if scope != ImportScope.Global or self.is_unclaimed: - # New users are marked unclaimed. - self.is_unclaimed = True - - # Give the user a cryptographically secure random password. The purpose here is to have - # a password that NO ONE knows - the only way to log into this account is to use the - # "claim your account" flow to create a new password (or to click "lost password" and - # end up there anyway), at which point we'll detect the user's `is_unclaimed` status and - # prompt them to change their `username` as well. - self.set_password( - "".join( - secrets.choice(RANDOM_PASSWORD_ALPHABET) for _ in range(RANDOM_PASSWORD_LENGTH) - ) - ) - - return old_pk - - def write_relocation_import( - self, scope: ImportScope, flags: ImportFlags - ) -> tuple[int, ImportKind] | None: - # Internal function that factors our some common logic. - def do_write(): - from sentry.api.endpoints.user_details import ( - BaseUserSerializer, - SuperuserUserSerializer, - UserSerializer, - ) - from sentry.users.services.lost_password_hash.impl import ( - DatabaseLostPasswordHashService, - ) - - serializer_cls = BaseUserSerializer - if scope not in {ImportScope.Config, ImportScope.Global}: - serializer_cls = UserSerializer - else: - serializer_cls = SuperuserUserSerializer - - serializer_user = serializer_cls(instance=self, data=model_to_dict(self), partial=True) - serializer_user.is_valid(raise_exception=True) - - self.save(force_insert=True) - - if scope != ImportScope.Global: - DatabaseLostPasswordHashService().get_or_create(user_id=self.id) - - # TODO(getsentry/team-ospo#191): we need to send an email informing the user of their - # new account with a resettable password - we'll need to figure out where in the process - # that actually goes, and how to prevent it from happening during the validation pass. - return (self.pk, ImportKind.Inserted) - - # If there is no existing user with this `username`, no special renaming or merging - # shenanigans are needed, as we can just insert this exact model directly. - existing = User.objects.filter(username=self.username).first() - if not existing: - return do_write() - - # Re-use the existing user if merging is enabled. - if flags.merge_users: - return (existing.pk, ImportKind.Existing) - - # We already have a user with this `username`, but merging users has not been enabled. In - # this case, add a random suffix to the importing username. - lock = locks.get(f"user:username:{self.id}", duration=10, name="username") - with TimedRetryPolicy(10)(lock.acquire): - unique_db_instance( - self, - self.username, - max_length=MAX_USERNAME_LENGTH, - field_name="username", - ) - - # Perform the remainder of the write while we're still holding the lock. - return do_write() - - @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) - - sanitizer.set_string(json, SanitizableField(model_name, "username")) - sanitizer.set_string(json, SanitizableField(model_name, "session_nonce")) - - @classmethod - def handle_async_deletion( - cls, - identifier: int, - region_name: str, - shard_identifier: int, - payload: Mapping[str, Any] | None, - ) -> None: - from sentry.hybridcloud.rpc.caching import region_caching_service - from sentry.users.services.user.service import get_many_by_id, get_user - - region_caching_service.clear_key(key=get_user.key_from(identifier), region_name=region_name) - region_caching_service.clear_key( - key=get_many_by_id.key_from(identifier), region_name=region_name - ) - - def handle_async_replication(self, region_name: str, shard_identifier: int) -> None: - from sentry.hybridcloud.rpc.caching import region_caching_service - from sentry.users.services.user.service import get_many_by_id, get_user - - region_caching_service.clear_key(key=get_user.key_from(self.id), region_name=region_name) - region_caching_service.clear_key( - key=get_many_by_id.key_from(self.id), region_name=region_name - ) - organization_service.update_region_user( - user=RpcRegionUser( - id=self.id, - is_active=self.is_active, - email=self.email, - ), - region_name=region_name, - ) - - -# HACK(dcramer): last_login needs nullable for Django 1.8 -User._meta.get_field("last_login").null = True - - -# When a user logs out, we want to always log them out of all -# sessions and refresh their nonce. -@receiver(user_logged_out, sender=User) -def refresh_user_nonce(sender, request, user, **kwargs): - if user is None: - return - user.refresh_session_nonce() - user.save(update_fields=["session_nonce"]) - - -@receiver(user_logged_out, sender=RpcUser) -def refresh_api_user_nonce(sender, request, user, **kwargs): - if user is None: - return - user = User.objects.get(id=user.id) - refresh_user_nonce(sender, request, user, **kwargs) - - -OutboxCategory.USER_UPDATE.connect_control_model_updates(User) +__all__ = ("User",) diff --git a/src/sentry/models/useremail.py b/src/sentry/models/useremail.py index 011eeed4ef481a..ddf4baab3257dd 100644 --- a/src/sentry/models/useremail.py +++ b/src/sentry/models/useremail.py @@ -1,163 +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.db.models.outboxes import ControlOutboxProducingModel -from sentry.models.outbox import ControlOutboxBase, 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.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.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",) diff --git a/src/sentry/models/userip.py b/src/sentry/models/userip.py index 2782bd4359668b..6c8a20ae1c9477 100644 --- a/src/sentry/models/userip.py +++ b/src/sentry/models/userip.py @@ -1,142 +1,3 @@ -from __future__ import annotations +from sentry.users.models.userip import UserIP -from typing import Any - -from django.conf import settings -from django.core.cache import cache -from django.db import models -from django.utils import timezone - -from sentry.audit_log.services.log import UserIpEvent, log_service -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, Model, control_silo_model, sane_repr -from sentry.models.user import User -from sentry.users.services.user import RpcUser -from sentry.utils.geo import geo_by_addr - - -@control_silo_model -class UserIP(Model): - # There is an absolutely massive number of `UserIP` models in any sufficiently long-lived - # install of Sentry. So while it would probably make semantic sense to have this be - # `RelocationScope.User`, only someone interested in backing up every bit of data could want - # this (we certainly don't need it on prod for relocation). Thus, this gets moved into the - # `Global` scope instead. - __relocation_scope__ = RelocationScope.Global - __relocation_custom_ordinal__ = ["user", "ip_address"] - - user = FlexibleForeignKey(settings.AUTH_USER_MODEL) - ip_address = models.GenericIPAddressField() - country_code = models.CharField(max_length=16, null=True) - region_code = models.CharField(max_length=16, null=True) - first_seen = models.DateTimeField(default=timezone.now) - last_seen = models.DateTimeField(default=timezone.now) - - class Meta: - app_label = "sentry" - db_table = "sentry_userip" - unique_together = (("user", "ip_address"),) - - __repr__ = sane_repr("user_id", "ip_address") - - @classmethod - def log(cls, user: User | RpcUser, ip_address: str): - # Only log once every 5 minutes for the same user/ip_address pair - # since this is hit pretty frequently by all API calls in the UI, etc. - cache_key = f"userip.log:{user.id}:{ip_address}" - if not cache.get(cache_key): - _perform_log(user, ip_address) - cache.set(cache_key, 1, 300) - - def normalize_before_relocation_import( - self, pk_map: PrimaryKeyMap, scope: ImportScope, flags: ImportFlags - ) -> int | None: - from sentry.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 IP and use the existing user's IP instead. - if pk_map.get_kind(get_model_name(User), old_user_id) == ImportKind.Existing: - return None - - # We'll recalculate the country codes from the IP when we call `log()` in - # `write_relocation_import()`. - self.country_code = None - self.region_code = None - - # Only preserve the submitted timing data in the backup/restore scope. - if scope != ImportScope.Global: - self.first_seen = self.last_seen = timezone.now() - - return old_pk - - def write_relocation_import( - self, _s: ImportScope, _f: ImportFlags - ) -> tuple[int, ImportKind] | None: - # Ensures that the IP address is valid. Exclude the codes, as they should be `None` until we - # `log()` them below. - self.full_clean(exclude=["country_code", "region_code", "user"]) - - # Update country/region codes as necessary by using the `log()` method. - (userip, _) = self.__class__.objects.get_or_create( - user=self.user, ip_address=self.ip_address - ) - - # Calling the `.log()` method makes a separate "update" call to the database, so we need to - # refresh this local version of the model immediately after. - self.__class__.log(self.user, self.ip_address) - userip.refresh_from_db() - - userip.first_seen = self.first_seen - userip.last_seen = self.last_seen - userip.save() - - self.country_code = userip.country_code - self.region_code = userip.region_code - - # If we've entered this method at all, we can be sure that the `UserIP` 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 (userip.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) - - # Always use British Columbia for fake IP geo data, cause why not. - sanitizer.set_string(json, SanitizableField(model_name, "country_code"), lambda _: "CA") - sanitizer.set_string(json, SanitizableField(model_name, "region_code"), lambda _: "BC") - - -def _perform_log(user: User | RpcUser, ip_address: str): - try: - geo = geo_by_addr(ip_address) - except Exception: - geo = None - - event = UserIpEvent( - user_id=user.id, - ip_address=ip_address, - last_seen=timezone.now(), - ) - - if geo: - event.country_code = geo["country_code"] - event.region_code = geo["region"] - - log_service.record_user_ip(event=event) +__all__ = ("UserIP",) diff --git a/src/sentry/models/userpermission.py b/src/sentry/models/userpermission.py index c3ab024f135b3a..a6ad1fc3c47a7e 100644 --- a/src/sentry/models/userpermission.py +++ b/src/sentry/models/userpermission.py @@ -1,70 +1,3 @@ -from __future__ import annotations +from sentry.users.models.userpermission import UserPermission -from django.db import models - -from sentry.backup.dependencies import ImportKind, PrimaryKeyMap, get_model_name -from sentry.backup.helpers import ImportFlags -from sentry.backup.mixins import OverwritableConfigMixin -from sentry.backup.scopes import ImportScope, RelocationScope -from sentry.db.models import FlexibleForeignKey, control_silo_model, sane_repr -from sentry.db.models.outboxes import ControlOutboxProducingModel -from sentry.models.outbox import ControlOutboxBase, OutboxCategory -from sentry.types.region import find_regions_for_user - - -@control_silo_model -class UserPermission(OverwritableConfigMixin, ControlOutboxProducingModel): - """ - Permissions are applied to administrative users and control explicit scope-like permissions within the API. - - Generally speaking, they should only apply to active superuser sessions. - """ - - __relocation_scope__ = RelocationScope.Config - __relocation_custom_ordinal__ = ["user", "permission"] - - user = FlexibleForeignKey("sentry.User") - # permissions should be in the form of 'service-name.permission-name' - permission = models.CharField(max_length=32) - - class Meta: - app_label = "sentry" - db_table = "sentry_userpermission" - unique_together = (("user", "permission"),) - - __repr__ = sane_repr("user_id", "permission") - - @classmethod - def for_user(cls, user_id: int) -> frozenset[str]: - """ - Return a set of permission for the given user ID. - """ - return frozenset(cls.objects.filter(user=user_id).values_list("permission", flat=True)) - - 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 normalize_before_relocation_import( - self, pk_map: PrimaryKeyMap, scope: ImportScope, flags: ImportFlags - ) -> int | None: - from sentry.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 permissions and use the existing user's - # permissions instead. - if pk_map.get_kind(get_model_name(User), old_user_id) == ImportKind.Existing: - return None - - return old_pk +__all__ = ("UserPermission",) diff --git a/src/sentry/models/userreport.py b/src/sentry/models/userreport.py index 14d1b4c844f1da..fb686a0ca6b3b4 100644 --- a/src/sentry/models/userreport.py +++ b/src/sentry/models/userreport.py @@ -1,41 +1,3 @@ -from django.db import models -from django.utils import timezone +from sentry.users.models.userreport import UserReport -from sentry.backup.scopes import RelocationScope -from sentry.db.models import BoundedBigIntegerField, Model, region_silo_model, sane_repr - - -@region_silo_model -class UserReport(Model): - __relocation_scope__ = RelocationScope.Excluded - - project_id = BoundedBigIntegerField(db_index=True) - group_id = BoundedBigIntegerField(null=True, db_index=True) - event_id = models.CharField(max_length=32) - environment_id = BoundedBigIntegerField(null=True, db_index=True) - name = models.CharField(max_length=128) - email = models.EmailField(max_length=75) - comments = models.TextField() - date_added = models.DateTimeField(default=timezone.now, db_index=True) - - class Meta: - app_label = "sentry" - db_table = "sentry_userreport" - indexes = ( - models.Index(fields=("project_id", "event_id")), - models.Index(fields=("project_id", "date_added")), - ) - unique_together = (("project_id", "event_id"),) - - __repr__ = sane_repr("event_id", "name", "email") - - def notify(self): - from django.contrib.auth.models import AnonymousUser - - from sentry.api.serializers import UserReportWithGroupSerializer, serialize - from sentry.tasks.user_report import user_report - - user_report.delay( - project_id=self.project_id, - report=serialize(self, AnonymousUser(), UserReportWithGroupSerializer()), - ) +__all__ = ("UserReport",) diff --git a/src/sentry/models/userrole.py b/src/sentry/models/userrole.py index 79c7ddd1702067..ed4d758aacf1bc 100644 --- a/src/sentry/models/userrole.py +++ b/src/sentry/models/userrole.py @@ -1,97 +1,3 @@ -from __future__ import annotations +from sentry.users.models.userrole import UserRole, UserRoleUser, manage_default_super_admin_role -from collections.abc import Sequence - -from django.conf import settings -from django.db import models -from django.utils import timezone - -from sentry.backup.mixins import OverwritableConfigMixin -from sentry.backup.scopes import RelocationScope -from sentry.db.models import ArrayField, control_silo_model, sane_repr -from sentry.db.models.fields.foreignkey import FlexibleForeignKey -from sentry.db.models.outboxes import ControlOutboxProducingModel -from sentry.models.outbox import ControlOutboxBase, OutboxCategory -from sentry.signals import post_upgrade -from sentry.silo.base import SiloMode -from sentry.types.region import find_all_region_names - -MAX_USER_ROLE_NAME_LENGTH = 32 - - -@control_silo_model -class UserRole(OverwritableConfigMixin, ControlOutboxProducingModel): - """ - Roles are applied to administrative users and apply a set of `UserPermission`. - """ - - __relocation_scope__ = RelocationScope.Config - __relocation_custom_ordinal__ = ["name"] - - date_updated = models.DateTimeField(default=timezone.now) - date_added = models.DateTimeField(default=timezone.now, null=True) - - name = models.CharField(max_length=MAX_USER_ROLE_NAME_LENGTH, unique=True) - permissions: models.Field[Sequence[str], list[str]] = ArrayField() - users = models.ManyToManyField("sentry.User", through="sentry.UserRoleUser") - - class Meta: - app_label = "sentry" - db_table = "sentry_userrole" - - __repr__ = sane_repr("name", "permissions") - - def outboxes_for_update(self, shard_identifier: int | None = None) -> list[ControlOutboxBase]: - regions = list(find_all_region_names()) - return [ - outbox - for user_id in self.users.values_list("id", flat=True) - for outbox in OutboxCategory.USER_UPDATE.as_control_outboxes( - region_names=regions, - shard_identifier=user_id, - object_identifier=user_id, - ) - ] - - -@control_silo_model -class UserRoleUser(ControlOutboxProducingModel): - __relocation_scope__ = RelocationScope.Config - - date_updated = models.DateTimeField(default=timezone.now) - date_added = models.DateTimeField(default=timezone.now, null=True) - - user = FlexibleForeignKey("sentry.User") - role = FlexibleForeignKey("sentry.UserRole") - - def outboxes_for_update(self, shard_identifier: int | None = None) -> list[ControlOutboxBase]: - regions = list(find_all_region_names()) - return OutboxCategory.USER_UPDATE.as_control_outboxes( - region_names=regions, - shard_identifier=self.user_id, - object_identifier=self.user_id, - ) - - class Meta: - app_label = "sentry" - db_table = "sentry_userrole_users" - - __repr__ = sane_repr("user", "role") - - -# this must be idempotent because it executes on every upgrade -def manage_default_super_admin_role(**kwargs): - role, _ = UserRole.objects.get_or_create( - name="Super Admin", defaults={"permissions": settings.SENTRY_USER_PERMISSIONS} - ) - if role.permissions != settings.SENTRY_USER_PERMISSIONS: - role.permissions = settings.SENTRY_USER_PERMISSIONS - role.save(update_fields=["permissions"]) - - -post_upgrade.connect( - manage_default_super_admin_role, - dispatch_uid="manage_default_super_admin_role", - weak=False, - sender=SiloMode.MONOLITH, -) +__all__ = ("UserRole", "UserRoleUser", "manage_default_super_admin_role") diff --git a/src/sentry/monitors/utils.py b/src/sentry/monitors/utils.py index f4a2c54c740f8e..d9c0e8c4a3be77 100644 --- a/src/sentry/monitors/utils.py +++ b/src/sentry/monitors/utils.py @@ -12,7 +12,6 @@ from sentry.models.group import Group from sentry.models.project import Project from sentry.models.rule import Rule, RuleActivity, RuleActivityType, RuleSource -from sentry.models.user import User from sentry.monitors.constants import DEFAULT_CHECKIN_MARGIN, MAX_TIMEOUT, TIMEOUT from sentry.monitors.models import CheckInStatus, Monitor, MonitorCheckIn from sentry.signals import ( @@ -20,6 +19,7 @@ first_cron_checkin_received, first_cron_monitor_created, ) +from sentry.users.models.user import User from sentry.utils.auth import AuthenticatedHttpRequest diff --git a/src/sentry/newsletter/base.py b/src/sentry/newsletter/base.py index b36d16610242ab..b2fe04bd6520d8 100644 --- a/src/sentry/newsletter/base.py +++ b/src/sentry/newsletter/base.py @@ -3,7 +3,7 @@ from collections.abc import Sequence from typing import Any -from sentry.models.user import User +from sentry.users.models.user import User from sentry.utils.services import Service diff --git a/src/sentry/newsletter/dummy.py b/src/sentry/newsletter/dummy.py index 8f155735a7e379..60ae365c135be1 100644 --- a/src/sentry/newsletter/dummy.py +++ b/src/sentry/newsletter/dummy.py @@ -7,7 +7,7 @@ from django.utils import timezone -from sentry.models.user import User +from sentry.users.models.user import User from .base import Newsletter @@ -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 diff --git a/src/sentry/notifications/helpers.py b/src/sentry/notifications/helpers.py index ba58f908b0e039..359654bac8da13 100644 --- a/src/sentry/notifications/helpers.py +++ b/src/sentry/notifications/helpers.py @@ -29,7 +29,7 @@ if TYPE_CHECKING: from sentry.models.group import Group from sentry.models.team import Team - from sentry.models.user import User + from sentry.users.models.user import User logger = logging.getLogger(__name__) @@ -110,7 +110,7 @@ def get_reason_context(extra_context: Mapping[str, Any]) -> MutableMapping[str, def recipient_is_user(recipient: Actor | Team | RpcUser | User) -> bool: - from sentry.models.user import User + from sentry.users.models.user import User if isinstance(recipient, Actor) and recipient.is_user: return True diff --git a/src/sentry/notifications/notificationcontroller.py b/src/sentry/notifications/notificationcontroller.py index f45fdaec2d5415..c1a99a80652284 100644 --- a/src/sentry/notifications/notificationcontroller.py +++ b/src/sentry/notifications/notificationcontroller.py @@ -18,7 +18,6 @@ from sentry.models.notificationsettingprovider import NotificationSettingProvider from sentry.models.organizationmapping import OrganizationMapping from sentry.models.team import Team -from sentry.models.user import User from sentry.notifications.helpers import ( get_default_for_provider, get_team_members, @@ -34,6 +33,7 @@ NotificationSettingsOptionEnum, ) from sentry.types.actor import Actor, ActorType +from sentry.users.models.user import User from sentry.users.services.user.model import RpcUser Recipient = Union[Actor, Team, RpcUser, User] diff --git a/src/sentry/notifications/notifications/integration_nudge.py b/src/sentry/notifications/notifications/integration_nudge.py index db693556df54b5..086a05bd78d0f6 100644 --- a/src/sentry/notifications/notifications/integration_nudge.py +++ b/src/sentry/notifications/notifications/integration_nudge.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: from sentry.models.organization import Organization - from sentry.models.user import User + from sentry.users.models.user import User logger = logging.getLogger(__name__) diff --git a/src/sentry/notifications/notifications/organization_request/abstract_invite_request.py b/src/sentry/notifications/notifications/organization_request/abstract_invite_request.py index c7a145145a35c3..9bae280fce0459 100644 --- a/src/sentry/notifications/notifications/organization_request/abstract_invite_request.py +++ b/src/sentry/notifications/notifications/organization_request/abstract_invite_request.py @@ -17,7 +17,7 @@ from sentry.users.services.user.service import user_service if TYPE_CHECKING: - from sentry.models.user import User + from sentry.users.models.user import User # Abstract class for invite and join requests to inherit from diff --git a/src/sentry/notifications/notifications/organization_request/base.py b/src/sentry/notifications/notifications/organization_request/base.py index 9cb120f067e741..221fba9c79c73f 100644 --- a/src/sentry/notifications/notifications/organization_request/base.py +++ b/src/sentry/notifications/notifications/organization_request/base.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: from sentry.models.organization import Organization - from sentry.models.user import User + from sentry.users.models.user import User logger = logging.getLogger(__name__) diff --git a/src/sentry/notifications/notifications/organization_request/integration_request.py b/src/sentry/notifications/notifications/organization_request/integration_request.py index 8ccd37f0fbb117..ee41e4e121dbee 100644 --- a/src/sentry/notifications/notifications/organization_request/integration_request.py +++ b/src/sentry/notifications/notifications/organization_request/integration_request.py @@ -14,7 +14,7 @@ if TYPE_CHECKING: from sentry.models.organization import Organization - from sentry.models.user import User + from sentry.users.models.user import User provider_types = { "first_party": "integrations", diff --git a/src/sentry/notifications/services/impl.py b/src/sentry/notifications/services/impl.py index 9191e0224f5897..9740cc34aa589a 100644 --- a/src/sentry/notifications/services/impl.py +++ b/src/sentry/notifications/services/impl.py @@ -7,7 +7,6 @@ from sentry.integrations.types import EXTERNAL_PROVIDERS, ExternalProviderEnum, ExternalProviders from sentry.models.notificationsettingoption import NotificationSettingOption from sentry.models.notificationsettingprovider import NotificationSettingProvider -from sentry.models.user import User from sentry.notifications.notificationcontroller import NotificationController from sentry.notifications.services import NotificationsService from sentry.notifications.services.model import RpcSubscriptionStatus @@ -17,6 +16,7 @@ NotificationSettingsOptionEnum, ) from sentry.types.actor import Actor, ActorType +from sentry.users.models.user import User from sentry.users.services.user.service import user_service diff --git a/src/sentry/notifications/utils/avatar.py b/src/sentry/notifications/utils/avatar.py index 6bb256fc524fc5..0908bb377771be 100644 --- a/src/sentry/notifications/utils/avatar.py +++ b/src/sentry/notifications/utils/avatar.py @@ -5,7 +5,7 @@ from django.utils.safestring import SafeString from sentry.models.avatars.user_avatar import UserAvatar -from sentry.models.user import User +from sentry.users.models.user import User from sentry.users.services.user import RpcUser from sentry.utils.assets import get_asset_url from sentry.utils.avatar import get_email_avatar diff --git a/src/sentry/notifications/utils/participants.py b/src/sentry/notifications/utils/participants.py index 03d42d2aeb5e61..59f169e6948968 100644 --- a/src/sentry/notifications/utils/participants.py +++ b/src/sentry/notifications/utils/participants.py @@ -23,7 +23,6 @@ from sentry.models.rule import Rule from sentry.models.rulesnooze import RuleSnooze from sentry.models.team import Team -from sentry.models.user import User from sentry.notifications.services import notifications_service from sentry.notifications.types import ( ActionTargetType, @@ -33,6 +32,7 @@ NotificationSettingsOptionEnum, ) from sentry.types.actor import Actor, ActorType +from sentry.users.models.user import User from sentry.users.services.user import RpcUser from sentry.users.services.user.service import user_service from sentry.users.services.user_option import get_option_from_list, user_option_service diff --git a/src/sentry/plugins/bases/notify.py b/src/sentry/plugins/bases/notify.py index 17d3319196bf0b..b946842b30741c 100644 --- a/src/sentry/plugins/bases/notify.py +++ b/src/sentry/plugins/bases/notify.py @@ -109,7 +109,7 @@ def notify_about_activity(self, activity): pass def get_notification_recipients(self, project, user_option: str) -> set: - from sentry.models.options.user_option import UserOption + from sentry.users.models.user_option import UserOption alert_settings = { o.user_id: int(o.value) diff --git a/src/sentry/plugins/helpers.py b/src/sentry/plugins/helpers.py index 0e96320117ceaf..c7e7d0ce71df31 100644 --- a/src/sentry/plugins/helpers.py +++ b/src/sentry/plugins/helpers.py @@ -2,9 +2,9 @@ from sentry import options from sentry.models.options.project_option import ProjectOption -from sentry.models.options.user_option import UserOption from sentry.models.project import Project from sentry.projects.services.project import RpcProject, project_service +from sentry.users.models.user_option import UserOption __all__ = ("set_option", "get_option", "unset_option") diff --git a/src/sentry/plugins/providers/integration_repository.py b/src/sentry/plugins/providers/integration_repository.py index 18ad287f6cdaf6..a4207a3b365fee 100644 --- a/src/sentry/plugins/providers/integration_repository.py +++ b/src/sentry/plugins/providers/integration_repository.py @@ -19,10 +19,10 @@ from sentry.integrations.services.repository.model import RpcCreateRepository from sentry.models.integrations.integration import Integration from sentry.models.repository import Repository -from sentry.models.user import User from sentry.organizations.services.organization.model import RpcOrganization from sentry.shared_integrations.exceptions import IntegrationError from sentry.signals import repo_linked +from sentry.users.models.user import User from sentry.users.services.user.serial import serialize_rpc_user from sentry.utils import metrics @@ -207,9 +207,11 @@ def dispatch(self, request: Request, organization, **kwargs): repository_service.serialize_repository( organization_id=organization.id, id=repo.id, - as_user=serialize_rpc_user(request.user) - if isinstance(request.user, User) - else request.user, + as_user=( + serialize_rpc_user(request.user) + if isinstance(request.user, User) + else request.user + ), ), status=201, ) diff --git a/src/sentry/ratelimits/utils.py b/src/sentry/ratelimits/utils.py index 0d173031b7b874..5a0b1c41327bf5 100644 --- a/src/sentry/ratelimits/utils.py +++ b/src/sentry/ratelimits/utils.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: from sentry.models.apitoken import ApiToken from sentry.models.organization import Organization - from sentry.models.user import User + from sentry.users.models.user import User # TODO(mgaeta): It's not currently possible to type a Callable's args with kwargs. EndpointFunction = Callable[..., Response] diff --git a/src/sentry/receivers/analytics.py b/src/sentry/receivers/analytics.py index 75d67f750e0b04..23f089fa0afa9d 100644 --- a/src/sentry/receivers/analytics.py +++ b/src/sentry/receivers/analytics.py @@ -1,7 +1,7 @@ from django.db.models.signals import post_save from sentry import analytics -from sentry.models.user import User +from sentry.users.models.user import User def capture_signal(type): diff --git a/src/sentry/receivers/auth.py b/src/sentry/receivers/auth.py index 8533430fab1d77..ee0c59594f301d 100644 --- a/src/sentry/receivers/auth.py +++ b/src/sentry/receivers/auth.py @@ -4,7 +4,7 @@ from django.contrib.auth.signals import user_logged_in from django.db.utils import DatabaseError -from sentry.models.options.user_option import UserOption +from sentry.users.models.user_option import UserOption # Set user language if set diff --git a/src/sentry/receivers/email.py b/src/sentry/receivers/email.py index e429c3c6914b06..dbe10ade882dc1 100644 --- a/src/sentry/receivers/email.py +++ b/src/sentry/receivers/email.py @@ -2,7 +2,7 @@ from django.db.models.signals import post_delete, post_save from sentry.models.email import Email -from sentry.models.useremail import UserEmail +from sentry.users.models.useremail import UserEmail def create_email(instance, created, **kwargs): diff --git a/src/sentry/receivers/sentry_apps.py b/src/sentry/receivers/sentry_apps.py index 634d4c5b7fa409..d01f60426d421f 100644 --- a/src/sentry/receivers/sentry_apps.py +++ b/src/sentry/receivers/sentry_apps.py @@ -10,7 +10,6 @@ from sentry.models.organization import Organization from sentry.models.project import Project from sentry.models.team import Team -from sentry.models.user import User from sentry.sentry_apps.apps import consolidate_events from sentry.sentry_apps.services.app import RpcSentryAppInstallation, app_service from sentry.signals import ( @@ -24,6 +23,7 @@ issue_unresolved, ) from sentry.tasks.sentry_apps import build_comment_webhook, workflow_notification +from sentry.users.models.user import User from sentry.users.services.user import RpcUser diff --git a/src/sentry/receivers/useremail.py b/src/sentry/receivers/useremail.py index 01a3e09217dc81..0ca3e030bcd475 100644 --- a/src/sentry/receivers/useremail.py +++ b/src/sentry/receivers/useremail.py @@ -1,8 +1,8 @@ from django.db import IntegrityError from django.db.models.signals import post_save -from sentry.models.user import User -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): diff --git a/src/sentry/receivers/users.py b/src/sentry/receivers/users.py index daf798498cb43a..e6cdebb00e11c1 100644 --- a/src/sentry/receivers/users.py +++ b/src/sentry/receivers/users.py @@ -1,8 +1,8 @@ import sys -from sentry.models.user import User from sentry.signals import post_upgrade from sentry.silo.base import SiloMode +from sentry.users.models.user import User from sentry.utils.settings import is_self_hosted diff --git a/src/sentry/rules/filters/assigned_to.py b/src/sentry/rules/filters/assigned_to.py index d1f67a4c8a91ac..631a3e377c0924 100644 --- a/src/sentry/rules/filters/assigned_to.py +++ b/src/sentry/rules/filters/assigned_to.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: from sentry.models.group import Group - from sentry.models.user import User + from sentry.users.models.user import User class AssignedToFilter(EventFilter): diff --git a/src/sentry/runner/commands/createuser.py b/src/sentry/runner/commands/createuser.py index e03958b66d3176..14caacc67593cc 100644 --- a/src/sentry/runner/commands/createuser.py +++ b/src/sentry/runner/commands/createuser.py @@ -9,13 +9,13 @@ if TYPE_CHECKING: from django.db.models.fields import Field - from sentry.models.user import User + from sentry.users.models.user import User def _get_field(field_name: str) -> Field[str, str]: from django.db.models.fields import Field - from sentry.models.user import User + from sentry.users.models.user import User ret = User._meta.get_field(field_name) assert isinstance(ret, Field), ret @@ -53,7 +53,7 @@ def _set_superadmin(user: User) -> None: superadmin role approximates superuser (model attribute) but leveraging Sentry's role system. """ - from sentry.models.userrole import UserRole, UserRoleUser + from sentry.users.models.userrole import UserRole, UserRoleUser role = UserRole.objects.get(name="Super Admin") UserRoleUser.objects.create(user=user, role=role) @@ -135,7 +135,7 @@ def createuser( raise click.ClickException("No password set and --no-password not passed.") from sentry import roles - from sentry.models.user import User + from sentry.users.models.user import User # Loop through the email list provided. for email in emails: diff --git a/src/sentry/runner/commands/permissions.py b/src/sentry/runner/commands/permissions.py index 93c6d2f5f01e2d..d48c16164dd540 100644 --- a/src/sentry/runner/commands/permissions.py +++ b/src/sentry/runner/commands/permissions.py @@ -8,7 +8,7 @@ from sentry.runner.decorators import configuration if TYPE_CHECKING: - from sentry.models.user import User + from sentry.users.models.user import User def user_param_to_user(value: str) -> User: @@ -38,7 +38,7 @@ def add(user: str, permission: str) -> None: "Add a permission to a user." from django.db import IntegrityError, transaction - from sentry.models.userpermission import UserPermission + from sentry.users.models.userpermission import UserPermission user_inst = user_param_to_user(user) @@ -57,7 +57,7 @@ def add(user: str, permission: str) -> None: @configuration def remove(user: str, permission: str) -> None: "Remove a permission from a user." - from sentry.models.userpermission import UserPermission + from sentry.users.models.userpermission import UserPermission user_inst = user_param_to_user(user) @@ -75,7 +75,7 @@ def remove(user: str, permission: str) -> None: @configuration def list(user: str) -> None: "List permissions for a user." - from sentry.models.userpermission import UserPermission + from sentry.users.models.userpermission import UserPermission user_inst = user_param_to_user(user) up_list = UserPermission.objects.filter(user=user_inst).order_by("permission") diff --git a/src/sentry/search/snuba/backend.py b/src/sentry/search/snuba/backend.py index 85185afb9b2a9f..9d01d40debc228 100644 --- a/src/sentry/search/snuba/backend.py +++ b/src/sentry/search/snuba/backend.py @@ -31,7 +31,6 @@ from sentry.models.project import Project from sentry.models.release import Release from sentry.models.team import Team -from sentry.models.user import User from sentry.search.base import SearchBackend from sentry.search.events.constants import EQUALITY_OPERATORS, OPERATOR_TO_DJANGO from sentry.search.snuba.executors import ( @@ -41,6 +40,7 @@ PostgresSnubaQueryExecutor, TrendsSortWeights, ) +from sentry.users.models.user import User from sentry.utils import metrics from sentry.utils.cursors import Cursor, CursorResult diff --git a/src/sentry/search/snuba/executors.py b/src/sentry/search/snuba/executors.py index a17ed24d9282cb..be8ef82c24b213 100644 --- a/src/sentry/search/snuba/executors.py +++ b/src/sentry/search/snuba/executors.py @@ -58,11 +58,11 @@ from sentry.models.organization import Organization from sentry.models.project import Project from sentry.models.team import Team -from sentry.models.user import User from sentry.search.events.builder.discover import UnresolvedQuery from sentry.search.events.filter import convert_search_filter_to_snuba_query, format_search_filter from sentry.search.events.types import ParamsType, SnubaParams from sentry.snuba.dataset import Dataset +from sentry.users.models.user import User from sentry.users.services.user.model import RpcUser from sentry.utils import json, metrics, snuba from sentry.utils.cursors import Cursor, CursorResult diff --git a/src/sentry/search/utils.py b/src/sentry/search/utils.py index be621ab29a2b2c..e47d620fb52f73 100644 --- a/src/sentry/search/utils.py +++ b/src/sentry/search/utils.py @@ -17,10 +17,10 @@ from sentry.models.project import Project from sentry.models.release import Release, follows_semver_versioning_scheme from sentry.models.team import Team -from sentry.models.user import User from sentry.search.base import ANY from sentry.search.events.constants import MAX_PARAMETERS_IN_ARRAY from sentry.types.group import SUBSTATUS_UPDATE_CHOICES +from sentry.users.models.user import User from sentry.users.services.user.model import RpcUser from sentry.users.services.user.serial import serialize_rpc_user from sentry.users.services.user.service import user_service diff --git a/src/sentry/security/emails.py b/src/sentry/security/emails.py index b2b5cbef7f233f..afc96d33bd1169 100644 --- a/src/sentry/security/emails.py +++ b/src/sentry/security/emails.py @@ -11,7 +11,7 @@ from sentry.utils.email import MessageBuilder if TYPE_CHECKING: - from sentry.models.user import User + from sentry.users.models.user import User def generate_security_email( diff --git a/src/sentry/security/utils.py b/src/sentry/security/utils.py index 9582db77fc3502..17b91a2963c55e 100644 --- a/src/sentry/security/utils.py +++ b/src/sentry/security/utils.py @@ -13,7 +13,7 @@ from .emails import generate_security_email if TYPE_CHECKING: - from sentry.models.user import User + from sentry.users.models.user import User logger = logging.getLogger("sentry.security") diff --git a/src/sentry/sentry_apps/apps.py b/src/sentry/sentry_apps/apps.py index bdaf960817c9f6..92bdf63f8cd6bd 100644 --- a/src/sentry/sentry_apps/apps.py +++ b/src/sentry/sentry_apps/apps.py @@ -32,13 +32,13 @@ ) from sentry.models.integrations.sentry_app_component import SentryAppComponent from sentry.models.integrations.sentry_app_installation import SentryAppInstallation -from sentry.models.user import User from sentry.receivers.tokens import add_scope_hierarchy from sentry.sentry_apps.installations import ( SentryAppInstallationCreator, SentryAppInstallationTokenCreator, ) from sentry.sentry_apps.services.hook import hook_service +from sentry.users.models.user import User from sentry.users.services.user.model import RpcUser Schema = Mapping[str, Any] diff --git a/src/sentry/sentry_apps/installations.py b/src/sentry/sentry_apps/installations.py index 680a33c2efe960..599bb70ecf4759 100644 --- a/src/sentry/sentry_apps/installations.py +++ b/src/sentry/sentry_apps/installations.py @@ -16,9 +16,9 @@ from sentry.models.integrations.sentry_app import SentryApp from sentry.models.integrations.sentry_app_installation import SentryAppInstallation from sentry.models.integrations.sentry_app_installation_token import SentryAppInstallationToken -from sentry.models.user import User from sentry.sentry_apps.services.hook import hook_service from sentry.tasks.sentry_apps import installation_webhook +from sentry.users.models.user import User from sentry.utils import metrics diff --git a/src/sentry/sentry_apps/services/app/impl.py b/src/sentry/sentry_apps/services/app/impl.py index 82db528146a3c6..f490998315c6ec 100644 --- a/src/sentry/sentry_apps/services/app/impl.py +++ b/src/sentry/sentry_apps/services/app/impl.py @@ -18,7 +18,6 @@ prepare_sentry_app_components, ) from sentry.models.integrations.sentry_app_installation_token import SentryAppInstallationToken -from sentry.models.user import User from sentry.sentry_apps.apps import SentryAppCreator from sentry.sentry_apps.services.app import ( AppService, @@ -36,6 +35,7 @@ serialize_sentry_app_component, serialize_sentry_app_installation, ) +from sentry.users.models.user import User from sentry.users.services.user import RpcUser diff --git a/src/sentry/snuba/models.py b/src/sentry/snuba/models.py index 02357edaa667b2..b9475c5d7def85 100644 --- a/src/sentry/snuba/models.py +++ b/src/sentry/snuba/models.py @@ -11,7 +11,7 @@ from sentry.db.models import FlexibleForeignKey, Model, region_silo_model from sentry.db.models.manager.base import BaseManager from sentry.models.team import Team -from sentry.models.user import User +from sentry.users.models.user import User class QueryAggregations(Enum): diff --git a/src/sentry/tasks/auth.py b/src/sentry/tasks/auth.py index 1589317a18678a..ebf193084c3136 100644 --- a/src/sentry/tasks/auth.py +++ b/src/sentry/tasks/auth.py @@ -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.user import User -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 diff --git a/src/sentry/tasks/backfill_outboxes.py b/src/sentry/tasks/backfill_outboxes.py index ddc2e3e456b7f7..7d79e927c51a3a 100644 --- a/src/sentry/tasks/backfill_outboxes.py +++ b/src/sentry/tasks/backfill_outboxes.py @@ -3,6 +3,7 @@ When the replication_version on any class is bumped, callers to process_outbox_backfill_batch will produce new outboxes incrementally to replicate those models. """ + from __future__ import annotations from dataclasses import dataclass @@ -14,8 +15,8 @@ from sentry import options from sentry.db.models.outboxes import ControlOutboxProducingModel, RegionOutboxProducingModel from sentry.models.outbox import outbox_context -from sentry.models.user import User from sentry.silo.base import SiloMode +from sentry.users.models.user import User from sentry.utils import json, metrics, redis diff --git a/src/sentry/tasks/beacon.py b/src/sentry/tasks/beacon.py index 3c61c4181fd285..ac6d8ae1ce51e2 100644 --- a/src/sentry/tasks/beacon.py +++ b/src/sentry/tasks/beacon.py @@ -114,7 +114,7 @@ def send_beacon() -> None: from sentry.models.organization import Organization from sentry.models.project import Project from sentry.models.team import Team - from sentry.models.user import User + from sentry.users.models.user import User install_id = get_install_id() diff --git a/src/sentry/tasks/commits.py b/src/sentry/tasks/commits.py index d81fbe9e864f0f..fe3f6369179f8b 100644 --- a/src/sentry/tasks/commits.py +++ b/src/sentry/tasks/commits.py @@ -15,11 +15,11 @@ from sentry.models.releaseheadcommit import ReleaseHeadCommit from sentry.models.releases.exceptions import ReleaseCommitError from sentry.models.repository import Repository -from sentry.models.user import User from sentry.plugins.base import bindings from sentry.shared_integrations.exceptions import IntegrationError from sentry.silo.base import SiloMode from sentry.tasks.base import instrumented_task, retry +from sentry.users.models.user import User from sentry.users.services.user import RpcUser from sentry.users.services.user.service import user_service from sentry.utils.email import MessageBuilder diff --git a/src/sentry/tasks/integrations/slack/link_slack_user_identities.py b/src/sentry/tasks/integrations/slack/link_slack_user_identities.py index c121cad0c7f926..57478907dda464 100644 --- a/src/sentry/tasks/integrations/slack/link_slack_user_identities.py +++ b/src/sentry/tasks/integrations/slack/link_slack_user_identities.py @@ -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.user import User -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") diff --git a/src/sentry/tasks/integrations/sync_assignee_outbound_impl.py b/src/sentry/tasks/integrations/sync_assignee_outbound_impl.py index 060d595a802b2a..36843f3ebc3f6c 100644 --- a/src/sentry/tasks/integrations/sync_assignee_outbound_impl.py +++ b/src/sentry/tasks/integrations/sync_assignee_outbound_impl.py @@ -3,9 +3,9 @@ from sentry.models.integrations.external_issue import ExternalIssue from sentry.models.integrations.integration import Integration from sentry.models.organization import Organization -from sentry.models.user import User from sentry.silo.base import SiloMode from sentry.tasks.base import instrumented_task, retry +from sentry.users.models.user import User from sentry.users.services.user.service import user_service diff --git a/src/sentry/tasks/merge.py b/src/sentry/tasks/merge.py index 7abd08647a718f..cfc04463844ed3 100644 --- a/src/sentry/tasks/merge.py +++ b/src/sentry/tasks/merge.py @@ -46,7 +46,7 @@ def merge_groups( from sentry.models.groupredirect import GroupRedirect from sentry.models.grouprulestatus import GroupRuleStatus from sentry.models.groupsubscription import GroupSubscription - from sentry.models.userreport import UserReport + from sentry.users.models.userreport import UserReport if not (from_object_ids and to_object_id): logger.error("group.malformed.missing_params", extra={"transaction_id": transaction_id}) diff --git a/src/sentry/tasks/post_process.py b/src/sentry/tasks/post_process.py index 2effb23da0a599..9ff4602565ff6a 100644 --- a/src/sentry/tasks/post_process.py +++ b/src/sentry/tasks/post_process.py @@ -335,7 +335,7 @@ def handle_group_owners( """ from sentry.models.groupowner import GroupOwner, GroupOwnerType, OwnerRuleType from sentry.models.team import Team - from sentry.models.user import User + from sentry.users.models.user import User from sentry.users.services.user import RpcUser lock = locks.get(f"groupowner-bulk:{group.id}", duration=10, name="groupowner_bulk") @@ -1387,7 +1387,7 @@ def check_has_high_priority_alerts(job: PostProcessJob) -> None: def link_event_to_user_report(job: PostProcessJob) -> None: from sentry.feedback.usecases.create_feedback import FeedbackCreationSource, shim_to_feedback - from sentry.models.userreport import UserReport + from sentry.users.models.userreport import UserReport event = job["event"] project = event.project diff --git a/src/sentry/tasks/relocation.py b/src/sentry/tasks/relocation.py index 865b23addf5a29..5b3c20477acdd5 100644 --- a/src/sentry/tasks/relocation.py +++ b/src/sentry/tasks/relocation.py @@ -45,11 +45,11 @@ RelocationValidationAttempt, ValidationStatus, ) -from sentry.models.user import User from sentry.organizations.services.organization import organization_service from sentry.signals import relocated, relocation_redeem_promo_code from sentry.silo.base import SiloMode from sentry.tasks.base import instrumented_task +from sentry.users.models.user import User from sentry.users.services.lost_password_hash import lost_password_hash_service from sentry.users.services.user.service import user_service from sentry.utils import json diff --git a/src/sentry/tasks/summaries/weekly_reports.py b/src/sentry/tasks/summaries/weekly_reports.py index e4ba0a03fe99cd..fa590d22d96afa 100644 --- a/src/sentry/tasks/summaries/weekly_reports.py +++ b/src/sentry/tasks/summaries/weekly_reports.py @@ -21,7 +21,6 @@ from sentry.models.grouphistory import GroupHistoryStatus from sentry.models.organization import Organization, OrganizationStatus from sentry.models.organizationmember import OrganizationMember -from sentry.models.user import User from sentry.notifications.services import notifications_service from sentry.silo.base import SiloMode from sentry.snuba.referrer import Referrer @@ -42,6 +41,7 @@ user_project_ownership, ) from sentry.types.group import GroupSubStatus +from sentry.users.models.user import User from sentry.utils import json, redis from sentry.utils.dates import floor_to_utc_day, to_datetime from sentry.utils.email import MessageBuilder diff --git a/src/sentry/tasks/unmerge.py b/src/sentry/tasks/unmerge.py index 62b05df04b97e3..d11953c622720b 100644 --- a/src/sentry/tasks/unmerge.py +++ b/src/sentry/tasks/unmerge.py @@ -23,12 +23,12 @@ from sentry.models.grouprelease import GroupRelease from sentry.models.project import Project from sentry.models.release import Release -from sentry.models.userreport import UserReport from sentry.silo.base import SiloMode from sentry.tasks.base import instrumented_task from sentry.tsdb.base import TSDBModel from sentry.types.activity import ActivityType from sentry.unmerge import InitialUnmergeArgs, SuccessiveUnmergeArgs, UnmergeArgs, UnmergeArgsBase +from sentry.users.models.userreport import UserReport from sentry.utils.eventuser import EventUser from sentry.utils.query import celery_run_batch_query from sentry.utils.safe import get_path diff --git a/src/sentry/tasks/update_user_reports.py b/src/sentry/tasks/update_user_reports.py index 334b75804ec333..9cb0c64899265b 100644 --- a/src/sentry/tasks/update_user_reports.py +++ b/src/sentry/tasks/update_user_reports.py @@ -7,9 +7,9 @@ from sentry import eventstore, features from sentry.feedback.usecases.create_feedback import FeedbackCreationSource, shim_to_feedback from sentry.models.project import Project -from sentry.models.userreport import UserReport from sentry.silo.base import SiloMode from sentry.tasks.base import instrumented_task +from sentry.users.models.userreport import UserReport from sentry.utils.iterators import chunked logger = logging.getLogger(__name__) diff --git a/src/sentry/templatetags/sentry_avatars.py b/src/sentry/templatetags/sentry_avatars.py index 5ae8015a053017..aaa7090773e3cb 100644 --- a/src/sentry/templatetags/sentry_avatars.py +++ b/src/sentry/templatetags/sentry_avatars.py @@ -3,7 +3,7 @@ from django import template from django.urls import reverse -from sentry.models.user import User +from sentry.users.models.user import User from sentry.users.services.user import RpcUser from sentry.users.services.user.service import user_service from sentry.utils.avatar import get_email_avatar, get_gravatar_url, get_letter_avatar diff --git a/src/sentry/testutils/cases.py b/src/sentry/testutils/cases.py index 263a97111a93eb..aaec793cb03145 100644 --- a/src/sentry/testutils/cases.py +++ b/src/sentry/testutils/cases.py @@ -103,7 +103,6 @@ from sentry.models.notificationsettingoption import NotificationSettingOption from sentry.models.notificationsettingprovider import NotificationSettingProvider from sentry.models.options.project_option import ProjectOption -from sentry.models.options.user_option import UserOption from sentry.models.organization import Organization from sentry.models.organizationmember import OrganizationMember from sentry.models.project import Project @@ -111,8 +110,6 @@ from sentry.models.releasecommit import ReleaseCommit from sentry.models.repository import Repository from sentry.models.rule import RuleSource -from sentry.models.user import User -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 @@ -145,6 +142,9 @@ from sentry.testutils.helpers.slack import install_slack 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.user_option import UserOption +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 diff --git a/src/sentry/testutils/factories.py b/src/sentry/testutils/factories.py index 8f157d93ce6105..f760b4da5ef124 100644 --- a/src/sentry/testutils/factories.py +++ b/src/sentry/testutils/factories.py @@ -106,7 +106,6 @@ NotificationAction, ) from sentry.models.notificationsettingprovider import NotificationSettingProvider -from sentry.models.options.user_option import UserOption from sentry.models.organization import Organization from sentry.models.organizationmapping import OrganizationMapping from sentry.models.organizationmember import OrganizationMember @@ -130,11 +129,6 @@ from sentry.models.savedsearch import SavedSearch from sentry.models.servicehook import ServiceHook from sentry.models.team import Team -from sentry.models.user import User -from sentry.models.useremail import UserEmail -from sentry.models.userpermission import UserPermission -from sentry.models.userreport import UserReport -from sentry.models.userrole import UserRole from sentry.organizations.services.organization import RpcOrganization, RpcUserOrganizationContext from sentry.sentry_apps.apps import SentryAppCreator from sentry.sentry_apps.installations import ( @@ -158,6 +152,12 @@ ProjectUptimeSubscriptionMode, UptimeSubscription, ) +from sentry.users.models.user import User +from sentry.users.models.user_option import UserOption +from sentry.users.models.useremail import UserEmail +from sentry.users.models.userpermission import UserPermission +from sentry.users.models.userreport import UserReport +from sentry.users.models.userrole import UserRole from sentry.users.services.user import RpcUser from sentry.utils import loremipsum from sentry.utils.performance_issues.performance_problem import PerformanceProblem diff --git a/src/sentry/testutils/fixtures.py b/src/sentry/testutils/fixtures.py index 39a7352ba6a421..827f0e8256ec5a 100644 --- a/src/sentry/testutils/fixtures.py +++ b/src/sentry/testutils/fixtures.py @@ -22,7 +22,6 @@ from sentry.models.project import Project from sentry.models.projecttemplate import ProjectTemplate from sentry.models.rule import Rule -from sentry.models.user import User from sentry.monitors.models import Monitor, MonitorType, ScheduleType from sentry.organizations.services.organization import RpcOrganization from sentry.silo.base import SiloMode @@ -39,6 +38,7 @@ ProjectUptimeSubscriptionMode, UptimeSubscription, ) +from sentry.users.models.user import User from sentry.users.services.user import RpcUser diff --git a/src/sentry/testutils/helpers/backups.py b/src/sentry/testutils/helpers/backups.py index 2d5af2e5c189d5..40ec0b92be3c3a 100644 --- a/src/sentry/testutils/helpers/backups.py +++ b/src/sentry/testutils/helpers/backups.py @@ -57,7 +57,6 @@ from sentry.models.apigrant import ApiGrant from sentry.models.apikey import ApiKey from sentry.models.apitoken import ApiToken -from sentry.models.authenticator import Authenticator from sentry.models.authidentity import AuthIdentity from sentry.models.authprovider import AuthProvider from sentry.models.counter import Counter @@ -82,7 +81,6 @@ from sentry.models.options.option import ControlOption, Option from sentry.models.options.organization_option import OrganizationOption from sentry.models.options.project_template_option import ProjectTemplateOption -from sentry.models.options.user_option import UserOption from sentry.models.organization import Organization from sentry.models.organizationaccessrequest import OrganizationAccessRequest from sentry.models.organizationmember import InviteStatus, OrganizationMember @@ -96,9 +94,6 @@ from sentry.models.rule import NeglectedRule, RuleActivity, RuleActivityType from sentry.models.savedsearch import SavedSearch, Visibility from sentry.models.search_common import SearchType -from sentry.models.user import User -from sentry.models.userip import UserIP -from sentry.models.userrole import UserRole, UserRoleUser from sentry.monitors.models import Monitor, MonitorType, ScheduleType from sentry.nodestore.django.models import Node from sentry.sentry_apps.apps import SentryAppUpdater @@ -113,6 +108,11 @@ from sentry.testutils.fixtures import Fixtures from sentry.testutils.silo import assume_test_silo_mode from sentry.types.token import AuthTokenType +from sentry.users.models.authenticator import Authenticator +from sentry.users.models.user import User +from sentry.users.models.user_option import UserOption +from sentry.users.models.userip import UserIP +from sentry.users.models.userrole import UserRole, UserRoleUser from sentry.utils import json __all__ = [ diff --git a/src/sentry/testutils/helpers/notifications.py b/src/sentry/testutils/helpers/notifications.py index 4ac45f6238821c..728e7ad6782b23 100644 --- a/src/sentry/testutils/helpers/notifications.py +++ b/src/sentry/testutils/helpers/notifications.py @@ -17,10 +17,10 @@ from sentry.issues.issue_occurrence import IssueEvidence, IssueOccurrence from sentry.models.group import Group from sentry.models.team import Team -from sentry.models.user import User from sentry.notifications.notifications.base import BaseNotification from sentry.notifications.utils.actions import MessageAction from sentry.types.actor import Actor +from sentry.users.models.user import User from sentry.users.services.user import RpcUser diff --git a/src/sentry/testutils/helpers/slack.py b/src/sentry/testutils/helpers/slack.py index baa99cc52bb290..fef581ebe6458d 100644 --- a/src/sentry/testutils/helpers/slack.py +++ b/src/sentry/testutils/helpers/slack.py @@ -10,9 +10,9 @@ from sentry.models.integrations.organization_integration import OrganizationIntegration from sentry.models.organization import Organization from sentry.models.team import Team -from sentry.models.user import User from sentry.silo.base import SiloMode from sentry.testutils.silo import assume_test_silo_mode +from sentry.users.models.user import User from sentry.utils import json diff --git a/src/sentry/testutils/pytest/sentry.py b/src/sentry/testutils/pytest/sentry.py index a3bbe9ea6dee96..96b61864e0d16d 100644 --- a/src/sentry/testutils/pytest/sentry.py +++ b/src/sentry/testutils/pytest/sentry.py @@ -365,7 +365,7 @@ def pytest_runtest_teardown(item: pytest.Item) -> None: from sentry.models.options.organization_option import OrganizationOption from sentry.models.options.project_option import ProjectOption - from sentry.models.options.user_option import UserOption + from sentry.users.models.user_option import UserOption OrganizationOption.objects.clear_local_cache() ProjectOption.objects.clear_local_cache() diff --git a/src/sentry/types/actor.py b/src/sentry/types/actor.py index 5fea404e3c91f7..ba4c6e869f8198 100644 --- a/src/sentry/types/actor.py +++ b/src/sentry/types/actor.py @@ -11,8 +11,8 @@ if TYPE_CHECKING: from sentry.models.team import Team - from sentry.models.user import User from sentry.organizations.services.organization import RpcTeam + from sentry.users.models.user import User class ActorType(str, Enum): @@ -76,8 +76,8 @@ def many_from_object(cls, objects: Iterable[ActorTarget]) -> list["Actor"]: missing actors will have actors generated. """ from sentry.models.team import Team - from sentry.models.user import User from sentry.organizations.services.organization import RpcTeam + from sentry.users.models.user import User result: list["Actor"] = [] grouped_by_type: MutableMapping[str, list[int]] = defaultdict(list) @@ -115,8 +115,8 @@ def from_object(cls, obj: ActorTarget) -> "Actor": Without the actor_id the Actor acts as a tuple of id and type. """ from sentry.models.team import Team - from sentry.models.user import User from sentry.organizations.services.organization import RpcTeam + from sentry.users.models.user import User if isinstance(obj, cls): return obj diff --git a/src/sentry/users/models/__init__.py b/src/sentry/users/models/__init__.py new file mode 100644 index 00000000000000..107f714af7d428 --- /dev/null +++ b/src/sentry/users/models/__init__.py @@ -0,0 +1,9 @@ +# from sentry.users.models.user import * # NOQA +# from sentry.users.models.useremail import * # NOQA +# from sentry.users.models.userip import * # NOQA +# from sentry.users.models.userpermission import * # NOQA +# from sentry.users.models.userreport import * # NOQA +# from sentry.users.models.userrole import * # NOQA +# from sentry.users.models.authenticator import * # NOQA +# from sentry.users.models.user_avatar import * # NOQA +# from sentry.users.models.user_option import * # NOQA diff --git a/src/sentry/users/models/authenticator.py b/src/sentry/users/models/authenticator.py new file mode 100644 index 00000000000000..df0f03ff4cbcc8 --- /dev/null +++ b/src/sentry/users/models/authenticator.py @@ -0,0 +1,204 @@ +from __future__ import annotations + +import base64 +import copy +from typing import Any, ClassVar + +from django.db import models +from django.utils import timezone +from django.utils.functional import cached_property +from django.utils.translation import gettext_lazy as _ +from fido2.ctap2 import AuthenticatorData + +from sentry.auth.authenticators import ( + AUTHENTICATOR_CHOICES, + AUTHENTICATOR_INTERFACES, + AUTHENTICATOR_INTERFACES_BY_TYPE, + available_authenticators, +) +from sentry.auth.authenticators.base import EnrollmentStatus +from sentry.backup.dependencies import NormalizedModelName, get_model_name +from sentry.backup.sanitize import SanitizableField, Sanitizer +from sentry.backup.scopes import RelocationScope +from sentry.db.models import ( + BoundedAutoField, + BoundedPositiveIntegerField, + FlexibleForeignKey, + control_silo_model, +) +from sentry.db.models.fields.picklefield import PickledObjectField +from sentry.db.models.manager.base import BaseManager +from sentry.db.models.outboxes import ControlOutboxProducingModel +from sentry.models.outbox import ControlOutboxBase, OutboxCategory +from sentry.types.region import find_regions_for_user + + +class AuthenticatorManager(BaseManager["Authenticator"]): + def all_interfaces_for_user(self, user, return_missing=False, ignore_backup=False): + """Returns a correctly sorted list of all interfaces the user + has enabled. If `return_missing` is set to `True` then all + interfaces are returned even if not enabled. + """ + + def _sort(x): + return sorted(x, key=lambda x: (x.type == 0, x.type)) + + # Collect interfaces user is enrolled in + ifaces = [ + x.interface + for x in Authenticator.objects.filter( + user_id=user.id, + type__in=[a.type for a in available_authenticators(ignore_backup=ignore_backup)], + ) + ] + + if return_missing: + # Collect additional interfaces that the user + # is not enrolled in + rvm = dict(AUTHENTICATOR_INTERFACES) + for iface in ifaces: + rvm.pop(iface.interface_id, None) + for iface_cls in rvm.values(): + if iface_cls.is_available: + ifaces.append(iface_cls()) + + return _sort(ifaces) + + def auto_add_recovery_codes(self, user, force=False): + """This automatically adds the recovery code backup interface in + case no backup interface is currently set for the user. Returns + the interface that was added. + """ + from sentry.auth.authenticators.recovery_code import RecoveryCodeInterface + + has_authenticators = False + + # If we're not forcing, check for a backup interface already setup + # or if it's missing, we'll need to set it. + if not force: + for authenticator in Authenticator.objects.filter( + user_id=user.id, type__in=[a.type for a in available_authenticators()] + ): + iface = authenticator.interface + if iface.is_backup_interface: + return + has_authenticators = True + + if has_authenticators or force: + interface = RecoveryCodeInterface() + interface.enroll(user) + return interface + + def get_interface(self, user, interface_id): + """Looks up an interface by interface ID for a user. If the + interface is not available but configured a + `Authenticator.DoesNotExist` will be raised just as if the + authenticator was not configured at all. + """ + interface = AUTHENTICATOR_INTERFACES.get(interface_id) + if interface is None or not interface.is_available: + raise LookupError("No such interface %r" % interface_id) + try: + return Authenticator.objects.get(user_id=user.id, type=interface.type).interface + except Authenticator.DoesNotExist: + return interface.generate(EnrollmentStatus.NEW) + + def bulk_users_have_2fa(self, user_ids): + """Checks if a list of user ids have 2FA configured. + Returns a dict of {: } + """ + authenticators = set( + Authenticator.objects.filter( + user__in=user_ids, + type__in=[a.type for a in available_authenticators(ignore_backup=True)], + ) + .distinct() + .values_list("user_id", flat=True) + ) + return {id: id in authenticators for id in user_ids} + + +class AuthenticatorConfig(PickledObjectField): + def _is_devices_config(self, value: Any) -> bool: + return isinstance(value, dict) and "devices" in value + + def get_db_prep_value(self, value, *args, **kwargs): + if self._is_devices_config(value): + # avoid mutating the original object + value = copy.deepcopy(value) + for device in value["devices"]: + # AuthenticatorData is a non-json-serializable bytes subclass + if isinstance(device["binding"], AuthenticatorData): + device["binding"] = base64.b64encode(device["binding"]).decode() + + return super().get_db_prep_value(value, *args, **kwargs) + + def to_python(self, value): + ret = super().to_python(value) + if self._is_devices_config(ret): + for device in ret["devices"]: + if isinstance(device["binding"], str): + device["binding"] = AuthenticatorData(base64.b64decode(device["binding"])) + return ret + + +@control_silo_model +class Authenticator(ControlOutboxProducingModel): + # It only makes sense to import/export this data when doing a full global backup/restore, so it + # lives in the `Global` scope, even though it only depends on the `User` model. + __relocation_scope__ = RelocationScope.Global + + id = BoundedAutoField(primary_key=True) + user = FlexibleForeignKey("sentry.User", db_index=True) + created_at = models.DateTimeField(_("created at"), default=timezone.now) + last_used_at = models.DateTimeField(_("last used at"), null=True) + type = BoundedPositiveIntegerField(choices=AUTHENTICATOR_CHOICES) + + config = AuthenticatorConfig() + + objects: ClassVar[AuthenticatorManager] = AuthenticatorManager() + + class AlreadyEnrolled(Exception): + pass + + class Meta: + app_label = "sentry" + db_table = "auth_authenticator" + verbose_name = _("authenticator") + verbose_name_plural = _("authenticators") + unique_together = (("user", "type"),) + + def outboxes_for_update(self, shard_identifier: int | None = None) -> list[ControlOutboxBase]: + regions = find_regions_for_user(self.user_id) + return OutboxCategory.USER_UPDATE.as_control_outboxes( + region_names=regions, + shard_identifier=self.user_id, + object_identifier=self.user_id, + ) + + @cached_property + def interface(self): + return AUTHENTICATOR_INTERFACES_BY_TYPE[self.type](self) + + def mark_used(self, save=True): + self.last_used_at = timezone.now() + if save: + self.save() + + def reset_fields(self, save=True): + self.created_at = timezone.now() + self.last_used_at = None + if save: + self.save() + + def __repr__(self): + return f"" + + @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) + + sanitizer.set_string(json, SanitizableField(model_name, "config"), lambda _: '""') diff --git a/src/sentry/users/models/user.py b/src/sentry/users/models/user.py new file mode 100644 index 00000000000000..f9c43bd90aea23 --- /dev/null +++ b/src/sentry/users/models/user.py @@ -0,0 +1,598 @@ +from __future__ import annotations + +import logging +import secrets +import warnings +from collections.abc import Mapping +from string import ascii_letters, digits +from typing import Any, ClassVar + +from django.contrib.auth.models import AbstractBaseUser +from django.contrib.auth.models import UserManager as DjangoUserManager +from django.contrib.auth.signals import user_logged_out +from django.db import IntegrityError, models, router, transaction +from django.db.models import Count, Subquery +from django.db.models.query import QuerySet +from django.dispatch import receiver +from django.forms import model_to_dict +from django.urls import reverse +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ + +from bitfield import TypedClassBitField +from sentry.auth.authenticators import available_authenticators +from sentry.backup.dependencies import ( + ImportKind, + NormalizedModelName, + PrimaryKeyMap, + get_model_name, + merge_users_for_model_in_org, +) +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 Model, control_silo_model, sane_repr +from sentry.db.models.manager.base import BaseManager +from sentry.db.models.utils import unique_db_instance +from sentry.db.postgres.transactions import enforce_constraints +from sentry.integrations.types import EXTERNAL_PROVIDERS, ExternalProviders +from sentry.locks import locks +from sentry.models.avatars.user_avatar import UserAvatar +from sentry.models.lostpasswordhash import LostPasswordHash +from sentry.models.organizationmapping import OrganizationMapping +from sentry.models.organizationmembermapping import OrganizationMemberMapping +from sentry.models.orgauthtoken import OrgAuthToken +from sentry.models.outbox import ControlOutboxBase, OutboxCategory, outbox_context +from sentry.organizations.services.organization import RpcRegionUser, organization_service +from sentry.types.region import find_all_region_names, find_regions_for_user +from sentry.users.models.authenticator import Authenticator +from sentry.users.services.user import RpcUser +from sentry.utils.http import absolute_uri +from sentry.utils.retries import TimedRetryPolicy + +audit_logger = logging.getLogger("sentry.audit.user") + +MAX_USERNAME_LENGTH = 128 +RANDOM_PASSWORD_ALPHABET = ascii_letters + digits +RANDOM_PASSWORD_LENGTH = 32 + + +class UserManager(BaseManager["User"], DjangoUserManager["User"]): + def get_users_with_only_one_integration_for_provider( + self, provider: ExternalProviders, organization_id: int + ) -> QuerySet[User]: + """ + For a given organization, get the list of members that are only + connected to a single integration. + """ + from sentry.models.integrations.organization_integration import OrganizationIntegration + from sentry.models.organizationmembermapping import OrganizationMemberMapping + + org_user_ids = OrganizationMemberMapping.objects.filter( + organization_id=organization_id + ).values("user_id") + org_members_with_provider = ( + OrganizationMemberMapping.objects.values("user_id") + .annotate(org_counts=Count("organization_id")) + .filter( + user_id__in=Subquery(org_user_ids), + organization_id__in=Subquery( + OrganizationIntegration.objects.filter( + integration__provider=EXTERNAL_PROVIDERS[provider] + ).values("organization_id") + ), + org_counts=1, + ) + .values("user_id") + ) + return self.filter(id__in=Subquery(org_members_with_provider)) + + +@control_silo_model +class User(Model, AbstractBaseUser): + __relocation_scope__ = RelocationScope.User + __relocation_custom_ordinal__ = ["username"] + + replication_version: int = 2 + + username = models.CharField(_("username"), max_length=MAX_USERNAME_LENGTH, unique=True) + # this column is called first_name for legacy reasons, but it is the entire + # display name + name = models.CharField(_("name"), max_length=200, blank=True, db_column="first_name") + email = models.EmailField(_("email address"), blank=True, max_length=75) + is_staff = models.BooleanField( + _("staff status"), + default=False, + help_text=_("Designates whether the user can log into this admin site."), + ) + is_active = models.BooleanField( + _("active"), + default=True, + help_text=_( + "Designates whether this user should be treated as " + "active. Unselect this instead of deleting accounts." + ), + ) + is_unclaimed = models.BooleanField( + _("unclaimed"), + default=False, + help_text=_( + "Designates that this user was imported via the relocation tool, but has not yet been " + "claimed by the owner of the associated email. Users in this state have randomized " + "passwords - when email owners claim the account, they are prompted to reset their " + "password and do a one-time update to their username." + ), + ) + is_superuser = models.BooleanField( + _("superuser status"), + default=False, + help_text=_( + "Designates that this user has all permissions without explicitly assigning them." + ), + ) + is_managed = models.BooleanField( + _("managed"), + default=False, + help_text=_( + "Designates whether this user should be treated as " + "managed. Select this to disallow the user from " + "modifying their account (username, password, etc)." + ), + ) + is_sentry_app = models.BooleanField( + _("is sentry app"), + null=True, + default=None, + help_text=_( + "Designates whether this user is the entity used for Permissions" + "on behalf of a Sentry App. Cannot login or use Sentry like a" + "normal User would." + ), + ) + is_password_expired = models.BooleanField( + _("password expired"), + default=False, + help_text=_( + "If set to true then the user needs to change the " "password on next sign in." + ), + ) + last_password_change = models.DateTimeField( + _("date of last password change"), + null=True, + help_text=_("The date the password was changed last."), + ) + + class flags(TypedClassBitField): + # WARNING: Only add flags to the bottom of this list + # bitfield flags are dependent on their order and inserting/removing + # flags from the middle of the list will cause bits to shift corrupting + # existing data. + + # Do we need to ask this user for newsletter consent? + newsletter_consent_prompt: bool + + bitfield_default = 0 + bitfield_null = True + + session_nonce = models.CharField(max_length=12, null=True) + + date_joined = models.DateTimeField(_("date joined"), default=timezone.now) + last_active = models.DateTimeField(_("last active"), default=timezone.now, null=True) + + avatar_type = models.PositiveSmallIntegerField(default=0, choices=UserAvatar.AVATAR_TYPES) + avatar_url = models.CharField(_("avatar url"), max_length=120, null=True) + + objects: ClassVar[UserManager] = UserManager(cache_fields=["pk"]) + + USERNAME_FIELD = "username" + REQUIRED_FIELDS = ["email"] + + class Meta: + app_label = "sentry" + db_table = "auth_user" + verbose_name = _("user") + verbose_name_plural = _("users") + + __repr__ = sane_repr("id") + + def class_name(self): + return "User" + + def delete(self): + if self.username == "sentry": + raise Exception('You cannot delete the "sentry" user as it is required by Sentry.') + with outbox_context(transaction.atomic(using=router.db_for_write(User))): + avatar = self.avatar.first() + if avatar: + avatar.delete() + for outbox in self.outboxes_for_update(is_user_delete=True): + outbox.save() + return super().delete() + + def update(self, *args, **kwds): + with outbox_context(transaction.atomic(using=router.db_for_write(User))): + for outbox in self.outboxes_for_update(): + outbox.save() + return super().update(*args, **kwds) + + def save(self, *args, **kwargs): + with outbox_context(transaction.atomic(using=router.db_for_write(User))): + if not self.username: + self.username = self.email + result = super().save(*args, **kwargs) + for outbox in self.outboxes_for_update(): + outbox.save() + return result + + def has_perm(self, perm_name): + warnings.warn("User.has_perm is deprecated", DeprecationWarning) + return self.is_superuser + + def has_module_perms(self, app_label): + warnings.warn("User.has_module_perms is deprecated", DeprecationWarning) + return self.is_superuser + + def has_2fa(self): + return Authenticator.objects.filter( + user_id=self.id, type__in=[a.type for a in available_authenticators(ignore_backup=True)] + ).exists() + + def get_unverified_emails(self): + return self.emails.filter(is_verified=False) + + def get_verified_emails(self): + return self.emails.filter(is_verified=True) + + def has_verified_emails(self): + return self.get_verified_emails().exists() + + def has_unverified_emails(self): + return self.get_unverified_emails().exists() + + def has_usable_password(self): + if self.password == "" or self.password is None: + # This is the behavior we've been relying on from Django 1.6 - 2.0. + # In 2.1, a "" or None password is considered usable. + # Removing this override requires identifying all the places + # to put set_unusable_password and a migration. + return False + return super().has_usable_password() + + def get_label(self): + return self.email or self.username or self.id + + def get_display_name(self): + return self.name or self.email or self.username + + def get_full_name(self): + return self.name + + def get_salutation_name(self) -> str: + name = self.name or self.username.split("@", 1)[0].split(".", 1)[0] + first_name = name.split(" ", 1)[0] + return first_name.capitalize() + + def get_avatar_type(self): + return self.get_avatar_type_display() + + def get_actor_identifier(self): + return f"user:{self.id}" + + def send_confirm_email_singular(self, email, is_new_user=False): + from sentry import options + from sentry.utils.email import MessageBuilder + + if not email.hash_is_valid(): + email.set_hash() + email.save() + + context = { + "user": self, + "url": absolute_uri( + reverse("sentry-account-confirm-email", args=[self.id, email.validation_hash]) + ), + "confirm_email": email.email, + "is_new_user": is_new_user, + } + msg = MessageBuilder( + subject="{}Confirm Email".format(options.get("mail.subject-prefix")), + template="sentry/emails/confirm_email.txt", + html_template="sentry/emails/confirm_email.html", + type="user.confirm_email", + context=context, + ) + msg.send_async([email.email]) + + def send_confirm_emails(self, is_new_user=False): + email_list = self.get_unverified_emails() + for email in email_list: + self.send_confirm_email_singular(email, is_new_user) + + def outboxes_for_update(self, is_user_delete: bool = False) -> list[ControlOutboxBase]: + return User.outboxes_for_user_update(self.id, is_user_delete=is_user_delete) + + @staticmethod + def outboxes_for_user_update( + identifier: int, is_user_delete: bool = False + ) -> list[ControlOutboxBase]: + # User deletions must fan out to all regions to ensure cascade behavior + # of anything with a HybridCloudForeignKey, even if the user is no longer + # a member of any organizations in that region. + if is_user_delete: + user_regions = set(find_all_region_names()) + else: + user_regions = find_regions_for_user(identifier) + + return OutboxCategory.USER_UPDATE.as_control_outboxes( + region_names=user_regions, + object_identifier=identifier, + shard_identifier=identifier, + ) + + def merge_to(from_user: User, to_user: User) -> None: + # TODO: we could discover relations automatically and make this useful + from sentry.models.auditlogentry import AuditLogEntry + from sentry.models.authidentity import AuthIdentity + from sentry.models.avatars.user_avatar import UserAvatar + from sentry.models.identity import Identity + from sentry.models.organizationmembermapping import OrganizationMemberMapping + from sentry.users.models.authenticator import Authenticator + from sentry.users.models.user_option import UserOption + from sentry.users.models.useremail import UserEmail + + from_user_id = from_user.id + to_user_id = to_user.id + + audit_logger.info( + "user.merge", extra={"from_user_id": from_user_id, "to_user_id": to_user_id} + ) + + organization_ids = OrganizationMemberMapping.objects.filter( + user_id=from_user_id + ).values_list("organization_id", flat=True) + + for organization_id in organization_ids: + organization_service.merge_users( + organization_id=organization_id, from_user_id=from_user_id, to_user_id=to_user_id + ) + + # Update all organization control models to only use the new user id. + # + # TODO: in the future, proactively update `OrganizationMemberTeamReplica` as well. + with enforce_constraints( + transaction.atomic(using=router.db_for_write(OrganizationMemberMapping)) + ): + control_side_org_models: tuple[type[Model], ...] = ( + OrgAuthToken, + OrganizationMemberMapping, + ) + for model in control_side_org_models: + merge_users_for_model_in_org( + model, + organization_id=organization_id, + from_user_id=from_user_id, + to_user_id=to_user_id, + ) + + # While it would be nice to make the following changes in a transaction, there are too many + # unique constraints to make this feasible. Instead, we just do it sequentially and ignore + # the `IntegrityError`s. + user_related_models: tuple[type[Model], ...] = ( + Authenticator, + Identity, + UserAvatar, + UserEmail, + UserOption, + ) + for model in user_related_models: + for obj in model.objects.filter(user_id=from_user_id): + try: + with transaction.atomic(using=router.db_for_write(User)): + obj.update(user_id=to_user_id) + except IntegrityError: + pass + + # users can be either the subject or the object of actions which get logged + AuditLogEntry.objects.filter(actor=from_user).update(actor=to_user) + AuditLogEntry.objects.filter(target_user=from_user).update(target_user=to_user) + + with outbox_context(flush=False): + # remove any SSO identities that exist on from_user that might conflict + # with to_user's existing identities (only applies if both users have + # SSO identities in the same org), then pass the rest on to to_user + # NOTE: This could, become calls to identity_service.delete_ide + for ai in AuthIdentity.objects.filter( + user=from_user, + auth_provider__organization_id__in=AuthIdentity.objects.filter( + user_id=to_user_id + ).values("auth_provider__organization_id"), + ): + ai.delete() + for ai in AuthIdentity.objects.filter(user_id=from_user.id): + ai.update(user=to_user) + + def set_password(self, raw_password): + super().set_password(raw_password) + self.last_password_change = timezone.now() + self.is_password_expired = False + + def refresh_session_nonce(self, request=None): + from django.utils.crypto import get_random_string + + self.session_nonce = get_random_string(12) + if request is not None: + request.session["_nonce"] = self.session_nonce + + def has_org_requiring_2fa(self) -> bool: + from sentry.models.organization import OrganizationStatus + + return OrganizationMemberMapping.objects.filter( + user_id=self.id, + organization_id__in=Subquery( + OrganizationMapping.objects.filter( + require_2fa=True, + status=OrganizationStatus.ACTIVE, + ).values("organization_id") + ), + ).exists() + + def clear_lost_passwords(self): + LostPasswordHash.objects.filter(user=self).delete() + + def normalize_before_relocation_import( + self, pk_map: PrimaryKeyMap, scope: ImportScope, flags: ImportFlags + ) -> int | None: + old_pk = super().normalize_before_relocation_import(pk_map, scope, flags) + if old_pk is None: + return None + + # Importing in any scope besides `Global` (which does a naive, blanket restore of all data) + # and `Config` (which is explicitly meant to import admin accounts) should strip all + # incoming users of their admin privileges. + if scope not in {ImportScope.Config, ImportScope.Global}: + self.is_staff = False + self.is_superuser = False + self.is_managed = False + + # No need to mark users newly "unclaimed" when doing a global backup/restore. + if scope != ImportScope.Global or self.is_unclaimed: + # New users are marked unclaimed. + self.is_unclaimed = True + + # Give the user a cryptographically secure random password. The purpose here is to have + # a password that NO ONE knows - the only way to log into this account is to use the + # "claim your account" flow to create a new password (or to click "lost password" and + # end up there anyway), at which point we'll detect the user's `is_unclaimed` status and + # prompt them to change their `username` as well. + self.set_password( + "".join( + secrets.choice(RANDOM_PASSWORD_ALPHABET) for _ in range(RANDOM_PASSWORD_LENGTH) + ) + ) + + return old_pk + + def write_relocation_import( + self, scope: ImportScope, flags: ImportFlags + ) -> tuple[int, ImportKind] | None: + # Internal function that factors our some common logic. + def do_write(): + from sentry.api.endpoints.user_details import ( + BaseUserSerializer, + SuperuserUserSerializer, + UserSerializer, + ) + from sentry.users.services.lost_password_hash.impl import ( + DatabaseLostPasswordHashService, + ) + + serializer_cls = BaseUserSerializer + if scope not in {ImportScope.Config, ImportScope.Global}: + serializer_cls = UserSerializer + else: + serializer_cls = SuperuserUserSerializer + + serializer_user = serializer_cls(instance=self, data=model_to_dict(self), partial=True) + serializer_user.is_valid(raise_exception=True) + + self.save(force_insert=True) + + if scope != ImportScope.Global: + DatabaseLostPasswordHashService().get_or_create(user_id=self.id) + + # TODO(getsentry/team-ospo#191): we need to send an email informing the user of their + # new account with a resettable password - we'll need to figure out where in the process + # that actually goes, and how to prevent it from happening during the validation pass. + return (self.pk, ImportKind.Inserted) + + # If there is no existing user with this `username`, no special renaming or merging + # shenanigans are needed, as we can just insert this exact model directly. + existing = User.objects.filter(username=self.username).first() + if not existing: + return do_write() + + # Re-use the existing user if merging is enabled. + if flags.merge_users: + return (existing.pk, ImportKind.Existing) + + # We already have a user with this `username`, but merging users has not been enabled. In + # this case, add a random suffix to the importing username. + lock = locks.get(f"user:username:{self.id}", duration=10, name="username") + with TimedRetryPolicy(10)(lock.acquire): + unique_db_instance( + self, + self.username, + max_length=MAX_USERNAME_LENGTH, + field_name="username", + ) + + # Perform the remainder of the write while we're still holding the lock. + return do_write() + + @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) + + sanitizer.set_string(json, SanitizableField(model_name, "username")) + sanitizer.set_string(json, SanitizableField(model_name, "session_nonce")) + + @classmethod + def handle_async_deletion( + cls, + identifier: int, + region_name: str, + shard_identifier: int, + payload: Mapping[str, Any] | None, + ) -> None: + from sentry.hybridcloud.rpc.caching import region_caching_service + from sentry.users.services.user.service import get_many_by_id, get_user + + region_caching_service.clear_key(key=get_user.key_from(identifier), region_name=region_name) + region_caching_service.clear_key( + key=get_many_by_id.key_from(identifier), region_name=region_name + ) + + def handle_async_replication(self, region_name: str, shard_identifier: int) -> None: + from sentry.hybridcloud.rpc.caching import region_caching_service + from sentry.users.services.user.service import get_many_by_id, get_user + + region_caching_service.clear_key(key=get_user.key_from(self.id), region_name=region_name) + region_caching_service.clear_key( + key=get_many_by_id.key_from(self.id), region_name=region_name + ) + organization_service.update_region_user( + user=RpcRegionUser( + id=self.id, + is_active=self.is_active, + email=self.email, + ), + region_name=region_name, + ) + + +# HACK(dcramer): last_login needs nullable for Django 1.8 +User._meta.get_field("last_login").null = True + + +# When a user logs out, we want to always log them out of all +# sessions and refresh their nonce. +@receiver(user_logged_out, sender=User) +def refresh_user_nonce(sender, request, user, **kwargs): + if user is None: + return + user.refresh_session_nonce() + user.save(update_fields=["session_nonce"]) + + +@receiver(user_logged_out, sender=RpcUser) +def refresh_api_user_nonce(sender, request, user, **kwargs): + if user is None: + return + user = User.objects.get(id=user.id) + refresh_user_nonce(sender, request, user, **kwargs) + + +OutboxCategory.USER_UPDATE.connect_control_model_updates(User) diff --git a/src/sentry/users/models/user_option.py b/src/sentry/users/models/user_option.py new file mode 100644 index 00000000000000..dcdaf4f626f38f --- /dev/null +++ b/src/sentry/users/models/user_option.py @@ -0,0 +1,264 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, ClassVar + +from django.conf import settings +from django.db import models + +from sentry.backup.dependencies import ImportKind, PrimaryKeyMap, get_model_name +from sentry.backup.helpers import ImportFlags +from sentry.backup.scopes import ImportScope, RelocationScope +from sentry.db.models import FlexibleForeignKey, Model, control_silo_model, sane_repr +from sentry.db.models.fields import PickledObjectField +from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey +from sentry.db.models.manager.option import OptionManager + +if TYPE_CHECKING: + from sentry.models.organization import Organization + from sentry.models.project import Project + from sentry.users.models.user import User + from sentry.users.services.user import RpcUser + +option_scope_error = "this is not a supported use case, scope to project OR organization" + + +class UserOptionManager(OptionManager["UserOption"]): + def _make_key( # type: ignore[override] + self, + user: User | RpcUser | int, + project: Project | int | None = None, + organization: Organization | int | None = None, + ) -> str: + uid = user.id if user and not isinstance(user, int) else user + org_id: int | None = organization.id if isinstance(organization, Model) else organization + proj_id: int | None = project.id if isinstance(project, Model) else project + if project: + metakey = f"{uid}:{proj_id}:project" + elif organization: + metakey = f"{uid}:{org_id}:organization" + else: + metakey = f"{uid}:user" + + return super()._make_key(metakey) + + def get_value( + self, user: User | RpcUser, key: str, default: Any | None = None, **kwargs: Any + ) -> Any: + project = kwargs.get("project") + organization = kwargs.get("organization") + + if organization and project: + raise NotImplementedError(option_scope_error) + if organization: + result = self.get_all_values(user, None, organization) + else: + result = self.get_all_values(user, project) + return result.get(key, default) + + def unset_value(self, user: User, project: Project, key: str) -> None: + """ + This isn't implemented for user-organization scoped options yet, because it hasn't been needed. + """ + self.filter(user=user, project=project, key=key).delete() + + if not hasattr(self, "_metadata"): + return + + metakey = self._make_key(user, project=project) + + if metakey not in self._option_cache: + return + self._option_cache[metakey].pop(key, None) + + def set_value(self, user: User | int, key: str, value: Any, **kwargs: Any) -> None: + project = kwargs.get("project") + organization = kwargs.get("organization") + project_id = kwargs.get("project_id", None) + organization_id = kwargs.get("organization_id", None) + if project is not None: + project_id = project.id + if organization is not None: + organization_id = organization.id + + if organization and project: + raise NotImplementedError(option_scope_error) + + inst, created = self.get_or_create( + user_id=user.id if user and not isinstance(user, int) else user, + project_id=project_id, + organization_id=organization_id, + key=key, + defaults={"value": value}, + ) + if not created and inst.value != value: + inst.update(value=value) + + metakey = self._make_key(user, project=project, organization=organization) + + if metakey not in self._option_cache: + return + self._option_cache[metakey][key] = value + + def get_all_values( + self, + user: User | RpcUser | int, + project: Project | int | None = None, + organization: Organization | int | None = None, + force_reload: bool = False, + ) -> Mapping[str, Any]: + if organization and project: + raise NotImplementedError(option_scope_error) + + uid = user.id if user and not isinstance(user, int) else user + metakey = self._make_key(user, project=project, organization=organization) + project_id: int | None = project.id if isinstance(project, Model) else project + organization_id: int | None = ( + organization.id if isinstance(organization, Model) else organization + ) + + if metakey not in self._option_cache or force_reload: + result = { + i.key: i.value + for i in self.filter( + user_id=uid, project_id=project_id, organization_id=organization_id + ) + } + self._option_cache[metakey] = result + + return self._option_cache.get(metakey, {}) + + def post_save(self, instance: UserOption, **kwargs: Any) -> None: + self.get_all_values( + instance.user, instance.project_id, instance.organization_id, force_reload=True + ) + + def post_delete(self, instance: UserOption, **kwargs: Any) -> None: + self.get_all_values( + instance.user, instance.project_id, instance.organization_id, force_reload=True + ) + + +# TODO(dcramer): the NULL UNIQUE constraint here isn't valid, and instead has to +# be manually replaced in the database. We should restructure this model. +@control_silo_model +class UserOption(Model): + """ + User options apply only to a user, and optionally a project OR an organization. + + Options which are specific to a plugin should namespace + their key. e.g. key='myplugin:optname' + + Keeping user feature state + key: "feature:assignment" + value: { updated: datetime, state: bool } + + where key is one of: + (please add to this list if adding new keys) + - clock_24_hours + - 12hr vs. 24hr + - issue:defaults + - only used in Jira, set default reporter field + - issues:defaults:jira + - unused + - issues:defaults:jira_server + - unused + - issue_details_new_experience_q4_2023 + - Whether the user has opted into the new issue details experience (boolean) + - language + - which language to display the app in + - mail:email + - which email address to send an email to + - reports:disabled-organizations + - which orgs to not send weekly reports to + - seen_release_broadcast + - unused + - self_assign_issue + - "Claim Unassigned Issues I've Resolved" + - self_notifications + - "Notify Me About My Own Activity" + - stacktrace_order + - default, most recent first, most recent last + - subscribe_by_default + - "Only On Issues I Subscribe To", "Only On Deploys With My Commits" + - subscribe_notes + - unused + - timezone + - user's timezone to display timestamps + - theme + - dark, light, or default + - twilio:alert + - unused + - workflow_notifications + - unused + """ + + __relocation_scope__ = RelocationScope.User + + user = FlexibleForeignKey(settings.AUTH_USER_MODEL) + project_id = HybridCloudForeignKey("sentry.Project", null=True, on_delete="CASCADE") + organization_id = HybridCloudForeignKey("sentry.Organization", null=True, on_delete="CASCADE") + key = models.CharField(max_length=64) + value = PickledObjectField() + + objects: ClassVar[UserOptionManager] = UserOptionManager() + + class Meta: + app_label = "sentry" + db_table = "sentry_useroption" + unique_together = (("user", "project_id", "key"), ("user", "organization_id", "key")) + + __repr__ = sane_repr("user_id", "project_id", "organization_id", "key", "value") + + @classmethod + def get_relocation_ordinal_fields(self, json_model: Any) -> list[str] | None: + # "global" user options (those with no organization and/or project scope) get a custom + # ordinal; non-global ones use the default ordering. + org_id = json_model["fields"].get("organization_id", None) + project_id = json_model["fields"].get("project_id", None) + if org_id is None and project_id is None: + return ["user", "key"] + + return None + + 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 options and use the existing user's + # options instead. + if pk_map.get_kind(get_model_name(User), old_user_id) == ImportKind.Existing: + return None + + return old_pk + + def write_relocation_import( + self, scope: ImportScope, flags: ImportFlags + ) -> tuple[int, ImportKind] | None: + # TODO(getsentry/team-ospo#190): This circular import is a bit gross. See if we can't find a + # better place for this logic to live. + from sentry.api.endpoints.user_details import UserOptionsSerializer + + serializer_options = UserOptionsSerializer(data={self.key: self.value}, partial=True) + serializer_options.is_valid(raise_exception=True) + + # TODO(getsentry/team-ospo#190): Find a more general solution to one-off indices such as + # this. We currently have this constraint on prod, but not in Django, probably from legacy + # SQL manipulation. + # + # Ensure that global (ie: `organization_id` and `project_id` both `NULL`) constraints are + # not duplicated on import. + if self.organization_id is None and self.project_id is None: + colliding_global_user_option = self.objects.filter( + user=self.user, key=self.key, organization_id__isnull=True, project_id__isnull=True + ).first() + if colliding_global_user_option is not None: + return None + + return super().write_relocation_import(scope, flags) diff --git a/src/sentry/users/models/useremail.py b/src/sentry/users/models/useremail.py new file mode 100644 index 00000000000000..6fc02f2a012852 --- /dev/null +++ b/src/sentry/users/models/useremail.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +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.db.models.outboxes import ControlOutboxProducingModel +from sentry.models.outbox import ControlOutboxBase, 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 + ) diff --git a/src/sentry/users/models/userip.py b/src/sentry/users/models/userip.py new file mode 100644 index 00000000000000..2b6ae56c491f57 --- /dev/null +++ b/src/sentry/users/models/userip.py @@ -0,0 +1,142 @@ +from __future__ import annotations + +from typing import Any + +from django.conf import settings +from django.core.cache import cache +from django.db import models +from django.utils import timezone + +from sentry.audit_log.services.log import UserIpEvent, log_service +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, Model, control_silo_model, sane_repr +from sentry.users.models.user import User +from sentry.users.services.user import RpcUser +from sentry.utils.geo import geo_by_addr + + +@control_silo_model +class UserIP(Model): + # There is an absolutely massive number of `UserIP` models in any sufficiently long-lived + # install of Sentry. So while it would probably make semantic sense to have this be + # `RelocationScope.User`, only someone interested in backing up every bit of data could want + # this (we certainly don't need it on prod for relocation). Thus, this gets moved into the + # `Global` scope instead. + __relocation_scope__ = RelocationScope.Global + __relocation_custom_ordinal__ = ["user", "ip_address"] + + user = FlexibleForeignKey(settings.AUTH_USER_MODEL) + ip_address = models.GenericIPAddressField() + country_code = models.CharField(max_length=16, null=True) + region_code = models.CharField(max_length=16, null=True) + first_seen = models.DateTimeField(default=timezone.now) + last_seen = models.DateTimeField(default=timezone.now) + + class Meta: + app_label = "sentry" + db_table = "sentry_userip" + unique_together = (("user", "ip_address"),) + + __repr__ = sane_repr("user_id", "ip_address") + + @classmethod + def log(cls, user: User | RpcUser, ip_address: str): + # Only log once every 5 minutes for the same user/ip_address pair + # since this is hit pretty frequently by all API calls in the UI, etc. + cache_key = f"userip.log:{user.id}:{ip_address}" + if not cache.get(cache_key): + _perform_log(user, ip_address) + cache.set(cache_key, 1, 300) + + 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 IP and use the existing user's IP instead. + if pk_map.get_kind(get_model_name(User), old_user_id) == ImportKind.Existing: + return None + + # We'll recalculate the country codes from the IP when we call `log()` in + # `write_relocation_import()`. + self.country_code = None + self.region_code = None + + # Only preserve the submitted timing data in the backup/restore scope. + if scope != ImportScope.Global: + self.first_seen = self.last_seen = timezone.now() + + return old_pk + + def write_relocation_import( + self, _s: ImportScope, _f: ImportFlags + ) -> tuple[int, ImportKind] | None: + # Ensures that the IP address is valid. Exclude the codes, as they should be `None` until we + # `log()` them below. + self.full_clean(exclude=["country_code", "region_code", "user"]) + + # Update country/region codes as necessary by using the `log()` method. + (userip, _) = self.__class__.objects.get_or_create( + user=self.user, ip_address=self.ip_address + ) + + # Calling the `.log()` method makes a separate "update" call to the database, so we need to + # refresh this local version of the model immediately after. + self.__class__.log(self.user, self.ip_address) + userip.refresh_from_db() + + userip.first_seen = self.first_seen + userip.last_seen = self.last_seen + userip.save() + + self.country_code = userip.country_code + self.region_code = userip.region_code + + # If we've entered this method at all, we can be sure that the `UserIP` 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 (userip.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) + + # Always use British Columbia for fake IP geo data, cause why not. + sanitizer.set_string(json, SanitizableField(model_name, "country_code"), lambda _: "CA") + sanitizer.set_string(json, SanitizableField(model_name, "region_code"), lambda _: "BC") + + +def _perform_log(user: User | RpcUser, ip_address: str): + try: + geo = geo_by_addr(ip_address) + except Exception: + geo = None + + event = UserIpEvent( + user_id=user.id, + ip_address=ip_address, + last_seen=timezone.now(), + ) + + if geo: + event.country_code = geo["country_code"] + event.region_code = geo["region"] + + log_service.record_user_ip(event=event) diff --git a/src/sentry/users/models/userpermission.py b/src/sentry/users/models/userpermission.py new file mode 100644 index 00000000000000..50a4d09a1526df --- /dev/null +++ b/src/sentry/users/models/userpermission.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from django.db import models + +from sentry.backup.dependencies import ImportKind, PrimaryKeyMap, get_model_name +from sentry.backup.helpers import ImportFlags +from sentry.backup.mixins import OverwritableConfigMixin +from sentry.backup.scopes import ImportScope, RelocationScope +from sentry.db.models import FlexibleForeignKey, control_silo_model, sane_repr +from sentry.db.models.outboxes import ControlOutboxProducingModel +from sentry.models.outbox import ControlOutboxBase, OutboxCategory +from sentry.types.region import find_regions_for_user + + +@control_silo_model +class UserPermission(OverwritableConfigMixin, ControlOutboxProducingModel): + """ + Permissions are applied to administrative users and control explicit scope-like permissions within the API. + + Generally speaking, they should only apply to active superuser sessions. + """ + + __relocation_scope__ = RelocationScope.Config + __relocation_custom_ordinal__ = ["user", "permission"] + + user = FlexibleForeignKey("sentry.User") + # permissions should be in the form of 'service-name.permission-name' + permission = models.CharField(max_length=32) + + class Meta: + app_label = "sentry" + db_table = "sentry_userpermission" + unique_together = (("user", "permission"),) + + __repr__ = sane_repr("user_id", "permission") + + @classmethod + def for_user(cls, user_id: int) -> frozenset[str]: + """ + Return a set of permission for the given user ID. + """ + return frozenset(cls.objects.filter(user=user_id).values_list("permission", flat=True)) + + 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 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 permissions and use the existing user's + # permissions instead. + if pk_map.get_kind(get_model_name(User), old_user_id) == ImportKind.Existing: + return None + + return old_pk diff --git a/src/sentry/users/models/userreport.py b/src/sentry/users/models/userreport.py new file mode 100644 index 00000000000000..14d1b4c844f1da --- /dev/null +++ b/src/sentry/users/models/userreport.py @@ -0,0 +1,41 @@ +from django.db import models +from django.utils import timezone + +from sentry.backup.scopes import RelocationScope +from sentry.db.models import BoundedBigIntegerField, Model, region_silo_model, sane_repr + + +@region_silo_model +class UserReport(Model): + __relocation_scope__ = RelocationScope.Excluded + + project_id = BoundedBigIntegerField(db_index=True) + group_id = BoundedBigIntegerField(null=True, db_index=True) + event_id = models.CharField(max_length=32) + environment_id = BoundedBigIntegerField(null=True, db_index=True) + name = models.CharField(max_length=128) + email = models.EmailField(max_length=75) + comments = models.TextField() + date_added = models.DateTimeField(default=timezone.now, db_index=True) + + class Meta: + app_label = "sentry" + db_table = "sentry_userreport" + indexes = ( + models.Index(fields=("project_id", "event_id")), + models.Index(fields=("project_id", "date_added")), + ) + unique_together = (("project_id", "event_id"),) + + __repr__ = sane_repr("event_id", "name", "email") + + def notify(self): + from django.contrib.auth.models import AnonymousUser + + from sentry.api.serializers import UserReportWithGroupSerializer, serialize + from sentry.tasks.user_report import user_report + + user_report.delay( + project_id=self.project_id, + report=serialize(self, AnonymousUser(), UserReportWithGroupSerializer()), + ) diff --git a/src/sentry/users/models/userrole.py b/src/sentry/users/models/userrole.py new file mode 100644 index 00000000000000..79c7ddd1702067 --- /dev/null +++ b/src/sentry/users/models/userrole.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from collections.abc import Sequence + +from django.conf import settings +from django.db import models +from django.utils import timezone + +from sentry.backup.mixins import OverwritableConfigMixin +from sentry.backup.scopes import RelocationScope +from sentry.db.models import ArrayField, control_silo_model, sane_repr +from sentry.db.models.fields.foreignkey import FlexibleForeignKey +from sentry.db.models.outboxes import ControlOutboxProducingModel +from sentry.models.outbox import ControlOutboxBase, OutboxCategory +from sentry.signals import post_upgrade +from sentry.silo.base import SiloMode +from sentry.types.region import find_all_region_names + +MAX_USER_ROLE_NAME_LENGTH = 32 + + +@control_silo_model +class UserRole(OverwritableConfigMixin, ControlOutboxProducingModel): + """ + Roles are applied to administrative users and apply a set of `UserPermission`. + """ + + __relocation_scope__ = RelocationScope.Config + __relocation_custom_ordinal__ = ["name"] + + date_updated = models.DateTimeField(default=timezone.now) + date_added = models.DateTimeField(default=timezone.now, null=True) + + name = models.CharField(max_length=MAX_USER_ROLE_NAME_LENGTH, unique=True) + permissions: models.Field[Sequence[str], list[str]] = ArrayField() + users = models.ManyToManyField("sentry.User", through="sentry.UserRoleUser") + + class Meta: + app_label = "sentry" + db_table = "sentry_userrole" + + __repr__ = sane_repr("name", "permissions") + + def outboxes_for_update(self, shard_identifier: int | None = None) -> list[ControlOutboxBase]: + regions = list(find_all_region_names()) + return [ + outbox + for user_id in self.users.values_list("id", flat=True) + for outbox in OutboxCategory.USER_UPDATE.as_control_outboxes( + region_names=regions, + shard_identifier=user_id, + object_identifier=user_id, + ) + ] + + +@control_silo_model +class UserRoleUser(ControlOutboxProducingModel): + __relocation_scope__ = RelocationScope.Config + + date_updated = models.DateTimeField(default=timezone.now) + date_added = models.DateTimeField(default=timezone.now, null=True) + + user = FlexibleForeignKey("sentry.User") + role = FlexibleForeignKey("sentry.UserRole") + + def outboxes_for_update(self, shard_identifier: int | None = None) -> list[ControlOutboxBase]: + regions = list(find_all_region_names()) + return OutboxCategory.USER_UPDATE.as_control_outboxes( + region_names=regions, + shard_identifier=self.user_id, + object_identifier=self.user_id, + ) + + class Meta: + app_label = "sentry" + db_table = "sentry_userrole_users" + + __repr__ = sane_repr("user", "role") + + +# this must be idempotent because it executes on every upgrade +def manage_default_super_admin_role(**kwargs): + role, _ = UserRole.objects.get_or_create( + name="Super Admin", defaults={"permissions": settings.SENTRY_USER_PERMISSIONS} + ) + if role.permissions != settings.SENTRY_USER_PERMISSIONS: + role.permissions = settings.SENTRY_USER_PERMISSIONS + role.save(update_fields=["permissions"]) + + +post_upgrade.connect( + manage_default_super_admin_role, + dispatch_uid="manage_default_super_admin_role", + weak=False, + sender=SiloMode.MONOLITH, +) diff --git a/src/sentry/users/services/user/impl.py b/src/sentry/users/services/user/impl.py index af4c989e627114..c8660c9feba717 100644 --- a/src/sentry/users/services/user/impl.py +++ b/src/sentry/users/services/user/impl.py @@ -21,13 +21,13 @@ from sentry.hybridcloud.services.organization_mapping.model import RpcOrganizationMapping from sentry.hybridcloud.services.organization_mapping.serial import serialize_organization_mapping from sentry.models.authidentity import AuthIdentity -from sentry.models.avatars import UserAvatar +from sentry.models.avatars.user_avatar import UserAvatar from sentry.models.organization import OrganizationStatus from sentry.models.organizationmapping import OrganizationMapping from sentry.models.organizationmembermapping import OrganizationMemberMapping -from sentry.models.user import User -from sentry.models.useremail import UserEmail from sentry.signals import user_signup +from sentry.users.models.user import User +from sentry.users.models.useremail import UserEmail from sentry.users.services.user import ( RpcAvatar, RpcUser, diff --git a/src/sentry/users/services/user/serial.py b/src/sentry/users/services/user/serial.py index 30987ad398cdc1..4599b4a1957efb 100644 --- a/src/sentry/users/services/user/serial.py +++ b/src/sentry/users/services/user/serial.py @@ -7,7 +7,7 @@ from sentry.db.models.manager.base_query_set import BaseQuerySet from sentry.models.avatars.user_avatar import UserAvatar -from sentry.models.user import User +from sentry.users.models.user import User from sentry.users.services.user import ( RpcAuthenticator, RpcAvatar, diff --git a/src/sentry/users/services/user_option/impl.py b/src/sentry/users/services/user_option/impl.py index d2c88b1762186a..c4cf48d724ac93 100644 --- a/src/sentry/users/services/user_option/impl.py +++ b/src/sentry/users/services/user_option/impl.py @@ -8,7 +8,7 @@ from sentry.api.serializers.base import Serializer from sentry.auth.services.auth import AuthenticationContext from sentry.hybridcloud.rpc.filter_query import FilterQueryDatabaseImpl, OpaqueSerializedResponse -from sentry.models.options.user_option import UserOption +from sentry.users.models.user_option import UserOption from sentry.users.services.user import RpcUser from sentry.users.services.user_option import RpcUserOption, UserOptionFilterArgs, UserOptionService diff --git a/src/sentry/utils/audit.py b/src/sentry/utils/audit.py index 4288ebf83dc21b..b8bcc1203f7b81 100644 --- a/src/sentry/utils/audit.py +++ b/src/sentry/utils/audit.py @@ -17,10 +17,10 @@ from sentry.models.orgauthtoken import OrgAuthToken from sentry.models.project import Project from sentry.models.team import Team -from sentry.models.user import User from sentry.organizations.services.organization import RpcOrganization, organization_service from sentry.organizations.services.organization.model import RpcAuditLogEntryActor from sentry.silo.base import region_silo_function +from sentry.users.models.user import User from sentry.users.services.user import RpcUser diff --git a/src/sentry/utils/auth.py b/src/sentry/utils/auth.py index fd2bab81a0a0bb..42dd8666f22955 100644 --- a/src/sentry/utils/auth.py +++ b/src/sentry/utils/auth.py @@ -18,8 +18,8 @@ from sentry import options from sentry.models.organization import Organization from sentry.models.outbox import outbox_context -from sentry.models.user import User from sentry.organizations.services.organization import RpcOrganization +from sentry.users.models.user import User from sentry.users.services.user import RpcUser from sentry.users.services.user.service import user_service from sentry.utils import metrics diff --git a/src/sentry/utils/mockdata/core.py b/src/sentry/utils/mockdata/core.py index f8e23f15f01976..5ff6908be46539 100644 --- a/src/sentry/utils/mockdata/core.py +++ b/src/sentry/utils/mockdata/core.py @@ -52,8 +52,6 @@ from sentry.models.releaseprojectenvironment import ReleaseProjectEnvironment from sentry.models.repository import Repository from sentry.models.team import Team -from sentry.models.user import User -from sentry.models.userreport import UserReport from sentry.monitors.models import ( CheckInStatus, Monitor, @@ -67,6 +65,8 @@ from sentry.similarity import features from sentry.tsdb.base import TSDBModel from sentry.types.activity import ActivityType +from sentry.users.models.user import User +from sentry.users.models.userreport import UserReport from sentry.utils import loremipsum from sentry.utils.hashlib import md5_text from sentry.utils.samples import create_sample_event as _create_sample_event diff --git a/src/sentry/web/client_config.py b/src/sentry/web/client_config.py index c81951631ed94b..f1dd1a8673bc43 100644 --- a/src/sentry/web/client_config.py +++ b/src/sentry/web/client_config.py @@ -21,7 +21,6 @@ from sentry.auth.services.auth import AuthenticatedToken, AuthenticationContext from sentry.auth.superuser import is_active_superuser from sentry.models.organizationmapping import OrganizationMapping -from sentry.models.user import User from sentry.organizations.services.organization import ( RpcOrganization, RpcUserOrganizationContext, @@ -35,6 +34,7 @@ find_all_multitenant_region_names, get_region_by_name, ) +from sentry.users.models.user import User from sentry.users.services.user import UserSerializeType from sentry.users.services.user.serial import serialize_generic_user from sentry.users.services.user.service import user_service diff --git a/src/sentry/web/forms/accounts.py b/src/sentry/web/forms/accounts.py index b9945f754f69fa..e278909efa4649 100644 --- a/src/sentry/web/forms/accounts.py +++ b/src/sentry/web/forms/accounts.py @@ -15,7 +15,7 @@ from sentry import newsletter, options from sentry import ratelimits as ratelimiter from sentry.auth import password_validation -from sentry.models.user import User +from sentry.users.models.user import User from sentry.utils.auth import find_users, logger from sentry.utils.dates import AVAILABLE_TIMEZONES from sentry.web.forms.fields import AllowedEmailField, CustomTypedChoiceField diff --git a/src/sentry/web/forms/fields.py b/src/sentry/web/forms/fields.py index ca1e5de63555f9..c5b198e41d3e9d 100644 --- a/src/sentry/web/forms/fields.py +++ b/src/sentry/web/forms/fields.py @@ -5,7 +5,7 @@ from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ -from sentry.models.user import User +from sentry.users.models.user import User from sentry.utils.email.address import is_valid_email_address diff --git a/src/sentry/web/frontend/accounts.py b/src/sentry/web/frontend/accounts.py index 6ed24d772d1a89..49391a4344a7e6 100644 --- a/src/sentry/web/frontend/accounts.py +++ b/src/sentry/web/frontend/accounts.py @@ -13,12 +13,12 @@ from sentry.models.lostpasswordhash import LostPasswordHash from sentry.models.organizationmapping import OrganizationMapping from sentry.models.organizationmembermapping import OrganizationMemberMapping -from sentry.models.user import User -from sentry.models.useremail import UserEmail from sentry.organizations.services.organization import organization_service from sentry.security.utils import capture_security_activity from sentry.signals import email_verified, terms_accepted from sentry.silo.base import control_silo_function +from sentry.users.models.user import User +from sentry.users.models.useremail import UserEmail from sentry.users.services.lost_password_hash import lost_password_hash_service from sentry.users.services.user.service import user_service from sentry.utils import auth diff --git a/src/sentry/web/frontend/auth_login.py b/src/sentry/web/frontend/auth_login.py index a8208afe140c13..5f02049d4a46d4 100644 --- a/src/sentry/web/frontend/auth_login.py +++ b/src/sentry/web/frontend/auth_login.py @@ -25,10 +25,10 @@ from sentry.models.authprovider import AuthProvider from sentry.models.organization import OrganizationStatus from sentry.models.organizationmapping import OrganizationMapping -from sentry.models.user import User from sentry.organizations.services.organization import RpcOrganization, organization_service from sentry.signals import join_request_link_viewed, user_signup from sentry.types.ratelimit import RateLimit, RateLimitCategory +from sentry.users.models.user import User from sentry.utils import auth, json, metrics from sentry.utils.auth import ( construct_link_with_query, diff --git a/src/sentry/web/frontend/debug/debug_auth_views.py b/src/sentry/web/frontend/debug/debug_auth_views.py index 99eb396a308e90..5de8010b1afc7f 100644 --- a/src/sentry/web/frontend/debug/debug_auth_views.py +++ b/src/sentry/web/frontend/debug/debug_auth_views.py @@ -1,7 +1,7 @@ from django.http import HttpRequest, HttpResponse from django.views.generic import View -from sentry.models.user import User +from sentry.users.models.user import User from sentry.web.helpers import render_to_response diff --git a/src/sentry/web/frontend/debug/debug_codeowners_auto_sync_failure_email.py b/src/sentry/web/frontend/debug/debug_codeowners_auto_sync_failure_email.py index 208adad00a29ea..d7ea9c9000b6f4 100644 --- a/src/sentry/web/frontend/debug/debug_codeowners_auto_sync_failure_email.py +++ b/src/sentry/web/frontend/debug/debug_codeowners_auto_sync_failure_email.py @@ -4,8 +4,8 @@ from sentry.models.organization import Organization from sentry.models.organizationmember import OrganizationMember from sentry.models.project import Project -from sentry.models.user import User from sentry.notifications.notifications.codeowners_auto_sync import AutoSyncNotification +from sentry.users.models.user import User from .mail import render_preview_email_for_notification diff --git a/src/sentry/web/frontend/debug/debug_incident_activity_email.py b/src/sentry/web/frontend/debug/debug_incident_activity_email.py index 1fbd288625ae6d..4d8509fcb5f95a 100644 --- a/src/sentry/web/frontend/debug/debug_incident_activity_email.py +++ b/src/sentry/web/frontend/debug/debug_incident_activity_email.py @@ -4,7 +4,7 @@ from sentry.incidents.models.incident import Incident, IncidentActivity, IncidentActivityType from sentry.incidents.tasks import generate_incident_activity_email from sentry.models.organization import Organization -from sentry.models.user import User +from sentry.users.models.user import User from .mail import MailPreview diff --git a/src/sentry/web/frontend/debug/debug_incident_trigger_email.py b/src/sentry/web/frontend/debug/debug_incident_trigger_email.py index 4d2e3d0c5cdab6..9f2a5587584ba9 100644 --- a/src/sentry/web/frontend/debug/debug_incident_trigger_email.py +++ b/src/sentry/web/frontend/debug/debug_incident_trigger_email.py @@ -8,8 +8,8 @@ from sentry.incidents.models.incident import Incident, IncidentStatus, TriggerStatus from sentry.models.organization import Organization from sentry.models.project import Project -from sentry.models.user import User from sentry.snuba.models import SnubaQuery +from sentry.users.models.user import User from .mail import MailPreviewView diff --git a/src/sentry/web/frontend/debug/debug_incident_trigger_email_activated_alert.py b/src/sentry/web/frontend/debug/debug_incident_trigger_email_activated_alert.py index 293a16ac85e100..4a26ecae911a97 100644 --- a/src/sentry/web/frontend/debug/debug_incident_trigger_email_activated_alert.py +++ b/src/sentry/web/frontend/debug/debug_incident_trigger_email_activated_alert.py @@ -10,8 +10,8 @@ from sentry.incidents.utils.types import AlertRuleActivationConditionType from sentry.models.organization import Organization from sentry.models.project import Project -from sentry.models.user import User from sentry.snuba.models import SnubaQuery +from sentry.users.models.user import User from .mail import MailPreviewView diff --git a/src/sentry/web/frontend/debug/debug_mfa_added_email.py b/src/sentry/web/frontend/debug/debug_mfa_added_email.py index 64687b7cc7297c..449e359350f8ac 100644 --- a/src/sentry/web/frontend/debug/debug_mfa_added_email.py +++ b/src/sentry/web/frontend/debug/debug_mfa_added_email.py @@ -3,8 +3,8 @@ from django.http import HttpRequest, HttpResponse from django.views.generic import View -from sentry.models.authenticator import Authenticator from sentry.security.emails import generate_security_email +from sentry.users.models.authenticator import Authenticator from .mail import MailPreview diff --git a/src/sentry/web/frontend/debug/debug_mfa_removed_email.py b/src/sentry/web/frontend/debug/debug_mfa_removed_email.py index ffce0a4dec8580..0f868207f8e671 100644 --- a/src/sentry/web/frontend/debug/debug_mfa_removed_email.py +++ b/src/sentry/web/frontend/debug/debug_mfa_removed_email.py @@ -3,8 +3,8 @@ from django.http import HttpRequest, HttpResponse from django.views.generic import View -from sentry.models.authenticator import Authenticator from sentry.security.emails import generate_security_email +from sentry.users.models.authenticator import Authenticator from .mail import MailPreview diff --git a/src/sentry/web/frontend/debug/debug_new_release_email.py b/src/sentry/web/frontend/debug/debug_new_release_email.py index 2b16eb5520a50c..43d53c7563b3ae 100644 --- a/src/sentry/web/frontend/debug/debug_new_release_email.py +++ b/src/sentry/web/frontend/debug/debug_new_release_email.py @@ -11,8 +11,8 @@ from sentry.models.organization import Organization from sentry.models.project import Project from sentry.models.release import Release -from sentry.models.user import User from sentry.notifications.types import GroupSubscriptionReason +from sentry.users.models.user import User from sentry.utils.http import absolute_uri from .mail import MailPreview diff --git a/src/sentry/web/frontend/debug/debug_onboarding_continuation_email.py b/src/sentry/web/frontend/debug/debug_onboarding_continuation_email.py index 9a179a36df4fdd..f837ba16ccd1e2 100644 --- a/src/sentry/web/frontend/debug/debug_onboarding_continuation_email.py +++ b/src/sentry/web/frontend/debug/debug_onboarding_continuation_email.py @@ -3,7 +3,7 @@ from sentry.api.endpoints.organization_onboarding_continuation_email import get_request_builder_args from sentry.models.organization import Organization -from sentry.models.user import User +from sentry.users.models.user import User from sentry.web.frontend.debug.mail import MailPreviewAdapter from sentry.web.helpers import render_to_response diff --git a/src/sentry/web/frontend/debug/debug_organization_integration_request.py b/src/sentry/web/frontend/debug/debug_organization_integration_request.py index 23ea16189e5839..285617e6add453 100644 --- a/src/sentry/web/frontend/debug/debug_organization_integration_request.py +++ b/src/sentry/web/frontend/debug/debug_organization_integration_request.py @@ -3,10 +3,10 @@ from sentry.models.organization import Organization from sentry.models.organizationmember import OrganizationMember -from sentry.models.user import User from sentry.notifications.notifications.organization_request.integration_request import ( IntegrationRequestNotification, ) +from sentry.users.models.user import User from .mail import render_preview_email_for_notification diff --git a/src/sentry/web/frontend/debug/debug_organization_invite_request.py b/src/sentry/web/frontend/debug/debug_organization_invite_request.py index 8a8ab3dcec9821..cb0de2d4a69313 100644 --- a/src/sentry/web/frontend/debug/debug_organization_invite_request.py +++ b/src/sentry/web/frontend/debug/debug_organization_invite_request.py @@ -3,8 +3,8 @@ from sentry.models.organization import Organization from sentry.models.organizationmember import OrganizationMember -from sentry.models.user import User from sentry.notifications.notifications.organization_request import InviteRequestNotification +from sentry.users.models.user import User from .mail import render_preview_email_for_notification diff --git a/src/sentry/web/frontend/debug/debug_organization_join_request.py b/src/sentry/web/frontend/debug/debug_organization_join_request.py index 32cfcaf0bb3e92..86645ec614cb24 100644 --- a/src/sentry/web/frontend/debug/debug_organization_join_request.py +++ b/src/sentry/web/frontend/debug/debug_organization_join_request.py @@ -3,8 +3,8 @@ from sentry.models.organization import Organization from sentry.models.organizationmember import InviteStatus, OrganizationMember -from sentry.models.user import User from sentry.notifications.notifications.organization_request import JoinRequestNotification +from sentry.users.models.user import User from .mail import render_preview_email_for_notification diff --git a/src/sentry/web/frontend/debug/debug_recovery_codes_regenerated_email.py b/src/sentry/web/frontend/debug/debug_recovery_codes_regenerated_email.py index c4ab7a9d6f01d5..4c21edbb188556 100644 --- a/src/sentry/web/frontend/debug/debug_recovery_codes_regenerated_email.py +++ b/src/sentry/web/frontend/debug/debug_recovery_codes_regenerated_email.py @@ -3,8 +3,8 @@ from django.http import HttpResponse from django.views.generic import View -from sentry.models.authenticator import Authenticator from sentry.security.emails import generate_security_email +from sentry.users.models.authenticator import Authenticator from sentry.utils.auth import AuthenticatedHttpRequest from .mail import MailPreview diff --git a/src/sentry/web/frontend/error_page_embed.py b/src/sentry/web/frontend/error_page_embed.py index fdfc803460ce42..c1949a6149bdb6 100644 --- a/src/sentry/web/frontend/error_page_embed.py +++ b/src/sentry/web/frontend/error_page_embed.py @@ -16,9 +16,9 @@ from sentry.models.options.project_option import ProjectOption from sentry.models.project import Project from sentry.models.projectkey import ProjectKey -from sentry.models.userreport import UserReport from sentry.signals import user_feedback_received from sentry.types.region import get_local_region +from sentry.users.models.userreport import UserReport from sentry.utils import json from sentry.utils.db import atomic_transaction from sentry.utils.http import is_valid_origin, origin_from_request diff --git a/src/sentry/web/frontend/openidtoken.py b/src/sentry/web/frontend/openidtoken.py index 2eddfd890cb011..71981e41873658 100644 --- a/src/sentry/web/frontend/openidtoken.py +++ b/src/sentry/web/frontend/openidtoken.py @@ -3,7 +3,7 @@ from django.utils import timezone from sentry.models.apigrant import ApiGrant -from sentry.models.useremail import UserEmail +from sentry.users.models.useremail import UserEmail from sentry.utils import jwt as jwt_utils DEFAULT_EXPIRATION = timedelta(minutes=10) diff --git a/src/sentry/web/frontend/setup_wizard.py b/src/sentry/web/frontend/setup_wizard.py index 8ee72191dd5a80..4c54deefa6a6fa 100644 --- a/src/sentry/web/frontend/setup_wizard.py +++ b/src/sentry/web/frontend/setup_wizard.py @@ -19,11 +19,11 @@ from sentry.models.organizationmapping import OrganizationMapping from sentry.models.organizationmembermapping import OrganizationMemberMapping from sentry.models.orgauthtoken import OrgAuthToken -from sentry.models.user import User from sentry.projects.services.project.service import project_service from sentry.projects.services.project_key.model import ProjectKeyRole from sentry.projects.services.project_key.service import project_key_service from sentry.types.token import AuthTokenType +from sentry.users.models.user import User from sentry.users.services.user.model import RpcUser from sentry.utils.http import absolute_uri from sentry.utils.security.orgauthtoken_token import ( diff --git a/src/sentry/web/frontend/sudo.py b/src/sentry/web/frontend/sudo.py index ae0f38839551f4..962b262bfb0441 100644 --- a/src/sentry/web/frontend/sudo.py +++ b/src/sentry/web/frontend/sudo.py @@ -5,7 +5,7 @@ from django.http.request import HttpRequest from sentry.auth.authenticators.u2f import U2fInterface -from sentry.models.authenticator import Authenticator +from sentry.users.models.authenticator import Authenticator from sentry.utils import json from sentry.web.frontend.base import control_silo_view from sudo.views import SudoView as BaseSudoView diff --git a/src/sentry/web/frontend/twofactor.py b/src/sentry/web/frontend/twofactor.py index 61f8c56e258e04..0fd48268b81c50 100644 --- a/src/sentry/web/frontend/twofactor.py +++ b/src/sentry/web/frontend/twofactor.py @@ -12,8 +12,8 @@ from sentry import ratelimits as ratelimiter from sentry.auth.authenticators.sms import SMSRateLimitExceeded from sentry.auth.authenticators.u2f import U2fInterface -from sentry.models.authenticator import Authenticator from sentry.silo.base import control_silo_function +from sentry.users.models.authenticator import Authenticator from sentry.utils import auth, json from sentry.utils.email import MessageBuilder from sentry.utils.geo import geo_by_addr diff --git a/tests/relay_integration/test_sdk.py b/tests/relay_integration/test_sdk.py index 597536bd44808c..d9853f27447b21 100644 --- a/tests/relay_integration/test_sdk.py +++ b/tests/relay_integration/test_sdk.py @@ -8,7 +8,6 @@ from sentry import eventstore from sentry.eventstore.models import Event -from sentry.models.userrole import manage_default_super_admin_role from sentry.receivers import create_default_projects from sentry.silo.base import SiloMode from sentry.testutils.asserts import assert_mock_called_once_with_partial @@ -16,6 +15,7 @@ from sentry.testutils.pytest.relay import adjust_settings_for_relay_tests from sentry.testutils.silo import assume_test_silo_mode, no_silo_test from sentry.testutils.skips import requires_kafka +from sentry.users.models.userrole import manage_default_super_admin_role from sentry.utils.sdk import bind_organization_context, configure_sdk pytestmark = [requires_kafka] diff --git a/tests/sentry/api/endpoints/relocations/test_retry.py b/tests/sentry/api/endpoints/relocations/test_retry.py index 1dc182f32e8bc2..0bb82639ae002a 100644 --- a/tests/sentry/api/endpoints/relocations/test_retry.py +++ b/tests/sentry/api/endpoints/relocations/test_retry.py @@ -19,13 +19,13 @@ from sentry.backup.crypto import LocalFileEncryptor, create_encrypted_export_tarball from sentry.models.files.file import File from sentry.models.relocation import Relocation, RelocationFile -from sentry.models.user import User from sentry.silo.base import SiloMode from sentry.testutils.cases import APITestCase from sentry.testutils.factories import get_fixture_path from sentry.testutils.helpers.backups import generate_rsa_key_pair from sentry.testutils.helpers.options import override_options from sentry.testutils.silo import assume_test_silo_mode +from sentry.users.models.user import User from sentry.utils.relocation import RELOCATION_FILE_TYPE, OrderedTask FRESH_INSTALL_PATH = get_fixture_path("backup", "fresh-install.json") diff --git a/tests/sentry/api/endpoints/test_auth_index.py b/tests/sentry/api/endpoints/test_auth_index.py index 1c387885ce6df8..4d657c26128904 100644 --- a/tests/sentry/api/endpoints/test_auth_index.py +++ b/tests/sentry/api/endpoints/test_auth_index.py @@ -6,11 +6,11 @@ from sentry.api.validators.auth import MISSING_PASSWORD_OR_U2F_CODE from sentry.auth.superuser import COOKIE_NAME -from sentry.models.authenticator import Authenticator from sentry.models.authidentity import AuthIdentity from sentry.models.authprovider import AuthProvider from sentry.testutils.cases import APITestCase, AuthProviderTestCase from sentry.testutils.silo import control_silo_test +from sentry.users.models.authenticator import Authenticator from sentry.utils.auth import SSO_EXPIRY_TIME, SsoSession diff --git a/tests/sentry/api/endpoints/test_group_user_reports.py b/tests/sentry/api/endpoints/test_group_user_reports.py index 325d070814787f..ca5f7646c151bc 100644 --- a/tests/sentry/api/endpoints/test_group_user_reports.py +++ b/tests/sentry/api/endpoints/test_group_user_reports.py @@ -1,9 +1,9 @@ from functools import cached_property from sentry.models.environment import Environment -from sentry.models.userreport import UserReport from sentry.testutils.cases import APITestCase, SnubaTestCase from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.users.models.userreport import UserReport class GroupUserReport(APITestCase, SnubaTestCase): diff --git a/tests/sentry/api/endpoints/test_organization_details.py b/tests/sentry/api/endpoints/test_organization_details.py index ba1e51ac3c17b9..c912b80d293041 100644 --- a/tests/sentry/api/endpoints/test_organization_details.py +++ b/tests/sentry/api/endpoints/test_organization_details.py @@ -23,7 +23,6 @@ from sentry.auth.authenticators.totp import TotpInterface from sentry.constants import RESERVED_ORGANIZATION_SLUGS, ObjectStatus from sentry.models.auditlogentry import AuditLogEntry -from sentry.models.authenticator import Authenticator from sentry.models.authprovider import AuthProvider from sentry.models.avatars.organization_avatar import OrganizationAvatar from sentry.models.deletedorganization import DeletedOrganization @@ -33,7 +32,6 @@ from sentry.models.organizationmapping import OrganizationMapping from sentry.models.organizationslugreservation import OrganizationSlugReservation from sentry.models.scheduledeletion import RegionScheduledDeletion -from sentry.models.user import User from sentry.signals import project_created from sentry.silo.safety import unguarded_write from sentry.testutils.cases import APITestCase, TwoFactorAPITestCase @@ -41,6 +39,8 @@ from sentry.testutils.outbox import outbox_runner from sentry.testutils.silo import assume_test_silo_mode_of, create_test_regions, region_silo_test from sentry.testutils.skips import requires_snuba +from sentry.users.models.authenticator import Authenticator +from sentry.users.models.user import User pytestmark = [requires_snuba] diff --git a/tests/sentry/api/endpoints/test_organization_index.py b/tests/sentry/api/endpoints/test_organization_index.py index 7603e1f56f9deb..5808c929c511f6 100644 --- a/tests/sentry/api/endpoints/test_organization_index.py +++ b/tests/sentry/api/endpoints/test_organization_index.py @@ -7,7 +7,6 @@ from django.test import override_settings from sentry.auth.authenticators.totp import TotpInterface -from sentry.models.authenticator import Authenticator from sentry.models.options.organization_option import OrganizationOption from sentry.models.organization import Organization, OrganizationStatus from sentry.models.organizationmapping import OrganizationMapping @@ -19,6 +18,7 @@ from sentry.testutils.cases import APITestCase, TwoFactorAPITestCase from sentry.testutils.hybrid_cloud import HybridCloudTestMixin from sentry.testutils.silo import assume_test_silo_mode, create_test_regions, region_silo_test +from sentry.users.models.authenticator import Authenticator class OrganizationIndexTest(APITestCase): diff --git a/tests/sentry/api/endpoints/test_organization_member_details.py b/tests/sentry/api/endpoints/test_organization_member_details.py index 757447c6ed8ead..01f4e500603104 100644 --- a/tests/sentry/api/endpoints/test_organization_member_details.py +++ b/tests/sentry/api/endpoints/test_organization_member_details.py @@ -7,9 +7,7 @@ from sentry.auth.authenticators.recovery_code import RecoveryCodeInterface from sentry.auth.authenticators.totp import TotpInterface -from sentry.models.authenticator import Authenticator from sentry.models.authprovider import AuthProvider -from sentry.models.options.user_option import UserOption from sentry.models.organization import Organization from sentry.models.organizationmember import InviteStatus, OrganizationMember from sentry.models.organizationmemberteam import OrganizationMemberTeam @@ -21,6 +19,8 @@ from sentry.testutils.hybrid_cloud import HybridCloudTestMixin from sentry.testutils.outbox import outbox_runner from sentry.testutils.silo import assume_test_silo_mode +from sentry.users.models.authenticator import Authenticator +from sentry.users.models.user_option import UserOption from tests.sentry.api.endpoints.test_organization_member_index import ( mock_organization_roles_get_factory, ) diff --git a/tests/sentry/api/endpoints/test_organization_member_index.py b/tests/sentry/api/endpoints/test_organization_member_index.py index e06b0e5dc26c08..0b31ff54c13fb5 100644 --- a/tests/sentry/api/endpoints/test_organization_member_index.py +++ b/tests/sentry/api/endpoints/test_organization_member_index.py @@ -7,10 +7,8 @@ from sentry.api.endpoints.accept_organization_invite import get_invite_state from sentry.api.endpoints.organization_member.index import OrganizationMemberRequestSerializer from sentry.api.invite_helper import ApiInviteHelper -from sentry.models.authenticator import Authenticator from sentry.models.organizationmember import InviteStatus, OrganizationMember from sentry.models.organizationmemberteam import OrganizationMemberTeam -from sentry.models.useremail import UserEmail from sentry.roles import organization_roles from sentry.silo.base import SiloMode from sentry.testutils.cases import APITestCase, TestCase @@ -18,6 +16,8 @@ from sentry.testutils.hybrid_cloud import HybridCloudTestMixin from sentry.testutils.outbox import outbox_runner from sentry.testutils.silo import assume_test_silo_mode +from sentry.users.models.authenticator import Authenticator +from sentry.users.models.useremail import UserEmail def mock_organization_roles_get_factory(original_organization_roles_get): diff --git a/tests/sentry/api/endpoints/test_organization_user_reports.py b/tests/sentry/api/endpoints/test_organization_user_reports.py index acbcee1ef4a143..bf18e37ca7f21d 100644 --- a/tests/sentry/api/endpoints/test_organization_user_reports.py +++ b/tests/sentry/api/endpoints/test_organization_user_reports.py @@ -3,8 +3,8 @@ from sentry.feedback.usecases.create_feedback import FeedbackCreationSource from sentry.ingest.userreport import save_userreport from sentry.models.group import GroupStatus -from sentry.models.userreport import UserReport from sentry.testutils.cases import APITestCase, SnubaTestCase +from sentry.users.models.userreport import UserReport class OrganizationUserReportListTest(APITestCase, SnubaTestCase): diff --git a/tests/sentry/api/endpoints/test_project_rules.py b/tests/sentry/api/endpoints/test_project_rules.py index df066ca934294d..4318df8e31cd51 100644 --- a/tests/sentry/api/endpoints/test_project_rules.py +++ b/tests/sentry/api/endpoints/test_project_rules.py @@ -17,13 +17,13 @@ from sentry.integrations.slack.utils.channel import SlackChannelIdData from sentry.models.environment import Environment from sentry.models.rule import Rule, RuleActivity, RuleActivityType -from sentry.models.user import User from sentry.silo.base import SiloMode from sentry.tasks.integrations.slack.find_channel_id_for_rule import find_channel_id_for_rule from sentry.testutils.cases import APITestCase from sentry.testutils.helpers import install_slack, with_feature from sentry.testutils.silo import assume_test_silo_mode from sentry.types.actor import Actor +from sentry.users.models.user import User class ProjectRuleBaseTestCase(APITestCase): diff --git a/tests/sentry/api/endpoints/test_project_user_reports.py b/tests/sentry/api/endpoints/test_project_user_reports.py index a369630a052831..4b514a8c15467a 100644 --- a/tests/sentry/api/endpoints/test_project_user_reports.py +++ b/tests/sentry/api/endpoints/test_project_user_reports.py @@ -5,9 +5,9 @@ from django.utils import timezone from sentry.models.group import GroupStatus -from sentry.models.userreport import UserReport from sentry.testutils.cases import APITestCase, SnubaTestCase from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.users.models.userreport import UserReport class ProjectUserReportListTest(APITestCase, SnubaTestCase): diff --git a/tests/sentry/api/endpoints/test_user_authenticator_details.py b/tests/sentry/api/endpoints/test_user_authenticator_details.py index 7c4da86afc868c..67084ce788b4f1 100644 --- a/tests/sentry/api/endpoints/test_user_authenticator_details.py +++ b/tests/sentry/api/endpoints/test_user_authenticator_details.py @@ -11,12 +11,12 @@ from sentry.auth.authenticators.sms import SmsInterface from sentry.auth.authenticators.totp import TotpInterface from sentry.auth.authenticators.u2f import create_credential_object -from sentry.models.authenticator import Authenticator from sentry.models.organization import Organization -from sentry.models.user import User from sentry.testutils.cases import APITestCase from sentry.testutils.helpers.options import override_options from sentry.testutils.silo import control_silo_test +from sentry.users.models.authenticator import Authenticator +from sentry.users.models.user import User def get_auth(user: User) -> Authenticator: diff --git a/tests/sentry/api/endpoints/test_user_authenticator_enroll.py b/tests/sentry/api/endpoints/test_user_authenticator_enroll.py index 48cab58d8dc88f..b867f89b282b12 100644 --- a/tests/sentry/api/endpoints/test_user_authenticator_enroll.py +++ b/tests/sentry/api/endpoints/test_user_authenticator_enroll.py @@ -7,10 +7,8 @@ from sentry import audit_log from sentry.models.auditlogentry import AuditLogEntry -from sentry.models.authenticator import Authenticator from sentry.models.organization import Organization from sentry.models.organizationmember import OrganizationMember -from sentry.models.useremail import UserEmail from sentry.organizations.services.organization.serial import serialize_member from sentry.silo.base import SiloMode from sentry.silo.safety import unguarded_write @@ -18,6 +16,8 @@ from sentry.testutils.helpers import override_options from sentry.testutils.outbox import outbox_runner from sentry.testutils.silo import assume_test_silo_mode, control_silo_test +from sentry.users.models.authenticator import Authenticator +from sentry.users.models.useremail import UserEmail from tests.sentry.api.endpoints.test_user_authenticator_details import assert_security_email_sent @@ -479,8 +479,9 @@ def test_org_member_does_not_exist(self, try_enroll, log): # Mutate the OrganizationMember, putting it out of sync with the # pending member cookie. - with assume_test_silo_mode(SiloMode.REGION), unguarded_write( - using=router.db_for_write(OrganizationMember) + with ( + assume_test_silo_mode(SiloMode.REGION), + unguarded_write(using=router.db_for_write(OrganizationMember)), ): om.update(id=om.id + 1) @@ -501,8 +502,9 @@ def test_invalid_token(self, try_enroll, log): # Mutate the OrganizationMember, putting it out of sync with the # pending member cookie. - with assume_test_silo_mode(SiloMode.REGION), unguarded_write( - using=router.db_for_write(OrganizationMember) + with ( + assume_test_silo_mode(SiloMode.REGION), + unguarded_write(using=router.db_for_write(OrganizationMember)), ): om.update(token="123") diff --git a/tests/sentry/api/endpoints/test_user_authenticators_index.py b/tests/sentry/api/endpoints/test_user_authenticators_index.py index d8d29dbbb2670e..cf4cf677bf0a23 100644 --- a/tests/sentry/api/endpoints/test_user_authenticators_index.py +++ b/tests/sentry/api/endpoints/test_user_authenticators_index.py @@ -1,9 +1,9 @@ from django.urls import reverse -from sentry.models.authenticator import Authenticator from sentry.testutils.cases import APITestCase from sentry.testutils.helpers.options import override_options from sentry.testutils.silo import control_silo_test +from sentry.users.models.authenticator import Authenticator @control_silo_test diff --git a/tests/sentry/api/endpoints/test_user_details.py b/tests/sentry/api/endpoints/test_user_details.py index 0e43dd509d697c..c4b8a7a6cdb81d 100644 --- a/tests/sentry/api/endpoints/test_user_details.py +++ b/tests/sentry/api/endpoints/test_user_details.py @@ -2,12 +2,8 @@ from pytest import fixture from sentry.models.deletedorganization import DeletedOrganization -from sentry.models.options.user_option import UserOption from sentry.models.organization import Organization, OrganizationStatus from sentry.models.organizationmember import OrganizationMember -from sentry.models.user import User -from sentry.models.userpermission import UserPermission -from sentry.models.userrole import UserRole from sentry.silo.base import SiloMode from sentry.tasks.deletion.hybrid_cloud import schedule_hybrid_cloud_foreign_key_jobs from sentry.testutils.cases import APITestCase @@ -15,6 +11,10 @@ from sentry.testutils.hybrid_cloud import HybridCloudTestMixin from sentry.testutils.outbox import outbox_runner from sentry.testutils.silo import assume_test_silo_mode, control_silo_test +from sentry.users.models.user import User +from sentry.users.models.user_option import UserOption +from sentry.users.models.userpermission import UserPermission +from sentry.users.models.userrole import UserRole class UserDetailsTest(APITestCase): diff --git a/tests/sentry/api/endpoints/test_user_emails.py b/tests/sentry/api/endpoints/test_user_emails.py index 5d4d4a25d3469a..ecbe59e61b465c 100644 --- a/tests/sentry/api/endpoints/test_user_emails.py +++ b/tests/sentry/api/endpoints/test_user_emails.py @@ -1,10 +1,10 @@ from django.urls import reverse -from sentry.models.options.user_option import UserOption -from sentry.models.user import User -from sentry.models.useremail import UserEmail from sentry.testutils.cases import APITestCase from sentry.testutils.silo import control_silo_test +from sentry.users.models.user import User +from sentry.users.models.user_option import UserOption +from sentry.users.models.useremail import UserEmail @control_silo_test diff --git a/tests/sentry/api/endpoints/test_user_emails_confirm.py b/tests/sentry/api/endpoints/test_user_emails_confirm.py index 8b99d55867f88e..64d04a1b9e8e8c 100644 --- a/tests/sentry/api/endpoints/test_user_emails_confirm.py +++ b/tests/sentry/api/endpoints/test_user_emails_confirm.py @@ -1,8 +1,8 @@ from unittest import mock -from sentry.models.useremail import UserEmail from sentry.testutils.cases import APITestCase from sentry.testutils.silo import control_silo_test +from sentry.users.models.useremail import UserEmail @control_silo_test diff --git a/tests/sentry/api/endpoints/test_user_index.py b/tests/sentry/api/endpoints/test_user_index.py index 298b5d237d6501..10801e4af93461 100644 --- a/tests/sentry/api/endpoints/test_user_index.py +++ b/tests/sentry/api/endpoints/test_user_index.py @@ -1,7 +1,7 @@ -from sentry.models.userpermission import UserPermission from sentry.testutils.cases import APITestCase from sentry.testutils.helpers.options import override_options from sentry.testutils.silo import control_silo_test +from sentry.users.models.userpermission import UserPermission @control_silo_test diff --git a/tests/sentry/api/endpoints/test_user_ips.py b/tests/sentry/api/endpoints/test_user_ips.py index aba9477e910535..22fc67123a50d5 100644 --- a/tests/sentry/api/endpoints/test_user_ips.py +++ b/tests/sentry/api/endpoints/test_user_ips.py @@ -1,8 +1,8 @@ from datetime import datetime, timezone -from sentry.models.userip import UserIP from sentry.testutils.cases import APITestCase from sentry.testutils.silo import control_silo_test +from sentry.users.models.userip import UserIP @control_silo_test diff --git a/tests/sentry/api/endpoints/test_user_notification_email.py b/tests/sentry/api/endpoints/test_user_notification_email.py index 0d421d32be0d09..73b380ba49273c 100644 --- a/tests/sentry/api/endpoints/test_user_notification_email.py +++ b/tests/sentry/api/endpoints/test_user_notification_email.py @@ -1,7 +1,7 @@ -from sentry.models.options.user_option import UserOption -from sentry.models.useremail import UserEmail from sentry.testutils.cases import APITestCase from sentry.testutils.silo import control_silo_test +from sentry.users.models.user_option import UserOption +from sentry.users.models.useremail import UserEmail class UserNotificationEmailTestBase(APITestCase): diff --git a/tests/sentry/api/endpoints/test_user_password.py b/tests/sentry/api/endpoints/test_user_password.py index d86ee2b990e937..d4af19be7e450e 100644 --- a/tests/sentry/api/endpoints/test_user_password.py +++ b/tests/sentry/api/endpoints/test_user_password.py @@ -1,9 +1,9 @@ from django.test import override_settings -from sentry.models.user import User from sentry.testutils.cases import APITestCase from sentry.testutils.helpers.datetime import freeze_time from sentry.testutils.silo import control_silo_test +from sentry.users.models.user import User @control_silo_test diff --git a/tests/sentry/api/endpoints/test_user_permission_details.py b/tests/sentry/api/endpoints/test_user_permission_details.py index 23b1b7deef44d2..b74232a73f425d 100644 --- a/tests/sentry/api/endpoints/test_user_permission_details.py +++ b/tests/sentry/api/endpoints/test_user_permission_details.py @@ -1,10 +1,10 @@ from unittest.mock import patch from sentry.api.permissions import StaffPermission -from sentry.models.userpermission import UserPermission from sentry.testutils.cases import APITestCase from sentry.testutils.helpers.options import override_options from sentry.testutils.silo import control_silo_test +from sentry.users.models.userpermission import UserPermission @control_silo_test diff --git a/tests/sentry/api/endpoints/test_user_role_details.py b/tests/sentry/api/endpoints/test_user_role_details.py index 609620a160e064..cf8522c3e99f9b 100644 --- a/tests/sentry/api/endpoints/test_user_role_details.py +++ b/tests/sentry/api/endpoints/test_user_role_details.py @@ -1,6 +1,6 @@ -from sentry.models.userrole import UserRole from sentry.testutils.cases import APITestCase from sentry.testutils.silo import control_silo_test +from sentry.users.models.userrole import UserRole @control_silo_test diff --git a/tests/sentry/api/endpoints/test_user_roles.py b/tests/sentry/api/endpoints/test_user_roles.py index 59cf049a499912..bd8bdaefedf232 100644 --- a/tests/sentry/api/endpoints/test_user_roles.py +++ b/tests/sentry/api/endpoints/test_user_roles.py @@ -1,6 +1,6 @@ -from sentry.models.userrole import UserRole from sentry.testutils.cases import APITestCase from sentry.testutils.silo import control_silo_test +from sentry.users.models.userrole import UserRole @control_silo_test diff --git a/tests/sentry/api/endpoints/test_user_subscriptions.py b/tests/sentry/api/endpoints/test_user_subscriptions.py index bbe3d57832ca35..51d9131c4bd37c 100644 --- a/tests/sentry/api/endpoints/test_user_subscriptions.py +++ b/tests/sentry/api/endpoints/test_user_subscriptions.py @@ -2,10 +2,10 @@ from django.conf import settings from sentry import newsletter -from sentry.models.useremail import UserEmail from sentry.newsletter.dummy import DummyNewsletter from sentry.testutils.cases import APITestCase from sentry.testutils.silo import control_silo_test +from sentry.users.models.useremail import UserEmail @pytest.mark.skipif( diff --git a/tests/sentry/api/endpoints/test_userroles_details.py b/tests/sentry/api/endpoints/test_userroles_details.py index 508617d36785bc..04910daf442c64 100644 --- a/tests/sentry/api/endpoints/test_userroles_details.py +++ b/tests/sentry/api/endpoints/test_userroles_details.py @@ -1,6 +1,6 @@ -from sentry.models.userrole import UserRole from sentry.testutils.cases import APITestCase from sentry.testutils.silo import control_silo_test +from sentry.users.models.userrole import UserRole @control_silo_test diff --git a/tests/sentry/api/endpoints/test_userroles_index.py b/tests/sentry/api/endpoints/test_userroles_index.py index 093fad26acd87c..59ca51de247658 100644 --- a/tests/sentry/api/endpoints/test_userroles_index.py +++ b/tests/sentry/api/endpoints/test_userroles_index.py @@ -1,6 +1,6 @@ -from sentry.models.userrole import UserRole from sentry.testutils.cases import APITestCase from sentry.testutils.silo import control_silo_test +from sentry.users.models.userrole import UserRole @control_silo_test diff --git a/tests/sentry/api/serializers/test_group.py b/tests/sentry/api/serializers/test_group.py index e7770e0e0cf1cd..7a9d62441bde59 100644 --- a/tests/sentry/api/serializers/test_group.py +++ b/tests/sentry/api/serializers/test_group.py @@ -12,7 +12,6 @@ from sentry.models.groupsubscription import GroupSubscription from sentry.models.notificationsettingoption import NotificationSettingOption from sentry.models.notificationsettingprovider import NotificationSettingProvider -from sentry.models.options.user_option import UserOption from sentry.notifications.types import ( NotificationScopeEnum, NotificationSettingEnum, @@ -22,6 +21,7 @@ from sentry.testutils.cases import PerformanceIssueTestCase, TestCase from sentry.testutils.silo import assume_test_silo_mode from sentry.testutils.skips import requires_snuba +from sentry.users.models.user_option import UserOption pytestmark = [requires_snuba] diff --git a/tests/sentry/api/serializers/test_project.py b/tests/sentry/api/serializers/test_project.py index 82d788e50140f7..7742d678415b55 100644 --- a/tests/sentry/api/serializers/test_project.py +++ b/tests/sentry/api/serializers/test_project.py @@ -26,10 +26,10 @@ from sentry.models.project import Project from sentry.models.release import Release from sentry.models.releaseprojectenvironment import ReleaseProjectEnvironment -from sentry.models.userreport import UserReport from sentry.testutils.cases import SnubaTestCase, TestCase from sentry.testutils.helpers import with_feature from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.users.models.userreport import UserReport from sentry.utils.samples import load_data TEAM_CONTRIBUTOR = settings.SENTRY_TEAM_ROLES[0] diff --git a/tests/sentry/api/serializers/test_release.py b/tests/sentry/api/serializers/test_release.py index 2e61dfe502588f..6311ce0a5649c3 100644 --- a/tests/sentry/api/serializers/test_release.py +++ b/tests/sentry/api/serializers/test_release.py @@ -19,12 +19,12 @@ from sentry.models.releasecommit import ReleaseCommit from sentry.models.releaseprojectenvironment import ReleaseProjectEnvironment, ReleaseStages from sentry.models.releases.release_project import ReleaseProject -from sentry.models.user import User -from sentry.models.useremail import UserEmail from sentry.silo.base import SiloMode from sentry.testutils.cases import SnubaTestCase, TestCase from sentry.testutils.helpers.datetime import before_now, iso_format from sentry.testutils.silo import assume_test_silo_mode +from sentry.users.models.user import User +from sentry.users.models.useremail import UserEmail class ReleaseSerializerTest(TestCase, SnubaTestCase): diff --git a/tests/sentry/api/serializers/test_user.py b/tests/sentry/api/serializers/test_user.py index 10e77bdb457e41..ec10cd9392245b 100644 --- a/tests/sentry/api/serializers/test_user.py +++ b/tests/sentry/api/serializers/test_user.py @@ -1,14 +1,14 @@ from sentry.api.serializers import serialize from sentry.api.serializers.models.user import DetailedSelfUserSerializer, DetailedUserSerializer from sentry.auth.authenticators import available_authenticators -from sentry.models.authenticator import Authenticator from sentry.models.authidentity import AuthIdentity from sentry.models.authprovider import AuthProvider from sentry.models.avatars.user_avatar import UserAvatar -from sentry.models.useremail import UserEmail -from sentry.models.userpermission import UserPermission from sentry.testutils.cases import TestCase from sentry.testutils.silo import control_silo_test +from sentry.users.models.authenticator import Authenticator +from sentry.users.models.useremail import UserEmail +from sentry.users.models.userpermission import UserPermission @control_silo_test diff --git a/tests/sentry/api/test_paginator.py b/tests/sentry/api/test_paginator.py index 07c2108c13268a..40e81968a61643 100644 --- a/tests/sentry/api/test_paginator.py +++ b/tests/sentry/api/test_paginator.py @@ -34,10 +34,10 @@ from sentry.incidents.models.alert_rule import AlertRule from sentry.incidents.models.incident import Incident from sentry.models.rule import Rule -from sentry.models.user import User from sentry.testutils.cases import APITestCase, SnubaTestCase, TestCase from sentry.testutils.helpers.datetime import iso_format from sentry.testutils.silo import control_silo_test +from sentry.users.models.user import User from sentry.utils.cursors import Cursor from sentry.utils.snuba import raw_snql_query diff --git a/tests/sentry/audit_log/services/test_log.py b/tests/sentry/audit_log/services/test_log.py index 64296ea1481d7f..4fd1e1f09326b4 100644 --- a/tests/sentry/audit_log/services/test_log.py +++ b/tests/sentry/audit_log/services/test_log.py @@ -2,11 +2,11 @@ from sentry.db.postgres.transactions import in_test_hide_transaction_boundary from sentry.models.auditlogentry import AuditLogEntry from sentry.models.outbox import OutboxScope, RegionOutbox -from sentry.models.userip import UserIP from sentry.silo.base import SiloMode from sentry.testutils.factories import Factories from sentry.testutils.pytest.fixtures import django_db_all from sentry.testutils.silo import all_silo_test, assume_test_silo_mode +from sentry.users.models.userip import UserIP @django_db_all diff --git a/tests/sentry/auth/test_access.py b/tests/sentry/auth/test_access.py index 77dfc15ab3d633..65078da07faeec 100644 --- a/tests/sentry/auth/test_access.py +++ b/tests/sentry/auth/test_access.py @@ -16,14 +16,14 @@ from sentry.models.authprovider import AuthProvider from sentry.models.organization import Organization from sentry.models.team import TeamStatus -from sentry.models.user import User -from sentry.models.userrole import UserRole from sentry.organizations.services.organization import organization_service from sentry.silo.base import SiloMode from sentry.testutils.cases import TestCase from sentry.testutils.helpers import with_feature from sentry.testutils.helpers.options import override_options from sentry.testutils.silo import all_silo_test, assume_test_silo_mode, no_silo_test +from sentry.users.models.user import User +from sentry.users.models.userrole import UserRole def silo_from_user( diff --git a/tests/sentry/auth/test_password_validation.py b/tests/sentry/auth/test_password_validation.py index 284c5b11b2fb6c..42a24a1ade3a38 100644 --- a/tests/sentry/auth/test_password_validation.py +++ b/tests/sentry/auth/test_password_validation.py @@ -7,8 +7,8 @@ from sentry.auth.password_validation import validate_password from sentry.conf.server import AUTH_PASSWORD_VALIDATORS -from sentry.models.user import User from sentry.testutils.cases import TestCase +from sentry.users.models.user import User PWNED_PASSWORDS_RESPONSE_MOCK = """4145D488EF49819E75E71019A6E8EA21905:1 4186AA7593257C23D6A76D99FBEB3D3FEAF:2 diff --git a/tests/sentry/backup/test_coverage.py b/tests/sentry/backup/test_coverage.py index abd56373ac94a5..6c0c2ff8910d81 100644 --- a/tests/sentry/backup/test_coverage.py +++ b/tests/sentry/backup/test_coverage.py @@ -8,13 +8,13 @@ from sentry.models.groupseen import GroupSeen from sentry.models.groupshare import GroupShare from sentry.models.groupsubscription import GroupSubscription -from sentry.models.user import User +from sentry.users.models.user import User from tests.sentry.backup.test_exhaustive import EXHAUSTIVELY_TESTED, UNIQUENESS_TESTED from tests.sentry.backup.test_imports import COLLISION_TESTED from tests.sentry.backup.test_models import DYNAMIC_RELOCATION_SCOPE_TESTED from tests.sentry.backup.test_releases import RELEASE_TESTED from tests.sentry.backup.test_sanitize import SANITIZATION_TESTED -from tests.sentry.models.test_user import ORG_MEMBER_MERGE_TESTED +from tests.sentry.users.models.test_user import ORG_MEMBER_MERGE_TESTED ALL_EXPORTABLE_MODELS = {get_model_name(c) for c in get_exportable_sentry_models()} diff --git a/tests/sentry/backup/test_exports.py b/tests/sentry/backup/test_exports.py index 437cbb788dc009..1c50893ee5162c 100644 --- a/tests/sentry/backup/test_exports.py +++ b/tests/sentry/backup/test_exports.py @@ -15,10 +15,6 @@ from sentry.models.organization import Organization from sentry.models.organizationmember import OrganizationMember from sentry.models.orgauthtoken import OrgAuthToken -from sentry.models.user import User -from sentry.models.useremail import UserEmail -from sentry.models.userpermission import UserPermission -from sentry.models.userrole import UserRole, UserRoleUser from sentry.testutils.helpers.backups import ( BackupTransactionTestCase, ValidationError, @@ -26,6 +22,10 @@ export_to_file, ) from sentry.testutils.helpers.datetime import freeze_time +from sentry.users.models.user import User +from sentry.users.models.useremail import UserEmail +from sentry.users.models.userpermission import UserPermission +from sentry.users.models.userrole import UserRole, UserRoleUser from tests.sentry.backup import get_matching_exportable_models diff --git a/tests/sentry/backup/test_imports.py b/tests/sentry/backup/test_imports.py index fd49329cea947c..22b30f601227d8 100644 --- a/tests/sentry/backup/test_imports.py +++ b/tests/sentry/backup/test_imports.py @@ -34,7 +34,6 @@ from sentry.backup.scopes import ExportScope, ImportScope, RelocationScope from sentry.backup.services.import_export.model import RpcImportErrorKind from sentry.models.apitoken import DEFAULT_EXPIRATION, ApiToken, generate_token -from sentry.models.authenticator import Authenticator from sentry.models.email import Email from sentry.models.importchunk import ( ControlImportChunk, @@ -44,7 +43,6 @@ from sentry.models.lostpasswordhash import LostPasswordHash from sentry.models.options.option import ControlOption, Option from sentry.models.options.project_option import ProjectOption -from sentry.models.options.user_option import UserOption from sentry.models.organization import Organization from sentry.models.organizationmapping import OrganizationMapping from sentry.models.organizationmember import OrganizationMember @@ -59,11 +57,6 @@ from sentry.models.relay import Relay, RelayUsage from sentry.models.savedsearch import SavedSearch, Visibility from sentry.models.team import Team -from sentry.models.user import User -from sentry.models.useremail import UserEmail -from sentry.models.userip import UserIP -from sentry.models.userpermission import UserPermission -from sentry.models.userrole import UserRole, UserRoleUser from sentry.monitors.models import Monitor from sentry.receivers import create_default_projects from sentry.silo.base import SiloMode @@ -81,6 +74,13 @@ ) from sentry.testutils.hybrid_cloud import use_split_dbs from sentry.testutils.silo import assume_test_silo_mode +from sentry.users.models.authenticator import Authenticator +from sentry.users.models.user import User +from sentry.users.models.user_option import UserOption +from sentry.users.models.useremail import UserEmail +from sentry.users.models.userip import UserIP +from sentry.users.models.userpermission import UserPermission +from sentry.users.models.userrole import UserRole, UserRoleUser from tests.sentry.backup import ( expect_models, get_matching_exportable_models, diff --git a/tests/sentry/backup/test_rpc.py b/tests/sentry/backup/test_rpc.py index 7f62a9d27de834..de08a3329bf67d 100644 --- a/tests/sentry/backup/test_rpc.py +++ b/tests/sentry/backup/test_rpc.py @@ -27,11 +27,11 @@ from sentry.models.importchunk import ControlImportChunk, RegionImportChunk from sentry.models.options.option import ControlOption, Option from sentry.models.project import Project -from sentry.models.user import MAX_USERNAME_LENGTH, User from sentry.silo.base import SiloMode from sentry.testutils.cases import TestCase from sentry.testutils.factories import get_fixture_path from sentry.testutils.silo import assume_test_silo_mode, no_silo_test +from sentry.users.models.user import MAX_USERNAME_LENGTH, User CONTROL_OPTION_MODEL_NAME = get_model_name(ControlOption) OPTION_MODEL_NAME = get_model_name(Option) diff --git a/tests/sentry/db/test_router.py b/tests/sentry/db/test_router.py index cf3c12ebe7508e..8fddb36e53f3d6 100644 --- a/tests/sentry/db/test_router.py +++ b/tests/sentry/db/test_router.py @@ -4,9 +4,9 @@ from sentry.db.router import SiloRouter from sentry.models.organization import Organization -from sentry.models.user import User from sentry.testutils.cases import TestCase from sentry.testutils.hybrid_cloud import use_split_dbs +from sentry.users.models.user import User class SiloRouterSimulatedTest(TestCase): diff --git a/tests/sentry/db/test_transactions.py b/tests/sentry/db/test_transactions.py index 0d4d9167b6d92d..6f69c23d6c4314 100644 --- a/tests/sentry/db/test_transactions.py +++ b/tests/sentry/db/test_transactions.py @@ -13,13 +13,13 @@ from sentry.hybridcloud.rpc import silo_mode_delegation from sentry.models.organization import Organization from sentry.models.outbox import outbox_context -from sentry.models.user import User from sentry.silo.base import SiloMode from sentry.testutils.cases import TestCase, TransactionTestCase from sentry.testutils.factories import Factories from sentry.testutils.hybrid_cloud import collect_transaction_queries from sentry.testutils.pytest.fixtures import django_db_all from sentry.testutils.silo import no_silo_test +from sentry.users.models.user import User from sentry.utils.snowflake import MaxSnowflakeRetryError @@ -66,8 +66,9 @@ def test_safe_transaction_boundaries(self): with django_test_transaction_water_mark(): Factories.create_user() - with django_test_transaction_water_mark(), transaction.atomic( - using=router.db_for_write(User) + with ( + django_test_transaction_water_mark(), + transaction.atomic(using=router.db_for_write(User)), ): Factories.create_user() diff --git a/tests/sentry/deletions/test_group.py b/tests/sentry/deletions/test_group.py index b9f3fc48e9a454..580509f19840cb 100644 --- a/tests/sentry/deletions/test_group.py +++ b/tests/sentry/deletions/test_group.py @@ -11,11 +11,11 @@ from sentry.models.grouphash import GroupHash from sentry.models.groupmeta import GroupMeta from sentry.models.groupredirect import GroupRedirect -from sentry.models.userreport import UserReport from sentry.tasks.deletion.groups import delete_groups from sentry.testutils.cases import SnubaTestCase, TestCase from sentry.testutils.helpers.datetime import before_now, iso_format from sentry.testutils.helpers.features import with_feature +from sentry.users.models.userreport import UserReport class DeleteGroupTest(TestCase, SnubaTestCase): diff --git a/tests/sentry/deletions/test_sentry_app.py b/tests/sentry/deletions/test_sentry_app.py index 38bf999d734667..af2b7952652b1b 100644 --- a/tests/sentry/deletions/test_sentry_app.py +++ b/tests/sentry/deletions/test_sentry_app.py @@ -5,9 +5,9 @@ from sentry.models.apiapplication import ApiApplication from sentry.models.integrations.sentry_app import SentryApp from sentry.models.integrations.sentry_app_installation import SentryAppInstallation -from sentry.models.user import User from sentry.testutils.cases import TestCase from sentry.testutils.silo import control_silo_test +from sentry.users.models.user import User @control_silo_test diff --git a/tests/sentry/discover/test_models.py b/tests/sentry/discover/test_models.py index 1c2d908de90237..554217c7685620 100644 --- a/tests/sentry/discover/test_models.py +++ b/tests/sentry/discover/test_models.py @@ -2,9 +2,9 @@ from django.db import IntegrityError, router, transaction from sentry.discover.models import DiscoverSavedQuery, DiscoverSavedQueryProject -from sentry.models.user import User from sentry.testutils.cases import TestCase from sentry.testutils.silo import assume_test_silo_mode_of +from sentry.users.models.user import User class DiscoverSavedQueryTest(TestCase): @@ -84,19 +84,22 @@ def test_can_only_have_single_homepage_query_for_user_on_update(self): created_by_id=self.user.id, ) - with pytest.raises(IntegrityError), transaction.atomic( - router.db_for_write(DiscoverSavedQueryProject) + with ( + pytest.raises(IntegrityError), + transaction.atomic(router.db_for_write(DiscoverSavedQueryProject)), ): new_query.update(is_homepage=True) - with pytest.raises(IntegrityError), transaction.atomic( - router.db_for_write(DiscoverSavedQueryProject) + with ( + pytest.raises(IntegrityError), + transaction.atomic(router.db_for_write(DiscoverSavedQueryProject)), ): new_query.is_homepage = True new_query.save() - with pytest.raises(IntegrityError), transaction.atomic( - router.db_for_write(DiscoverSavedQueryProject) + with ( + pytest.raises(IntegrityError), + transaction.atomic(router.db_for_write(DiscoverSavedQueryProject)), ): DiscoverSavedQuery.objects.filter(id=new_query.id).update(is_homepage=True) diff --git a/tests/sentry/features/test_flagpole_context.py b/tests/sentry/features/test_flagpole_context.py index 1d5afcf57fe3ff..68b3203aae8c5e 100644 --- a/tests/sentry/features/test_flagpole_context.py +++ b/tests/sentry/features/test_flagpole_context.py @@ -10,10 +10,10 @@ user_context_transformer, ) from sentry.hybridcloud.services.organization_mapping import organization_mapping_service -from sentry.models.useremail import UserEmail from sentry.organizations.services.organization import organization_service from sentry.testutils.cases import TestCase from sentry.testutils.silo import control_silo_test +from sentry.users.models.useremail import UserEmail class TestSentryFlagpoleContext(TestCase): diff --git a/tests/sentry/features/test_manager.py b/tests/sentry/features/test_manager.py index 4948f23ce8fdf4..0fd24278914aca 100644 --- a/tests/sentry/features/test_manager.py +++ b/tests/sentry/features/test_manager.py @@ -14,8 +14,8 @@ SystemFeature, UserFeature, ) -from sentry.models.user import User from sentry.testutils.cases import TestCase +from sentry.users.models.user import User from sentry.users.services.user import RpcUser diff --git a/tests/sentry/hybridcloud/rpc/test_rpc_model.py b/tests/sentry/hybridcloud/rpc/test_rpc_model.py index 57765b2161b83b..7bd9b81c1eb6b1 100644 --- a/tests/sentry/hybridcloud/rpc/test_rpc_model.py +++ b/tests/sentry/hybridcloud/rpc/test_rpc_model.py @@ -1,9 +1,9 @@ from collections import deque from sentry.hybridcloud.rpc import RpcModel -from sentry.models.authenticator import Authenticator from sentry.testutils.cases import TestCase from sentry.testutils.silo import control_silo_test +from sentry.users.models.authenticator import Authenticator from sentry.users.services.user.service import user_service diff --git a/tests/sentry/hybridcloud/services/test_region_organization_provisioning.py b/tests/sentry/hybridcloud/services/test_region_organization_provisioning.py index 1bf4cf6eb46e0d..0ac6d52d72572f 100644 --- a/tests/sentry/hybridcloud/services/test_region_organization_provisioning.py +++ b/tests/sentry/hybridcloud/services/test_region_organization_provisioning.py @@ -15,7 +15,6 @@ ) from sentry.models.outbox import outbox_context from sentry.models.team import Team -from sentry.models.user import User from sentry.services.organization import ( OrganizationOptions, OrganizationProvisioningOptions, @@ -24,6 +23,7 @@ from sentry.silo.base import SiloMode from sentry.testutils.cases import TestCase from sentry.testutils.silo import assume_test_silo_mode, control_silo_test, create_test_regions +from sentry.users.models.user import User @control_silo_test(regions=create_test_regions("us")) @@ -206,8 +206,9 @@ def setUp(self) -> None: ) def create_temporary_slug_res(self, organization: Organization, slug: str, region: str) -> None: - with assume_test_silo_mode(SiloMode.CONTROL), outbox_context( - transaction.atomic(router.db_for_write(OrganizationSlugReservation)) + with ( + assume_test_silo_mode(SiloMode.CONTROL), + outbox_context(transaction.atomic(router.db_for_write(OrganizationSlugReservation))), ): OrganizationSlugReservation( reservation_type=OrganizationSlugReservationType.TEMPORARY_RENAME_ALIAS, diff --git a/tests/sentry/hybridcloud/test_organization.py b/tests/sentry/hybridcloud/test_organization.py index e0f9389321a49b..f19d70ef89fd45 100644 --- a/tests/sentry/hybridcloud/test_organization.py +++ b/tests/sentry/hybridcloud/test_organization.py @@ -11,7 +11,6 @@ from sentry.models.organizationmemberteam import OrganizationMemberTeam from sentry.models.project import Project from sentry.models.team import Team, TeamStatus -from sentry.models.user import User from sentry.organizations.services.organization import ( RpcOrganization, RpcOrganizationMember, @@ -27,6 +26,7 @@ from sentry.testutils.helpers.task_runner import TaskRunner from sentry.testutils.pytest.fixtures import django_db_all from sentry.testutils.silo import all_silo_test, assume_test_silo_mode +from sentry.users.models.user import User def basic_filled_out_org() -> tuple[Organization, list[User]]: diff --git a/tests/sentry/incidents/action_handlers/test_email.py b/tests/sentry/incidents/action_handlers/test_email.py index 86b65528eb7d74..ba759b4604d7e9 100644 --- a/tests/sentry/incidents/action_handlers/test_email.py +++ b/tests/sentry/incidents/action_handlers/test_email.py @@ -22,8 +22,6 @@ from sentry.incidents.models.incident import INCIDENT_STATUS, IncidentStatus, TriggerStatus from sentry.incidents.utils.types import AlertRuleActivationConditionType from sentry.models.notificationsettingoption import NotificationSettingOption -from sentry.models.options.user_option import UserOption -from sentry.models.useremail import UserEmail from sentry.sentry_metrics import indexer from sentry.sentry_metrics.use_case_id_registry import UseCaseID from sentry.snuba.dataset import Dataset @@ -32,6 +30,8 @@ from sentry.testutils.helpers.datetime import freeze_time from sentry.testutils.helpers.features import with_feature from sentry.testutils.silo import assume_test_silo_mode_of +from sentry.users.models.user_option import UserOption +from sentry.users.models.useremail import UserEmail from . import FireTest diff --git a/tests/sentry/incidents/endpoints/test_serializers.py b/tests/sentry/incidents/endpoints/test_serializers.py index a021f76415360e..72842130210b53 100644 --- a/tests/sentry/incidents/endpoints/test_serializers.py +++ b/tests/sentry/incidents/endpoints/test_serializers.py @@ -39,7 +39,6 @@ from sentry.integrations.services.integration.serial import serialize_integration from sentry.integrations.slack.utils.channel import SlackChannelIdData from sentry.models.environment import Environment -from sentry.models.user import User from sentry.sentry_apps.services.app import app_service from sentry.shared_integrations.exceptions import ApiError from sentry.silo.base import SiloMode @@ -48,6 +47,8 @@ from sentry.testutils.cases import TestCase from sentry.testutils.silo import assume_test_silo_mode from sentry.testutils.skips import requires_snuba +from sentry.users.models.user import User +from sentry.utils import json pytestmark = [pytest.mark.sentry_metrics, requires_snuba] diff --git a/tests/sentry/ingest/ingest_consumer/test_ingest_consumer_processing.py b/tests/sentry/ingest/ingest_consumer/test_ingest_consumer_processing.py index adcddec8da2b74..929a8da8d9bfa0 100644 --- a/tests/sentry/ingest/ingest_consumer/test_ingest_consumer_processing.py +++ b/tests/sentry/ingest/ingest_consumer/test_ingest_consumer_processing.py @@ -26,11 +26,11 @@ ) from sentry.models.debugfile import create_files_from_dif_zip from sentry.models.eventattachment import EventAttachment -from sentry.models.userreport import UserReport from sentry.options import set from sentry.testutils.pytest.fixtures import django_db_all from sentry.testutils.skips import requires_snuba from sentry.usage_accountant import accountant +from sentry.users.models.userreport import UserReport from sentry.utils.eventuser import EventUser from sentry.utils.json import loads diff --git a/tests/sentry/ingest/test_userreport.py b/tests/sentry/ingest/test_userreport.py index 4b9bef8e370bc8..180076f73b447a 100644 --- a/tests/sentry/ingest/test_userreport.py +++ b/tests/sentry/ingest/test_userreport.py @@ -1,7 +1,7 @@ from sentry.feedback.usecases.create_feedback import UNREAL_FEEDBACK_UNATTENDED_MESSAGE from sentry.ingest.userreport import is_org_in_denylist, save_userreport, should_filter_user_report -from sentry.models.userreport import UserReport from sentry.testutils.pytest.fixtures import django_db_all +from sentry.users.models.userreport import UserReport @django_db_all diff --git a/tests/sentry/integrations/jira_server/__init__.py b/tests/sentry/integrations/jira_server/__init__.py index 3019dec4d16c22..4d2f26b4dd0d0d 100644 --- a/tests/sentry/integrations/jira_server/__init__.py +++ b/tests/sentry/integrations/jira_server/__init__.py @@ -8,9 +8,9 @@ from sentry.models.integrations.external_issue import ExternalIssue from sentry.models.integrations.integration import Integration from sentry.models.organization import Organization -from sentry.models.user import User from sentry.silo.base import SiloMode from sentry.testutils.silo import assume_test_silo_mode +from sentry.users.models.user import User EXAMPLE_PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY----- MIICWwIBAAKBgQC1cd9t8sA03awggLiX2gjZxyvOVUPJksLly1E662tttTeR3Wm9 diff --git a/tests/sentry/integrations/slack/test_message_builder.py b/tests/sentry/integrations/slack/test_message_builder.py index 1644a97d426664..f5b8d10de62af6 100644 --- a/tests/sentry/integrations/slack/test_message_builder.py +++ b/tests/sentry/integrations/slack/test_message_builder.py @@ -36,7 +36,6 @@ from sentry.models.pullrequest import PullRequest from sentry.models.repository import Repository from sentry.models.team import Team -from sentry.models.user import User from sentry.notifications.utils.actions import MessageAction from sentry.ownership.grammar import Matcher, Owner, Rule, dump_schema from sentry.silo.base import SiloMode @@ -48,6 +47,7 @@ from sentry.testutils.skips import requires_snuba from sentry.types.actor import Actor from sentry.types.group import GroupSubStatus +from sentry.users.models.user import User from sentry.utils.http import absolute_uri from tests.sentry.issues.test_utils import OccurrenceTestMixin diff --git a/tests/sentry/integrations/test_notification_utilities.py b/tests/sentry/integrations/test_notification_utilities.py index 3db2b560f07c59..27032ff81a0b1d 100644 --- a/tests/sentry/integrations/test_notification_utilities.py +++ b/tests/sentry/integrations/test_notification_utilities.py @@ -7,11 +7,11 @@ from sentry.integrations.services.integration.serial import serialize_integration from sentry.integrations.types import ExternalProviders from sentry.models.integrations.integration import Integration -from sentry.models.user import User from sentry.testutils.cases import TestCase from sentry.testutils.helpers.notifications import DummyNotification from sentry.testutils.silo import control_silo_test from sentry.types.actor import Actor +from sentry.users.models.user import User @control_silo_test diff --git a/tests/sentry/issues/endpoints/test_organization_group_index.py b/tests/sentry/issues/endpoints/test_organization_group_index.py index 343e5a376af929..ce601cc4eb28af 100644 --- a/tests/sentry/issues/endpoints/test_organization_group_index.py +++ b/tests/sentry/issues/endpoints/test_organization_group_index.py @@ -40,7 +40,6 @@ from sentry.models.grouptombstone import GroupTombstone from sentry.models.integrations.external_issue import ExternalIssue from sentry.models.integrations.organization_integration import OrganizationIntegration -from sentry.models.options.user_option import UserOption from sentry.models.platformexternalissue import PlatformExternalIssue from sentry.models.release import Release from sentry.models.releaseprojectenvironment import ReleaseStages @@ -61,6 +60,7 @@ from sentry.testutils.silo import assume_test_silo_mode from sentry.types.activity import ActivityType from sentry.types.group import GroupSubStatus, PriorityLevel +from sentry.users.models.user_option import UserOption from sentry.utils import json from tests.sentry.issues.test_utils import SearchIssueTestMixin diff --git a/tests/sentry/issues/endpoints/test_organization_searches.py b/tests/sentry/issues/endpoints/test_organization_searches.py index 48cb7108bf539a..26cf45a65ff63c 100644 --- a/tests/sentry/issues/endpoints/test_organization_searches.py +++ b/tests/sentry/issues/endpoints/test_organization_searches.py @@ -5,8 +5,8 @@ from sentry.api.serializers import serialize from sentry.models.savedsearch import SavedSearch, SortOptions, Visibility from sentry.models.search_common import SearchType -from sentry.models.user import User from sentry.testutils.cases import APITestCase +from sentry.users.models.user import User class OrgLevelOrganizationSearchesListTest(APITestCase): diff --git a/tests/sentry/mail/activity/test_note.py b/tests/sentry/mail/activity/test_note.py index 50de95b0b3ec16..da3d30b42dfc85 100644 --- a/tests/sentry/mail/activity/test_note.py +++ b/tests/sentry/mail/activity/test_note.py @@ -1,7 +1,6 @@ from sentry.integrations.types import ExternalProviders from sentry.models.activity import Activity from sentry.models.notificationsettingoption import NotificationSettingOption -from sentry.models.options.user_option import UserOption from sentry.notifications.notifications.activity.note import NoteActivityNotification from sentry.notifications.types import GroupSubscriptionReason from sentry.silo.base import SiloMode @@ -9,6 +8,7 @@ from sentry.testutils.silo import assume_test_silo_mode from sentry.types.activity import ActivityType from sentry.types.actor import Actor +from sentry.users.models.user_option import UserOption class NoteTestCase(ActivityTestCase): diff --git a/tests/sentry/mail/test_adapter.py b/tests/sentry/mail/test_adapter.py index ab9277b746e9c9..d4dc203fcf65cb 100644 --- a/tests/sentry/mail/test_adapter.py +++ b/tests/sentry/mail/test_adapter.py @@ -25,7 +25,6 @@ from sentry.models.notificationsettingoption import NotificationSettingOption from sentry.models.notificationsettingprovider import NotificationSettingProvider from sentry.models.options.project_option import ProjectOption -from sentry.models.options.user_option import UserOption from sentry.models.organization import Organization from sentry.models.organizationmember import OrganizationMember from sentry.models.organizationmemberteam import OrganizationMemberTeam @@ -33,8 +32,6 @@ from sentry.models.projectownership import ProjectOwnership from sentry.models.repository import Repository from sentry.models.rule import Rule -from sentry.models.useremail import UserEmail -from sentry.models.userreport import UserReport from sentry.notifications.notifications.rules import AlertRuleNotification from sentry.notifications.types import ActionTargetType, FallthroughChoiceType from sentry.notifications.utils.digest import get_digest_subject @@ -51,6 +48,9 @@ from sentry.types.actor import Actor from sentry.types.group import GroupSubStatus from sentry.types.rules import RuleFuture +from sentry.users.models.user_option import UserOption +from sentry.users.models.useremail import UserEmail +from sentry.users.models.userreport import UserReport from sentry.utils.email import MessageBuilder, get_email_addresses from sentry_plugins.opsgenie.plugin import OpsGeniePlugin from tests.sentry.mail import make_event_data, mock_notify diff --git a/tests/sentry/mediators/project_rules/test_updater.py b/tests/sentry/mediators/project_rules/test_updater.py index ced25e5d4b3b20..e761238d42ab51 100644 --- a/tests/sentry/mediators/project_rules/test_updater.py +++ b/tests/sentry/mediators/project_rules/test_updater.py @@ -1,8 +1,8 @@ from sentry.mediators.project_rules.updater import Updater -from sentry.models.user import User from sentry.testutils.cases import TestCase from sentry.testutils.silo import assume_test_silo_mode_of from sentry.types.actor import Actor +from sentry.users.models.user import User class TestUpdater(TestCase): diff --git a/tests/sentry/mediators/test_mediator.py b/tests/sentry/mediators/test_mediator.py index aee4d0b8f00610..5c4dadf986c1a6 100644 --- a/tests/sentry/mediators/test_mediator.py +++ b/tests/sentry/mediators/test_mediator.py @@ -7,9 +7,9 @@ from sentry.mediators.mediator import Mediator from sentry.mediators.param import Param -from sentry.models.user import User from sentry.testutils.cases import TestCase from sentry.testutils.silo import control_silo_test +from sentry.users.models.user import User class Double: diff --git a/tests/sentry/mediators/test_param.py b/tests/sentry/mediators/test_param.py index c5a7e37cc0eb27..7984dc328e9c02 100644 --- a/tests/sentry/mediators/test_param.py +++ b/tests/sentry/mediators/test_param.py @@ -1,8 +1,8 @@ import pytest from sentry.mediators.param import Param -from sentry.models.user import User from sentry.testutils.cases import TestCase +from sentry.users.models.user import User class TestParam(TestCase): diff --git a/tests/sentry/middleware/test_auth.py b/tests/sentry/middleware/test_auth.py index 4e723d3ecb9623..781f6b4929bcc3 100644 --- a/tests/sentry/middleware/test_auth.py +++ b/tests/sentry/middleware/test_auth.py @@ -7,11 +7,11 @@ from sentry.middleware.auth import AuthenticationMiddleware from sentry.models.apikey import ApiKey from sentry.models.apitoken import ApiToken -from sentry.models.userip import UserIP from sentry.silo.base import SiloMode from sentry.testutils.cases import TestCase from sentry.testutils.outbox import outbox_runner from sentry.testutils.silo import all_silo_test, assume_test_silo_mode +from sentry.users.models.userip import UserIP from sentry.users.services.user.service import user_service from sentry.utils.auth import login diff --git a/tests/sentry/middleware/test_ratelimit_middleware.py b/tests/sentry/middleware/test_ratelimit_middleware.py index 16b2ec75949eff..e373aff7561896 100644 --- a/tests/sentry/middleware/test_ratelimit_middleware.py +++ b/tests/sentry/middleware/test_ratelimit_middleware.py @@ -11,13 +11,13 @@ from sentry.api.base import Endpoint from sentry.middleware.ratelimit import RatelimitMiddleware -from sentry.models.user import User from sentry.ratelimits.config import RateLimitConfig, get_default_rate_limits_for_group from sentry.ratelimits.utils import get_rate_limit_config, get_rate_limit_value from sentry.testutils.cases import APITestCase, BaseTestCase, TestCase from sentry.testutils.helpers.datetime import freeze_time from sentry.testutils.silo import all_silo_test, assume_test_silo_mode_of from sentry.types.ratelimit import RateLimit, RateLimitCategory +from sentry.users.models.user import User @all_silo_test diff --git a/tests/sentry/models/test_groupsubscription.py b/tests/sentry/models/test_groupsubscription.py index d181239be21c19..2dedc4a5d7ea46 100644 --- a/tests/sentry/models/test_groupsubscription.py +++ b/tests/sentry/models/test_groupsubscription.py @@ -8,7 +8,6 @@ from sentry.models.notificationsettingoption import NotificationSettingOption from sentry.models.notificationsettingprovider import NotificationSettingProvider from sentry.models.team import Team -from sentry.models.user import User from sentry.notifications.types import ( GroupSubscriptionReason, NotificationScopeEnum, @@ -21,6 +20,7 @@ from sentry.testutils.helpers.slack import link_team from sentry.testutils.silo import assume_test_silo_mode from sentry.types.actor import Actor +from sentry.users.models.user import User from sentry.users.services.user.service import user_service diff --git a/tests/sentry/models/test_notificationsettingoption.py b/tests/sentry/models/test_notificationsettingoption.py index 93d4c160a7c9fa..4966054a4869fa 100644 --- a/tests/sentry/models/test_notificationsettingoption.py +++ b/tests/sentry/models/test_notificationsettingoption.py @@ -1,10 +1,10 @@ from sentry.models.notificationsettingoption import NotificationSettingOption -from sentry.models.user import User from sentry.silo.base import SiloMode from sentry.tasks.deletion.hybrid_cloud import schedule_hybrid_cloud_foreign_key_jobs_control from sentry.testutils.cases import TestCase from sentry.testutils.outbox import outbox_runner from sentry.testutils.silo import assume_test_silo_mode, control_silo_test +from sentry.users.models.user import User def assert_no_notification_settings(): diff --git a/tests/sentry/models/test_notificationsettingprovider.py b/tests/sentry/models/test_notificationsettingprovider.py index 92f734b8a43537..87ae968f4f70a1 100644 --- a/tests/sentry/models/test_notificationsettingprovider.py +++ b/tests/sentry/models/test_notificationsettingprovider.py @@ -1,10 +1,10 @@ from sentry.models.notificationsettingprovider import NotificationSettingProvider -from sentry.models.user import User from sentry.silo.base import SiloMode from sentry.tasks.deletion.hybrid_cloud import schedule_hybrid_cloud_foreign_key_jobs_control from sentry.testutils.cases import TestCase from sentry.testutils.outbox import outbox_runner from sentry.testutils.silo import assume_test_silo_mode, control_silo_test +from sentry.users.models.user import User def assert_no_notification_settings(): diff --git a/tests/sentry/models/test_organization.py b/tests/sentry/models/test_organization.py index 82a22ee1fe77c4..83701adcb59151 100644 --- a/tests/sentry/models/test_organization.py +++ b/tests/sentry/models/test_organization.py @@ -18,10 +18,8 @@ from sentry.models.notificationsettingoption import NotificationSettingOption from sentry.models.notificationsettingprovider import NotificationSettingProvider from sentry.models.options.organization_option import OrganizationOption -from sentry.models.options.user_option import UserOption from sentry.models.organization import Organization from sentry.models.organizationmember import OrganizationMember -from sentry.models.user import User from sentry.silo.base import SiloMode from sentry.tasks.deletion.hybrid_cloud import ( schedule_hybrid_cloud_foreign_key_jobs, @@ -32,6 +30,8 @@ from sentry.testutils.hybrid_cloud import HybridCloudTestMixin from sentry.testutils.outbox import outbox_runner from sentry.testutils.silo import assume_test_silo_mode, assume_test_silo_mode_of +from sentry.users.models.user import User +from sentry.users.models.user_option import UserOption class OrganizationTest(TestCase, HybridCloudTestMixin): @@ -228,9 +228,11 @@ def test_handle_2fa_required__compliant_and_non_compliant_members(self): self.assert_org_member_mapping(org_member=compliant_member) self.assert_org_member_mapping(org_member=non_compliant_member) - with self.options( - {"system.url-prefix": "http://example.com"} - ), self.tasks(), outbox_runner(): + with ( + self.options({"system.url-prefix": "http://example.com"}), + self.tasks(), + outbox_runner(), + ): self.org.handle_2fa_required(self.request) self.is_organization_member(compliant_user.id, compliant_member.id) @@ -280,9 +282,11 @@ def test_handle_2fa_required__non_compliant_members(self): self.assert_org_member_mapping(org_member=member) non_compliant.append((user, member)) - with self.options( - {"system.url-prefix": "http://example.com"} - ), self.tasks(), outbox_runner(): + with ( + self.options({"system.url-prefix": "http://example.com"}), + self.tasks(), + outbox_runner(), + ): self.org.handle_2fa_required(self.request) for user, member in non_compliant: @@ -340,9 +344,11 @@ def test_handle_2fa_required__no_actor_and_api_key__ok(self, auth_log): self.assert_org_member_mapping(org_member=member) - with self.options( - {"system.url-prefix": "http://example.com"} - ), self.tasks(), outbox_runner(): + with ( + self.options({"system.url-prefix": "http://example.com"}), + self.tasks(), + outbox_runner(), + ): with assume_test_silo_mode(SiloMode.CONTROL): api_key = ApiKey.objects.create( organization_id=self.org.id, @@ -373,9 +379,11 @@ def test_handle_2fa_required__no_ip_address__ok(self, auth_log): user, member = self._create_user_and_member() self.assert_org_member_mapping(org_member=member) - with self.options( - {"system.url-prefix": "http://example.com"} - ), self.tasks(), outbox_runner(): + with ( + self.options({"system.url-prefix": "http://example.com"}), + self.tasks(), + outbox_runner(), + ): request = copy.deepcopy(self.request) request.META["REMOTE_ADDR"] = None self.org.handle_2fa_required(request) diff --git a/tests/sentry/models/test_outbox.py b/tests/sentry/models/test_outbox.py index 3adecec2d97c1e..74b8e76a6a6584 100644 --- a/tests/sentry/models/test_outbox.py +++ b/tests/sentry/models/test_outbox.py @@ -22,7 +22,6 @@ RegionOutbox, outbox_context, ) -from sentry.models.user import User from sentry.silo.base import SiloMode from sentry.tasks.deliver_from_outbox import enqueue_outbox_jobs from sentry.testutils.cases import TestCase, TransactionTestCase @@ -31,6 +30,7 @@ from sentry.testutils.outbox import outbox_runner from sentry.testutils.silo import assume_test_silo_mode, assume_test_silo_mode_of, control_silo_test from sentry.types.region import Region, RegionCategory, get_local_region +from sentry.users.models.user import User def wrap_with_connection_closure(c: Callable[..., Any]) -> Callable[..., Any]: @@ -434,9 +434,10 @@ def assert_called_for_org(org): ensure_converged() def test_outbox_converges(self): - with patch( - "sentry.models.outbox.process_region_outbox.send" - ) as mock_process_region_outbox, outbox_context(flush=False): + with ( + patch("sentry.models.outbox.process_region_outbox.send") as mock_process_region_outbox, + outbox_context(flush=False), + ): Organization(id=10001).outbox_for_update().save() Organization(id=10001).outbox_for_update().save() diff --git a/tests/sentry/models/test_project.py b/tests/sentry/models/test_project.py index 854768c25662d8..056e1304d37dd8 100644 --- a/tests/sentry/models/test_project.py +++ b/tests/sentry/models/test_project.py @@ -8,7 +8,6 @@ from sentry.models.notificationsettingoption import NotificationSettingOption from sentry.models.options.project_option import ProjectOption from sentry.models.options.project_template_option import ProjectTemplateOption -from sentry.models.options.user_option import UserOption from sentry.models.organizationmember import OrganizationMember from sentry.models.organizationmemberteam import OrganizationMemberTeam from sentry.models.project import Project @@ -19,7 +18,6 @@ from sentry.models.releases.release_project import ReleaseProject from sentry.models.rule import Rule from sentry.models.scheduledeletion import RegionScheduledDeletion -from sentry.models.user import User from sentry.monitors.models import Monitor, MonitorEnvironment, MonitorType, ScheduleType from sentry.notifications.types import NotificationSettingEnum from sentry.notifications.utils.participants import get_notification_recipients @@ -31,6 +29,8 @@ from sentry.testutils.outbox import outbox_runner from sentry.testutils.silo import assume_test_silo_mode, control_silo_test from sentry.types.actor import Actor +from sentry.users.models.user import User +from sentry.users.models.user_option import UserOption class ProjectTest(APITestCase, TestCase): diff --git a/tests/sentry/monitors/tasks/test_detect_broken_monitor_envs.py b/tests/sentry/monitors/tasks/test_detect_broken_monitor_envs.py index 81cf7258e0a71d..a6d5dc6eaef64c 100644 --- a/tests/sentry/monitors/tasks/test_detect_broken_monitor_envs.py +++ b/tests/sentry/monitors/tasks/test_detect_broken_monitor_envs.py @@ -7,8 +7,6 @@ from sentry.constants import ObjectStatus from sentry.grouping.utils import hash_from_values from sentry.models.notificationsettingoption import NotificationSettingOption -from sentry.models.options.user_option import UserOption -from sentry.models.useremail import UserEmail from sentry.monitors.models import ( CheckInStatus, Monitor, @@ -25,6 +23,8 @@ from sentry.testutils.cases import TestCase from sentry.testutils.helpers.datetime import before_now from sentry.testutils.silo import assume_test_silo_mode +from sentry.users.models.user_option import UserOption +from sentry.users.models.useremail import UserEmail class MonitorDetectBrokenMonitorEnvTaskTest(TestCase): diff --git a/tests/sentry/notifications/test_notifications.py b/tests/sentry/notifications/test_notifications.py index 318745d9cfd09b..a718a03f99eda4 100644 --- a/tests/sentry/notifications/test_notifications.py +++ b/tests/sentry/notifications/test_notifications.py @@ -20,7 +20,6 @@ from sentry.models.groupassignee import GroupAssignee from sentry.models.identity import Identity, IdentityStatus from sentry.models.notificationsettingoption import NotificationSettingOption -from sentry.models.options.user_option import UserOption from sentry.models.rule import Rule from sentry.notifications.notifications.activity.assigned import AssignedActivityNotification from sentry.notifications.notifications.activity.regression import RegressionActivityNotification @@ -32,6 +31,7 @@ from sentry.testutils.silo import assume_test_silo_mode, control_silo_test from sentry.testutils.skips import requires_snuba from sentry.types.activity import ActivityType +from sentry.users.models.user_option import UserOption from sentry.utils import json pytestmark = [requires_snuba] diff --git a/tests/sentry/notifications/utils/test_participants.py b/tests/sentry/notifications/utils/test_participants.py index 582ff10122d7ae..950e95cc58586e 100644 --- a/tests/sentry/notifications/utils/test_participants.py +++ b/tests/sentry/notifications/utils/test_participants.py @@ -18,7 +18,6 @@ from sentry.models.projectownership import ProjectOwnership from sentry.models.repository import Repository from sentry.models.team import Team -from sentry.models.user import User from sentry.notifications.types import ( ActionTargetType, FallthroughChoiceType, @@ -41,6 +40,7 @@ from sentry.testutils.silo import assume_test_silo_mode from sentry.testutils.skips import requires_snuba from sentry.types.actor import Actor +from sentry.users.models.user import User from sentry.users.services.user.service import user_service from sentry.utils.cache import cache from tests.sentry.mail import make_event_data diff --git a/tests/sentry/plugins/bases/test_issue.py b/tests/sentry/plugins/bases/test_issue.py index 817a6aa5521b61..86acb8e2d36235 100644 --- a/tests/sentry/plugins/bases/test_issue.py +++ b/tests/sentry/plugins/bases/test_issue.py @@ -2,10 +2,10 @@ import pytest -from sentry.models.user import User from sentry.plugins.bases.issue import IssueTrackingPlugin from sentry.testutils.cases import TestCase from sentry.testutils.silo import control_silo_test +from sentry.users.models.user import User from social_auth.models import UserSocialAuth diff --git a/tests/sentry/plugins/bases/test_issue2.py b/tests/sentry/plugins/bases/test_issue2.py index 9c4ad58844cbf6..6f3ae3f55183e7 100644 --- a/tests/sentry/plugins/bases/test_issue2.py +++ b/tests/sentry/plugins/bases/test_issue2.py @@ -3,12 +3,12 @@ import pytest from sentry.models.groupmeta import GroupMeta -from sentry.models.user import User from sentry.plugins.base import plugins from sentry.plugins.bases.issue2 import IssueTrackingPlugin2 from sentry.testutils.cases import TestCase from sentry.testutils.helpers.datetime import before_now, iso_format from sentry.testutils.skips import requires_snuba +from sentry.users.models.user import User from sentry.utils import json pytestmark = [requires_snuba] diff --git a/tests/sentry/ratelimits/utils/test_get_ratelimit_key.py b/tests/sentry/ratelimits/utils/test_get_ratelimit_key.py index 889170545697b2..a01c9733693628 100644 --- a/tests/sentry/ratelimits/utils/test_get_ratelimit_key.py +++ b/tests/sentry/ratelimits/utils/test_get_ratelimit_key.py @@ -11,12 +11,12 @@ from sentry.models.apitoken import ApiToken from sentry.models.integrations.sentry_app_installation import SentryAppInstallation from sentry.models.integrations.sentry_app_installation_token import SentryAppInstallationToken -from sentry.models.user import User from sentry.ratelimits import get_rate_limit_config, get_rate_limit_key from sentry.ratelimits.config import RateLimitConfig from sentry.testutils.cases import TestCase from sentry.testutils.silo import all_silo_test, assume_test_silo_mode_of from sentry.types.ratelimit import RateLimit, RateLimitCategory +from sentry.users.models.user import User CONCURRENT_RATE_LIMIT = 20 diff --git a/tests/sentry/receivers/test_core.py b/tests/sentry/receivers/test_core.py index 2bd1da0e3b1344..ef606093b3e519 100644 --- a/tests/sentry/receivers/test_core.py +++ b/tests/sentry/receivers/test_core.py @@ -7,11 +7,11 @@ from sentry.models.project import Project from sentry.models.projectkey import ProjectKey from sentry.models.team import Team -from sentry.models.user import User from sentry.receivers.core import DEFAULT_SENTRY_PROJECT_ID, create_default_projects from sentry.silo.safety import unguarded_write from sentry.testutils.cases import TestCase from sentry.testutils.silo import assume_test_silo_mode_of +from sentry.users.models.user import User class CreateDefaultProjectsTest(TestCase): diff --git a/tests/sentry/receivers/test_releases.py b/tests/sentry/receivers/test_releases.py index 61c0976fdc220f..d1193f5e25dca4 100644 --- a/tests/sentry/receivers/test_releases.py +++ b/tests/sentry/receivers/test_releases.py @@ -12,17 +12,17 @@ from sentry.models.groupinbox import GroupInbox, GroupInboxReason, add_group_to_inbox from sentry.models.grouplink import GroupLink from sentry.models.groupsubscription import GroupSubscription -from sentry.models.options.user_option import UserOption from sentry.models.organizationmember import OrganizationMember from sentry.models.release import Release from sentry.models.releases.release_project import ReleaseProject from sentry.models.repository import Repository -from sentry.models.useremail import UserEmail from sentry.signals import buffer_incr_complete, receivers_raise_on_send from sentry.silo.base import SiloMode from sentry.testutils.cases import TestCase from sentry.testutils.silo import assume_test_silo_mode from sentry.types.activity import ActivityType +from sentry.users.models.user_option import UserOption +from sentry.users.models.useremail import UserEmail class ResolveGroupResolutionsTest(TestCase): diff --git a/tests/sentry/runner/commands/test_createuser.py b/tests/sentry/runner/commands/test_createuser.py index 68bea019b35b3b..0ce3802317dc83 100644 --- a/tests/sentry/runner/commands/test_createuser.py +++ b/tests/sentry/runner/commands/test_createuser.py @@ -1,13 +1,13 @@ from sentry import roles from sentry.models.organization import Organization from sentry.models.organizationmember import OrganizationMember -from sentry.models.user import User -from sentry.models.userrole import manage_default_super_admin_role from sentry.receivers import create_default_projects from sentry.runner.commands.createuser import createuser from sentry.silo.base import SiloMode from sentry.testutils.cases import CliTestCase from sentry.testutils.silo import assume_test_silo_mode, control_silo_test +from sentry.users.models.user import User +from sentry.users.models.userrole import manage_default_super_admin_role from sentry.users.services.user.service import user_service diff --git a/tests/sentry/sentry_apps/test_sentry_app_creator.py b/tests/sentry/sentry_apps/test_sentry_app_creator.py index 942c69a35942e2..ec44e9e15882b1 100644 --- a/tests/sentry/sentry_apps/test_sentry_app_creator.py +++ b/tests/sentry/sentry_apps/test_sentry_app_creator.py @@ -9,10 +9,10 @@ from sentry.models.integrations.sentry_app import SentryApp from sentry.models.integrations.sentry_app_component import SentryAppComponent from sentry.models.integrations.sentry_app_installation import SentryAppInstallation -from sentry.models.user import User from sentry.sentry_apps.apps import SentryAppCreator from sentry.testutils.cases import TestCase from sentry.testutils.silo import control_silo_test +from sentry.users.models.user import User @control_silo_test diff --git a/tests/sentry/tasks/deletion/test_hybrid_cloud.py b/tests/sentry/tasks/deletion/test_hybrid_cloud.py index daed20de7f9f2d..6df693764b12e8 100644 --- a/tests/sentry/tasks/deletion/test_hybrid_cloud.py +++ b/tests/sentry/tasks/deletion/test_hybrid_cloud.py @@ -20,7 +20,6 @@ from sentry.models.project import Project from sentry.models.savedsearch import SavedSearch from sentry.models.tombstone import RegionTombstone -from sentry.models.user import User from sentry.monitors.models import Monitor from sentry.silo.base import SiloMode from sentry.tasks.deletion.hybrid_cloud import ( @@ -45,6 +44,7 @@ region_silo_test, ) from sentry.types.region import find_regions_for_user +from sentry.users.models.user import User @region_silo_model @@ -58,8 +58,9 @@ class Meta: @pytest.fixture(autouse=True) def batch_size_one(): - with patch("sentry.deletions.base.ModelDeletionTask.DEFAULT_QUERY_LIMIT", new=1), patch( - "sentry.tasks.deletion.hybrid_cloud.get_batch_size", return_value=1 + with ( + patch("sentry.deletions.base.ModelDeletionTask.DEFAULT_QUERY_LIMIT", new=1), + patch("sentry.tasks.deletion.hybrid_cloud.get_batch_size", return_value=1), ): yield @@ -392,8 +393,9 @@ def test_raises_when_option_disabled(self): assert Monitor.objects.filter(id=monitor.id).exists() - with pytest.raises(Exception) as exc, override_options( - {"hybrid_cloud.allow_cross_db_tombstones": False} + with ( + pytest.raises(Exception) as exc, + override_options({"hybrid_cloud.allow_cross_db_tombstones": False}), ): with BurstTaskRunner() as burst: schedule_hybrid_cloud_foreign_key_jobs() diff --git a/tests/sentry/tasks/test_merge.py b/tests/sentry/tasks/test_merge.py index 387f7aa5b85d19..3f8fc1d71d584f 100644 --- a/tests/sentry/tasks/test_merge.py +++ b/tests/sentry/tasks/test_merge.py @@ -5,11 +5,11 @@ from sentry.models.groupenvironment import GroupEnvironment from sentry.models.groupmeta import GroupMeta from sentry.models.groupredirect import GroupRedirect -from sentry.models.userreport import UserReport from sentry.similarity import _make_index_backend, features from sentry.tasks.merge import merge_groups from sentry.testutils.cases import SnubaTestCase, TestCase from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.users.models.userreport import UserReport from sentry.utils import redis # Use the default redis client as a cluster client in the similarity index diff --git a/tests/sentry/tasks/test_on_demand_metrics.py b/tests/sentry/tasks/test_on_demand_metrics.py index 32ca37bb29e708..659efc06fd7e45 100644 --- a/tests/sentry/tasks/test_on_demand_metrics.py +++ b/tests/sentry/tasks/test_on_demand_metrics.py @@ -7,7 +7,6 @@ from sentry.models.dashboard_widget import DashboardWidgetQueryOnDemand from sentry.models.organization import Organization from sentry.models.project import Project -from sentry.models.user import User from sentry.tasks import on_demand_metrics from sentry.tasks.on_demand_metrics import ( get_field_cardinality_cache_key, @@ -18,6 +17,7 @@ from sentry.testutils.helpers import Feature, override_options from sentry.testutils.helpers.on_demand import create_widget from sentry.testutils.pytest.fixtures import django_db_all +from sentry.users.models.user import User from sentry.utils.cache import cache _WIDGET_EXTRACTION_FEATURES = {"organizations:on-demand-metrics-extraction-widgets": True} @@ -402,18 +402,19 @@ def test_schedule_on_demand_check( dashboard=dashboard, ) - with mock.patch( - "sentry.tasks.on_demand_metrics._query_cardinality", - return_value=( - {"data": [{f"count_unique({col[0]})": 50 for col in columns if col}]}, - [col[0] for col in columns if col], - ), - ) as _query_cardinality, mock.patch.object( - process_widget_specs, "delay", wraps=process_widget_specs - ) as process_widget_specs_spy, override_options( - options - ), Feature( - feature_flags + with ( + mock.patch( + "sentry.tasks.on_demand_metrics._query_cardinality", + return_value=( + {"data": [{f"count_unique({col[0]})": 50 for col in columns if col}]}, + [col[0] for col in columns if col], + ), + ) as _query_cardinality, + mock.patch.object( + process_widget_specs, "delay", wraps=process_widget_specs + ) as process_widget_specs_spy, + override_options(options), + Feature(feature_flags), ): assert not process_widget_specs_spy.called schedule_on_demand_check() diff --git a/tests/sentry/tasks/test_post_process.py b/tests/sentry/tasks/test_post_process.py index 22309d04f37915..72ee3516e5b18c 100644 --- a/tests/sentry/tasks/test_post_process.py +++ b/tests/sentry/tasks/test_post_process.py @@ -45,7 +45,6 @@ from sentry.models.integrations.integration import Integration from sentry.models.projectownership import ProjectOwnership from sentry.models.projectteam import ProjectTeam -from sentry.models.userreport import UserReport from sentry.ownership.grammar import Matcher, Owner, Rule, dump_schema from sentry.replays.lib import kafka as replays_kafka from sentry.replays.lib.kafka import clear_replay_publisher @@ -76,6 +75,7 @@ from sentry.types.activity import ActivityType from sentry.types.group import GroupSubStatus, PriorityLevel from sentry.uptime.detectors.ranking import _get_cluster, get_project_bucket_key +from sentry.users.models.userreport import UserReport from sentry.users.services.user.service import user_service from sentry.utils import json from sentry.utils.cache import cache diff --git a/tests/sentry/tasks/test_relocation.py b/tests/sentry/tasks/test_relocation.py index 78383ae85eb617..454030d6b51ffd 100644 --- a/tests/sentry/tasks/test_relocation.py +++ b/tests/sentry/tasks/test_relocation.py @@ -38,7 +38,6 @@ RelocationValidationAttempt, ValidationStatus, ) -from sentry.models.user import User from sentry.silo.base import SiloMode from sentry.tasks.relocation import ( ERR_NOTIFYING_INTERNAL, @@ -80,6 +79,7 @@ from sentry.testutils.helpers.backups import FakeKeyManagementServiceClient, generate_rsa_key_pair from sentry.testutils.helpers.task_runner import BurstTaskRunner, BurstTaskRunnerRetryError from sentry.testutils.silo import assume_test_silo_mode +from sentry.users.models.user import User from sentry.utils import json from sentry.utils.relocation import RELOCATION_BLOB_SIZE, RELOCATION_FILE_TYPE, OrderedTask diff --git a/tests/sentry/tasks/test_reprocessing2.py b/tests/sentry/tasks/test_reprocessing2.py index aec917497cbb4a..14d9570b53acec 100644 --- a/tests/sentry/tasks/test_reprocessing2.py +++ b/tests/sentry/tasks/test_reprocessing2.py @@ -19,7 +19,6 @@ from sentry.models.group import Group from sentry.models.groupassignee import GroupAssignee from sentry.models.groupredirect import GroupRedirect -from sentry.models.userreport import UserReport from sentry.plugins.base.v2 import Plugin2 from sentry.projectoptions.defaults import DEFAULT_GROUPING_CONFIG from sentry.reprocessing2 import is_group_finished @@ -30,6 +29,7 @@ from sentry.testutils.pytest.fixtures import django_db_all from sentry.testutils.skips import requires_snuba from sentry.types.activity import ActivityType +from sentry.users.models.userreport import UserReport from sentry.utils.cache import cache_key_for_event pytestmark = [requires_snuba] diff --git a/tests/sentry/tasks/test_update_user_reports.py b/tests/sentry/tasks/test_update_user_reports.py index a132f0444eb3d8..fe5935dda786b9 100644 --- a/tests/sentry/tasks/test_update_user_reports.py +++ b/tests/sentry/tasks/test_update_user_reports.py @@ -3,10 +3,10 @@ from django.utils import timezone -from sentry.models.userreport import UserReport from sentry.tasks.update_user_reports import update_user_reports from sentry.testutils.cases import TestCase from sentry.testutils.skips import requires_snuba +from sentry.users.models.userreport import UserReport pytestmark = [requires_snuba] diff --git a/tests/sentry/models/test_authenticator.py b/tests/sentry/users/models/test_authenticator.py similarity index 96% rename from tests/sentry/models/test_authenticator.py rename to tests/sentry/users/models/test_authenticator.py index 9c93a950ffeb7c..0c9fd0207d4dd9 100644 --- a/tests/sentry/models/test_authenticator.py +++ b/tests/sentry/users/models/test_authenticator.py @@ -4,9 +4,9 @@ from sentry.auth.authenticators.recovery_code import RecoveryCodeInterface from sentry.auth.authenticators.totp import TotpInterface from sentry.auth.authenticators.u2f import create_credential_object -from sentry.models.authenticator import Authenticator, AuthenticatorConfig from sentry.testutils.cases import TestCase from sentry.testutils.silo import control_silo_test +from sentry.users.models.authenticator import Authenticator, AuthenticatorConfig @control_silo_test diff --git a/tests/sentry/models/test_avatar.py b/tests/sentry/users/models/test_avatar.py similarity index 100% rename from tests/sentry/models/test_avatar.py rename to tests/sentry/users/models/test_avatar.py diff --git a/tests/sentry/models/test_user.py b/tests/sentry/users/models/test_user.py similarity index 99% rename from tests/sentry/models/test_user.py rename to tests/sentry/users/models/test_user.py index dd88ce3ecefbd3..985976d3ecd2b0 100644 --- a/tests/sentry/models/test_user.py +++ b/tests/sentry/users/models/test_user.py @@ -8,7 +8,6 @@ from sentry.incidents.models.alert_rule import AlertRule, AlertRuleActivity from sentry.incidents.models.incident import IncidentActivity, IncidentSubscription from sentry.models.activity import Activity -from sentry.models.authenticator import Authenticator from sentry.models.authidentity import AuthIdentity from sentry.models.dashboard import Dashboard from sentry.models.dynamicsampling import CustomDynamicSamplingRule @@ -28,8 +27,6 @@ from sentry.models.rulesnooze import RuleSnooze from sentry.models.savedsearch import SavedSearch from sentry.models.tombstone import RegionTombstone -from sentry.models.user import User -from sentry.models.useremail import UserEmail from sentry.monitors.models import Monitor from sentry.sentry_metrics.models import ( SpanAttributeExtractionRuleCondition, @@ -44,6 +41,9 @@ from sentry.testutils.outbox import outbox_runner from sentry.testutils.silo import assume_test_silo_mode, assume_test_silo_mode_of, control_silo_test from sentry.types.region import Region, RegionCategory, find_regions_for_user +from sentry.users.models.authenticator import Authenticator +from sentry.users.models.user import User +from sentry.users.models.useremail import UserEmail from tests.sentry.backup import expect_models _TEST_REGIONS = ( diff --git a/tests/sentry/models/test_useremail.py b/tests/sentry/users/models/test_useremail.py similarity index 89% rename from tests/sentry/models/test_useremail.py rename to tests/sentry/users/models/test_useremail.py index ffd0d9955dbef1..e851c6b8bdf442 100644 --- a/tests/sentry/models/test_useremail.py +++ b/tests/sentry/users/models/test_useremail.py @@ -1,6 +1,6 @@ -from sentry.models.useremail import UserEmail from sentry.testutils.cases import TestCase from sentry.testutils.silo import control_silo_test +from sentry.users.models.useremail import UserEmail @control_silo_test diff --git a/tests/sentry/models/test_userpermission.py b/tests/sentry/users/models/test_userpermission.py similarity index 90% rename from tests/sentry/models/test_userpermission.py rename to tests/sentry/users/models/test_userpermission.py index 4e5dedcd4f8789..596da7e7093888 100644 --- a/tests/sentry/models/test_userpermission.py +++ b/tests/sentry/users/models/test_userpermission.py @@ -1,6 +1,6 @@ -from sentry.models.userpermission import UserPermission from sentry.testutils.cases import TestCase from sentry.testutils.silo import control_silo_test +from sentry.users.models.userpermission import UserPermission @control_silo_test diff --git a/tests/sentry/models/test_userrole.py b/tests/sentry/users/models/test_userrole.py similarity index 84% rename from tests/sentry/models/test_userrole.py rename to tests/sentry/users/models/test_userrole.py index e51e932690eea5..ec7566bb5f0968 100644 --- a/tests/sentry/models/test_userrole.py +++ b/tests/sentry/users/models/test_userrole.py @@ -1,8 +1,8 @@ from django.conf import settings -from sentry.models.userrole import UserRole, manage_default_super_admin_role from sentry.testutils.cases import TestCase from sentry.testutils.silo import control_silo_test +from sentry.users.models.userrole import UserRole, manage_default_super_admin_role @control_silo_test diff --git a/tests/sentry/users/services/test_user_impl.py b/tests/sentry/users/services/test_user_impl.py index 49ffa911b508ad..810aa2c735a31f 100644 --- a/tests/sentry/users/services/test_user_impl.py +++ b/tests/sentry/users/services/test_user_impl.py @@ -1,10 +1,10 @@ from sentry.auth.providers.fly.provider import FlyOAuth2Provider from sentry.models.authidentity import AuthIdentity from sentry.models.authprovider import AuthProvider -from sentry.models.user import User -from sentry.models.useremail import UserEmail from sentry.testutils.cases import TestCase from sentry.testutils.silo import control_silo_test +from sentry.users.models.user import User +from sentry.users.models.useremail import UserEmail from sentry.users.services.user.service import user_service diff --git a/tests/sentry/utils/email/test_message_builder.py b/tests/sentry/utils/email/test_message_builder.py index f2936129491bfe..fc9724200826fa 100644 --- a/tests/sentry/utils/email/test_message_builder.py +++ b/tests/sentry/utils/email/test_message_builder.py @@ -6,12 +6,12 @@ from sentry import options from sentry.models.groupemailthread import GroupEmailThread -from sentry.models.options.user_option import UserOption -from sentry.models.user import User -from sentry.models.useremail import UserEmail from sentry.silo.base import SiloMode from sentry.testutils.cases import TestCase from sentry.testutils.silo import assume_test_silo_mode +from sentry.users.models.user import User +from sentry.users.models.user_option import UserOption +from sentry.users.models.useremail import UserEmail from sentry.utils import json from sentry.utils.email import MessageBuilder from sentry.utils.email.faker import create_fake_email diff --git a/tests/sentry/utils/mockdata/test_core.py b/tests/sentry/utils/mockdata/test_core.py index 4cb846bf20b355..8cd95a3d22980b 100644 --- a/tests/sentry/utils/mockdata/test_core.py +++ b/tests/sentry/utils/mockdata/test_core.py @@ -4,12 +4,12 @@ from sentry.models.project import Project from sentry.models.release import Release from sentry.models.team import Team -from sentry.models.user import User from sentry.monitors.models import Monitor from sentry.silo.base import SiloMode from sentry.testutils.cases import SnubaTestCase, TestCase from sentry.testutils.pytest.fixtures import django_db_all from sentry.testutils.silo import assume_test_silo_mode, control_silo_test, no_silo_test +from sentry.users.models.user import User from sentry.utils import mockdata diff --git a/tests/sentry/utils/test_auth.py b/tests/sentry/utils/test_auth.py index 8dc0b756d1bbf8..3f9208e9c55294 100644 --- a/tests/sentry/utils/test_auth.py +++ b/tests/sentry/utils/test_auth.py @@ -6,9 +6,9 @@ from django.urls import reverse import sentry.utils.auth -from sentry.models.user import User from sentry.testutils.cases import TestCase from sentry.testutils.silo import control_silo_test +from sentry.users.models.user import User from sentry.utils.auth import ( EmailAuthBackend, SsoSession, diff --git a/tests/sentry/utils/test_query.py b/tests/sentry/utils/test_query.py index 7850f7ea31025a..fd4c5ac4935406 100644 --- a/tests/sentry/utils/test_query.py +++ b/tests/sentry/utils/test_query.py @@ -2,10 +2,10 @@ from sentry.db.models.query import in_iexact from sentry.models.organization import Organization -from sentry.models.user import User -from sentry.models.userreport import UserReport from sentry.testutils.cases import TestCase from sentry.testutils.silo import no_silo_test +from sentry.users.models.user import User +from sentry.users.models.userreport import UserReport from sentry.utils.query import ( InvalidQuerySetError, RangeQuerySetWrapper, diff --git a/tests/sentry/utils/test_ratelimits.py b/tests/sentry/utils/test_ratelimits.py index 5ccd3645192971..8774990107fcfe 100644 --- a/tests/sentry/utils/test_ratelimits.py +++ b/tests/sentry/utils/test_ratelimits.py @@ -3,8 +3,8 @@ from sentry import ratelimits from sentry.models.apitoken import ApiToken from sentry.models.organization import Organization -from sentry.models.user import User from sentry.testutils.cases import TestCase +from sentry.users.models.user import User # Produce faster tests by reducing the limits so we don't have to generate so many. RELAXED_CONFIG = { diff --git a/tests/sentry/utils/test_snowflake.py b/tests/sentry/utils/test_snowflake.py index 7f9036df69979b..5040a0b05dfd39 100644 --- a/tests/sentry/utils/test_snowflake.py +++ b/tests/sentry/utils/test_snowflake.py @@ -7,12 +7,12 @@ from sentry.models.organization import Organization from sentry.models.project import Project from sentry.models.team import Team -from sentry.models.user import User from sentry.silo.base import SiloMode from sentry.testutils.cases import TestCase from sentry.testutils.helpers.datetime import freeze_time from sentry.testutils.region import override_regions from sentry.types.region import Region, RegionCategory +from sentry.users.models.user import User from sentry.utils import snowflake from sentry.utils.snowflake import ( _TTL, diff --git a/tests/sentry/web/frontend/test_accounts.py b/tests/sentry/web/frontend/test_accounts.py index 288c62ffe302c9..9bb564495df34d 100644 --- a/tests/sentry/web/frontend/test_accounts.py +++ b/tests/sentry/web/frontend/test_accounts.py @@ -8,11 +8,11 @@ from django.utils import timezone from sentry.models.lostpasswordhash import LostPasswordHash -from sentry.models.useremail import UserEmail from sentry.organizations.services.organization import organization_service from sentry.testutils.cases import TestCase from sentry.testutils.helpers.task_runner import BurstTaskRunner from sentry.testutils.silo import control_silo_test +from sentry.users.models.useremail import UserEmail from sentry.web.frontend.accounts import recover_confirm diff --git a/tests/sentry/web/frontend/test_auth_login.py b/tests/sentry/web/frontend/test_auth_login.py index 3d19cf9f304138..da389dad3bc8e3 100644 --- a/tests/sentry/web/frontend/test_auth_login.py +++ b/tests/sentry/web/frontend/test_auth_login.py @@ -16,7 +16,6 @@ from sentry.models.authprovider import AuthProvider from sentry.models.organization import Organization from sentry.models.organizationmember import OrganizationMember -from sentry.models.user import User from sentry.newsletter.dummy import DummyNewsletter from sentry.receivers import create_default_projects from sentry.silo.base import SiloMode @@ -25,6 +24,7 @@ from sentry.testutils.helpers.features import with_feature from sentry.testutils.hybrid_cloud import HybridCloudTestMixin from sentry.testutils.silo import assume_test_silo_mode, control_silo_test +from sentry.users.models.user import User from sentry.utils import json diff --git a/tests/sentry/web/frontend/test_auth_organization_login.py b/tests/sentry/web/frontend/test_auth_organization_login.py index c58a0a29c61bdd..9a43adb72152ad 100644 --- a/tests/sentry/web/frontend/test_auth_organization_login.py +++ b/tests/sentry/web/frontend/test_auth_organization_login.py @@ -14,12 +14,12 @@ from sentry.models.options.organization_option import OrganizationOption from sentry.models.organization import OrganizationStatus from sentry.models.organizationmember import OrganizationMember -from sentry.models.useremail import UserEmail from sentry.organizations.services.organization.serial import serialize_rpc_organization from sentry.silo.base import SiloMode from sentry.testutils.cases import AuthProviderTestCase from sentry.testutils.helpers import with_feature from sentry.testutils.silo import assume_test_silo_mode, control_silo_test +from sentry.users.models.useremail import UserEmail from sentry.utils import json diff --git a/tests/sentry/web/frontend/test_error_page_embed.py b/tests/sentry/web/frontend/test_error_page_embed.py index 444d289d088270..15c6f76a846cf1 100644 --- a/tests/sentry/web/frontend/test_error_page_embed.py +++ b/tests/sentry/web/frontend/test_error_page_embed.py @@ -7,10 +7,10 @@ from django.urls import reverse from sentry.models.environment import Environment -from sentry.models.userreport import UserReport from sentry.testutils.cases import TestCase from sentry.testutils.helpers.datetime import before_now, iso_format from sentry.types.region import get_local_region +from sentry.users.models.userreport import UserReport @override_settings(ROOT_URLCONF="sentry.conf.urls") diff --git a/tests/sentry/web/frontend/test_organization_auth_settings.py b/tests/sentry/web/frontend/test_organization_auth_settings.py index afc03e719ce8c7..ea6a3e1622607f 100644 --- a/tests/sentry/web/frontend/test_organization_auth_settings.py +++ b/tests/sentry/web/frontend/test_organization_auth_settings.py @@ -26,7 +26,6 @@ from sentry.models.organization import Organization from sentry.models.organizationmember import OrganizationMember from sentry.models.team import Team -from sentry.models.user import User from sentry.organizations.services.organization import organization_service from sentry.signals import receivers_raise_on_send from sentry.silo.base import SiloMode @@ -34,6 +33,7 @@ from sentry.testutils.helpers.features import with_feature from sentry.testutils.outbox import outbox_runner from sentry.testutils.silo import assume_test_silo_mode, assume_test_silo_mode_of, control_silo_test +from sentry.users.models.user import User from sentry.web.frontend.organization_auth_settings import get_scim_url diff --git a/tests/snuba/api/endpoints/test_project_group_index.py b/tests/snuba/api/endpoints/test_project_group_index.py index 3fcc04c6e73ab6..a26c0806f3d7fc 100644 --- a/tests/snuba/api/endpoints/test_project_group_index.py +++ b/tests/snuba/api/endpoints/test_project_group_index.py @@ -25,7 +25,6 @@ from sentry.models.grouptombstone import GroupTombstone from sentry.models.integrations.external_issue import ExternalIssue from sentry.models.integrations.organization_integration import OrganizationIntegration -from sentry.models.options.user_option import UserOption from sentry.models.release import Release from sentry.silo.base import SiloMode from sentry.testutils.cases import APITestCase, SnubaTestCase @@ -33,6 +32,7 @@ from sentry.testutils.helpers.datetime import before_now, iso_format from sentry.testutils.silo import assume_test_silo_mode from sentry.types.activity import ActivityType +from sentry.users.models.user_option import UserOption from sentry.utils import json diff --git a/tests/snuba/api/serializers/test_group.py b/tests/snuba/api/serializers/test_group.py index 8dd0788e8bcb83..90fe986d0cffe0 100644 --- a/tests/snuba/api/serializers/test_group.py +++ b/tests/snuba/api/serializers/test_group.py @@ -14,13 +14,13 @@ from sentry.models.groupsnooze import GroupSnooze from sentry.models.groupsubscription import GroupSubscription from sentry.models.notificationsettingoption import NotificationSettingOption -from sentry.models.options.user_option import UserOption from sentry.notifications.types import NotificationSettingsOptionEnum from sentry.silo.base import SiloMode from sentry.testutils.cases import APITestCase, PerformanceIssueTestCase, SnubaTestCase from sentry.testutils.helpers.datetime import before_now, iso_format from sentry.testutils.silo import assume_test_silo_mode from sentry.types.group import PriorityLevel +from sentry.users.models.user_option import UserOption from sentry.utils.samples import load_data from tests.sentry.issues.test_utils import SearchIssueTestMixin diff --git a/tests/snuba/tasks/test_unmerge.py b/tests/snuba/tasks/test_unmerge.py index fe6487fefcf110..bbdb55f24fdbac 100644 --- a/tests/snuba/tasks/test_unmerge.py +++ b/tests/snuba/tasks/test_unmerge.py @@ -18,7 +18,6 @@ from sentry.models.grouphash import GroupHash from sentry.models.grouprelease import GroupRelease from sentry.models.release import Release -from sentry.models.userreport import UserReport from sentry.similarity import _make_index_backend, features from sentry.tasks.merge import merge_groups from sentry.tasks.unmerge import ( @@ -33,6 +32,7 @@ from sentry.testutils.helpers.datetime import before_now, iso_format from sentry.testutils.helpers.features import with_feature from sentry.tsdb.base import TSDBModel +from sentry.users.models.userreport import UserReport from sentry.utils import redis # Use the default redis client as a cluster client in the similarity index