Skip to content

Commit ccc4b07

Browse files
ref(control_silo): Move LostPasswordHash to users module (#76072)
Part of moving control silo user related resources into the users module. Includes adding of types for functions and an import shim for any getsentry refs. Apart of (#73856) --------- Co-authored-by: Mark Story <[email protected]>
1 parent 86ec4d5 commit ccc4b07

File tree

15 files changed

+129
-118
lines changed

15 files changed

+129
-118
lines changed

src/sentry/management/commands/generate_reset_password_link.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from django.core.management.base import BaseCommand
55
from django.utils import timezone
66

7-
from sentry.models.lostpasswordhash import LostPasswordHash
7+
from sentry.users.models.lostpasswordhash import LostPasswordHash
88
from sentry.utils.auth import find_users
99

1010

src/sentry/models/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from sentry.users.models.authenticator import * # NOQA
22
from sentry.users.models.email import * # NOQA
3+
from sentry.users.models.lostpasswordhash import * # NOQA
34
from sentry.users.models.user import * # NOQA
45
from sentry.users.models.userrole import * # NOQA
56

@@ -66,7 +67,6 @@
6667
from .importchunk import * # NOQA
6768
from .integrations import * # NOQA
6869
from .latestreporeleaseenvironment import * # NOQA
69-
from .lostpasswordhash import * # NOQA
7070
from .notificationmessage import * # NOQA
7171
from .notificationsettingoption import * # NOQA
7272
from .notificationsettingprovider import * # NOQA

src/sentry/models/lostpasswordhash.py

Lines changed: 2 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,3 @@
1-
from datetime import timedelta
2-
from typing import TYPE_CHECKING
1+
from sentry.users.models.lostpasswordhash import LostPasswordHash
32

4-
from django.conf import settings
5-
from django.db import models
6-
from django.urls import reverse
7-
from django.utils import timezone
8-
9-
from sentry.backup.scopes import RelocationScope
10-
from sentry.db.models import FlexibleForeignKey, Model, control_silo_model, sane_repr
11-
from sentry.utils.http import absolute_uri
12-
from sentry.utils.security import get_secure_token
13-
14-
if TYPE_CHECKING:
15-
from sentry.users.services.lost_password_hash import RpcLostPasswordHash
16-
17-
18-
@control_silo_model
19-
class LostPasswordHash(Model):
20-
__relocation_scope__ = RelocationScope.Excluded
21-
22-
user = FlexibleForeignKey(settings.AUTH_USER_MODEL, unique=True)
23-
hash = models.CharField(max_length=32)
24-
date_added = models.DateTimeField(default=timezone.now)
25-
26-
class Meta:
27-
app_label = "sentry"
28-
db_table = "sentry_lostpasswordhash"
29-
30-
__repr__ = sane_repr("user_id", "hash")
31-
32-
def save(self, *args, **kwargs):
33-
if not self.hash:
34-
self.set_hash()
35-
super().save(*args, **kwargs)
36-
37-
def set_hash(self) -> None:
38-
self.hash = get_secure_token()
39-
40-
def is_valid(self) -> bool:
41-
return self.date_added > timezone.now() - timedelta(hours=1)
42-
43-
@classmethod
44-
def send_recover_password_email(cls, user, hash, ip_address) -> None:
45-
extra = {
46-
"ip_address": ip_address,
47-
}
48-
cls._send_email("recover_password", user, hash, extra)
49-
50-
@classmethod
51-
def send_relocate_account_email(cls, user, hash, orgs) -> None:
52-
cls._send_email("relocate_account", user, hash, {"orgs": orgs})
53-
54-
@classmethod
55-
def _send_email(cls, mode, user, hash, extra) -> None:
56-
from sentry import options
57-
from sentry.http import get_server_hostname
58-
from sentry.utils.email import MessageBuilder
59-
60-
context = {
61-
"user": user,
62-
"domain": get_server_hostname(),
63-
"url": cls.get_lostpassword_url(user.id, hash, mode),
64-
"datetime": timezone.now(),
65-
**extra,
66-
}
67-
68-
subject = "Password Recovery"
69-
template = "recover_account"
70-
if mode == "set_password":
71-
template = "set_password"
72-
elif mode == "relocate_account":
73-
template = "relocate_account"
74-
subject = "Set Username and Password for Your Relocated Sentry.io Account"
75-
76-
msg = MessageBuilder(
77-
subject="{}{}".format(options.get("mail.subject-prefix"), subject),
78-
template=f"sentry/emails/{template}.txt",
79-
html_template=f"sentry/emails/{template}.html",
80-
type="user.password_recovery",
81-
context=context,
82-
)
83-
msg.send_async([user.email])
84-
85-
# Duplicated from RpcLostPasswordHash
86-
def get_absolute_url(self, mode: str = "recover") -> str:
87-
return LostPasswordHash.get_lostpassword_url(self.user_id, self.hash, mode)
88-
89-
@classmethod
90-
def get_lostpassword_url(self, user_id: int, hash: str, mode: str = "recover") -> str:
91-
url_key = "sentry-account-recover-confirm"
92-
if mode == "set_password":
93-
url_key = "sentry-account-set-password-confirm"
94-
elif mode == "relocate_account":
95-
url_key = "sentry-account-relocate-confirm"
96-
97-
return absolute_uri(reverse(url_key, args=[user_id, hash]))
98-
99-
@classmethod
100-
def for_user(cls, user) -> "RpcLostPasswordHash":
101-
from sentry.users.services.lost_password_hash import lost_password_hash_service
102-
103-
password_hash = lost_password_hash_service.get_or_create(user_id=user.id)
104-
return password_hash
3+
__all__ = ("LostPasswordHash",)

src/sentry/runner/commands/cleanup.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ def cleanup(
175175
from sentry.models.rulefirehistory import RuleFireHistory
176176
from sentry.monitors import models as monitor_models
177177
from sentry.replays import models as replay_models
178+
from sentry.users.models.lostpasswordhash import LostPasswordHash
178179
from sentry.utils import metrics
179180
from sentry.utils.query import RangeQuerySetWrapper
180181

@@ -234,10 +235,10 @@ def is_filtered(model: type[Model]) -> bool:
234235
]
235236

236237
debug_output("Removing expired values for LostPasswordHash")
237-
if is_filtered(models.LostPasswordHash):
238+
if is_filtered(LostPasswordHash):
238239
debug_output(">> Skipping LostPasswordHash")
239240
else:
240-
models.LostPasswordHash.objects.filter(
241+
LostPasswordHash.objects.filter(
241242
date_added__lte=timezone.now() - timedelta(hours=48)
242243
).delete()
243244

src/sentry/tasks/relocation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
from sentry.models.files.file import File
4444
from sentry.models.files.utils import get_relocation_storage
4545
from sentry.models.importchunk import ControlImportChunkReplica, RegionImportChunk
46-
from sentry.models.lostpasswordhash import LostPasswordHash as LostPasswordHash
4746
from sentry.models.organization import Organization, OrganizationStatus
4847
from sentry.models.organizationmember import OrganizationMember
4948
from sentry.models.relocation import (
@@ -62,6 +61,7 @@
6261
from sentry.silo.base import SiloMode
6362
from sentry.tasks.base import instrumented_task
6463
from sentry.types.region import get_local_region
64+
from sentry.users.models.lostpasswordhash import LostPasswordHash
6565
from sentry.users.models.user import User
6666
from sentry.users.services.lost_password_hash import lost_password_hash_service
6767
from sentry.users.services.user.service import user_service
@@ -1585,7 +1585,7 @@ def notifying_users(uuid: UUID) -> None:
15851585
for control_chunk in chunks:
15861586
imported_user_ids = imported_user_ids.union(set(control_chunk.inserted_map.values()))
15871587

1588-
imported_org_slugs: set[int] = set()
1588+
imported_org_slugs: set[str] = set()
15891589
for region_chunk in RegionImportChunk.objects.filter(
15901590
import_uuid=uuid_str, model="sentry.organization"
15911591
):
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Iterable
4+
from datetime import timedelta
5+
from typing import TYPE_CHECKING, Any
6+
7+
from django.conf import settings
8+
from django.db import models
9+
from django.urls import reverse
10+
from django.utils import timezone
11+
12+
from sentry.backup.scopes import RelocationScope
13+
from sentry.db.models import FlexibleForeignKey, Model, control_silo_model, sane_repr
14+
from sentry.users.services.user.model import RpcUser
15+
from sentry.utils.http import absolute_uri
16+
from sentry.utils.security import get_secure_token
17+
18+
if TYPE_CHECKING:
19+
from sentry.models.user import User
20+
from sentry.users.services.lost_password_hash import RpcLostPasswordHash
21+
22+
23+
@control_silo_model
24+
class LostPasswordHash(Model):
25+
__relocation_scope__ = RelocationScope.Excluded
26+
27+
user = FlexibleForeignKey(settings.AUTH_USER_MODEL, unique=True)
28+
hash = models.CharField(max_length=32)
29+
date_added = models.DateTimeField(default=timezone.now)
30+
31+
class Meta:
32+
app_label = "sentry"
33+
db_table = "sentry_lostpasswordhash"
34+
35+
__repr__ = sane_repr("user_id", "hash")
36+
37+
def save(self, *args: Any, **kwargs: Any) -> None:
38+
if not self.hash:
39+
self.set_hash()
40+
super().save(*args, **kwargs)
41+
42+
def set_hash(self) -> None:
43+
self.hash = get_secure_token()
44+
45+
def is_valid(self) -> bool:
46+
return self.date_added > timezone.now() - timedelta(hours=1)
47+
48+
@classmethod
49+
def send_recover_password_email(cls, user: User, hash: str, ip_address: str) -> None:
50+
extra = {
51+
"ip_address": ip_address,
52+
}
53+
cls._send_email("recover_password", user, hash, extra)
54+
55+
@classmethod
56+
def send_relocate_account_email(
57+
cls, user: User | RpcUser, hash: str, orgs: Iterable[str]
58+
) -> None:
59+
cls._send_email("relocate_account", user, hash, {"orgs": orgs})
60+
61+
@classmethod
62+
def _send_email(cls, mode: str, user: User | RpcUser, hash: str, extra: dict[str, Any]) -> None:
63+
from sentry import options
64+
from sentry.http import get_server_hostname
65+
from sentry.utils.email import MessageBuilder
66+
67+
context = {
68+
"user": user,
69+
"domain": get_server_hostname(),
70+
"url": cls.get_lostpassword_url(user.id, hash, mode),
71+
"datetime": timezone.now(),
72+
**extra,
73+
}
74+
75+
subject = "Password Recovery"
76+
template = "recover_account"
77+
if mode == "set_password":
78+
template = "set_password"
79+
elif mode == "relocate_account":
80+
template = "relocate_account"
81+
subject = "Set Username and Password for Your Relocated Sentry.io Account"
82+
83+
msg = MessageBuilder(
84+
subject="{}{}".format(options.get("mail.subject-prefix"), subject),
85+
template=f"sentry/emails/{template}.txt",
86+
html_template=f"sentry/emails/{template}.html",
87+
type="user.password_recovery",
88+
context=context,
89+
)
90+
msg.send_async([user.email])
91+
92+
# Duplicated from RpcLostPasswordHash
93+
def get_absolute_url(self, mode: str = "recover") -> str:
94+
return LostPasswordHash.get_lostpassword_url(self.user_id, self.hash, mode)
95+
96+
@classmethod
97+
def get_lostpassword_url(self, user_id: int, hash: str, mode: str = "recover") -> str:
98+
url_key = "sentry-account-recover-confirm"
99+
if mode == "set_password":
100+
url_key = "sentry-account-set-password-confirm"
101+
elif mode == "relocate_account":
102+
url_key = "sentry-account-relocate-confirm"
103+
104+
return absolute_uri(reverse(url_key, args=[user_id, hash]))
105+
106+
@classmethod
107+
def for_user(cls, user: User) -> RpcLostPasswordHash:
108+
from sentry.users.services.lost_password_hash import lost_password_hash_service
109+
110+
password_hash = lost_password_hash_service.get_or_create(user_id=user.id)
111+
return password_hash

src/sentry/users/services/lost_password_hash/impl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import datetime
22

3-
from sentry.models.lostpasswordhash import LostPasswordHash
3+
from sentry.users.models.lostpasswordhash import LostPasswordHash
44
from sentry.users.services.lost_password_hash import LostPasswordHashService, RpcLostPasswordHash
55
from sentry.users.services.lost_password_hash.serial import serialize_lostpasswordhash
66

src/sentry/users/services/lost_password_hash/model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from pydantic import Field
1010

1111
from sentry.hybridcloud.rpc import RpcModel
12-
from sentry.models.lostpasswordhash import LostPasswordHash
12+
from sentry.users.models.lostpasswordhash import LostPasswordHash
1313

1414

1515
class RpcLostPasswordHash(RpcModel):

src/sentry/users/services/lost_password_hash/serial.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from sentry.models.lostpasswordhash import LostPasswordHash
1+
from sentry.users.models.lostpasswordhash import LostPasswordHash
22
from sentry.users.services.lost_password_hash import RpcLostPasswordHash
33

44

src/sentry/web/frontend/accounts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010
from django.utils.translation import gettext as _
1111
from django.views.decorators.http import require_http_methods
1212

13-
from sentry.models.lostpasswordhash import LostPasswordHash
1413
from sentry.models.organizationmapping import OrganizationMapping
1514
from sentry.models.organizationmembermapping import OrganizationMemberMapping
1615
from sentry.models.useremail import UserEmail
1716
from sentry.organizations.services.organization import organization_service
1817
from sentry.security.utils import capture_security_activity
1918
from sentry.signals import email_verified, terms_accepted
2019
from sentry.silo.base import control_silo_function
20+
from sentry.users.models.lostpasswordhash import LostPasswordHash
2121
from sentry.users.models.user import User
2222
from sentry.users.services.lost_password_hash import lost_password_hash_service
2323
from sentry.users.services.user.service import user_service

src/sentry/web/frontend/debug/mail.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
from sentry.mail.notifications import get_builder_args
3737
from sentry.models.activity import Activity
3838
from sentry.models.group import Group, GroupStatus
39-
from sentry.models.lostpasswordhash import LostPasswordHash
4039
from sentry.models.organization import Organization
4140
from sentry.models.organizationmember import OrganizationMember
4241
from sentry.models.project import Project
@@ -61,6 +60,7 @@
6160
)
6261
from sentry.types.actor import Actor
6362
from sentry.types.group import GroupSubStatus
63+
from sentry.users.models.lostpasswordhash import LostPasswordHash
6464
from sentry.utils import json, loremipsum
6565
from sentry.utils.auth import AuthenticatedHttpRequest
6666
from sentry.utils.dates import to_datetime

tests/sentry/backup/test_imports.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
ControlImportChunkReplica,
4040
RegionImportChunk,
4141
)
42-
from sentry.models.lostpasswordhash import LostPasswordHash
4342
from sentry.models.options.option import ControlOption, Option
4443
from sentry.models.options.project_option import ProjectOption
4544
from sentry.models.options.user_option import UserOption
@@ -79,6 +78,7 @@
7978
from sentry.testutils.silo import assume_test_silo_mode
8079
from sentry.users.models.authenticator import Authenticator
8180
from sentry.users.models.email import Email
81+
from sentry.users.models.lostpasswordhash import LostPasswordHash
8282
from sentry.users.models.user import User
8383
from sentry.users.models.userrole import UserRole, UserRoleUser
8484
from tests.sentry.backup import (

tests/sentry/tasks/test_relocation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@
6464
MAX_FAST_TASK_RETRIES,
6565
MAX_VALIDATION_POLL_ATTEMPTS,
6666
MAX_VALIDATION_POLLS,
67-
LostPasswordHash,
6867
completed,
6968
importing,
7069
notifying_owner,
@@ -92,6 +91,7 @@
9291
)
9392
from sentry.testutils.outbox import outbox_runner
9493
from sentry.testutils.silo import assume_test_silo_mode, create_test_regions, region_silo_test
94+
from sentry.users.models.lostpasswordhash import LostPasswordHash
9595
from sentry.users.models.user import User
9696
from sentry.utils import json
9797
from sentry.utils.relocation import RELOCATION_BLOB_SIZE, RELOCATION_FILE_TYPE, OrderedTask

tests/sentry/models/test_lostpasswordhash.py renamed to tests/sentry/users/models/test_lostpasswordhash.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from django.core import mail
22
from django.urls import reverse
33

4-
from sentry.models.lostpasswordhash import LostPasswordHash
54
from sentry.testutils.cases import TestCase
65
from sentry.testutils.silo import control_silo_test
6+
from sentry.users.models.lostpasswordhash import LostPasswordHash
77

88

99
@control_silo_test

0 commit comments

Comments
 (0)