diff --git a/src/sentry/api/endpoints/api_tokens.py b/src/sentry/api/endpoints/api_tokens.py index fb066218b5b996..d683fd62b951f4 100644 --- a/src/sentry/api/endpoints/api_tokens.py +++ b/src/sentry/api/endpoints/api_tokens.py @@ -19,6 +19,7 @@ from sentry.models.apitoken import ApiToken from sentry.models.outbox import outbox_context from sentry.security.utils import capture_security_activity +from sentry.types.token import AuthTokenType class ApiTokenSerializer(serializers.Serializer): @@ -78,8 +79,8 @@ def post(self, request: Request) -> Response: token = ApiToken.objects.create( user_id=request.user.id, name=result.get("name", None), + token_type=AuthTokenType.USER, scope_list=result["scopes"], - refresh_token=None, expires_at=None, ) diff --git a/src/sentry/api/serializers/models/apitoken.py b/src/sentry/api/serializers/models/apitoken.py index a58fb4ee7327bb..13d206a8feb86c 100644 --- a/src/sentry/api/serializers/models/apitoken.py +++ b/src/sentry/api/serializers/models/apitoken.py @@ -1,5 +1,6 @@ from sentry.api.serializers import Serializer, register, serialize from sentry.models.apitoken import ApiToken +from sentry.types.token import AuthTokenType @register(ApiToken) @@ -30,9 +31,10 @@ def serialize(self, obj, attrs, user, **kwargs): if not attrs["application"]: include_token = kwargs.get("include_token", True) if include_token: - data["token"] = obj.token + data["token"] = obj.plaintext_token - data["refreshToken"] = obj.refresh_token + if not obj.token_type == AuthTokenType.USER: + data["refreshToken"] = obj.plaintext_refresh_token """ While this is a nullable column at the db level, this should never be empty. If it is, it's a sign that the diff --git a/src/sentry/models/apitoken.py b/src/sentry/models/apitoken.py index 1c66823f020496..fef4baaed74120 100644 --- a/src/sentry/models/apitoken.py +++ b/src/sentry/models/apitoken.py @@ -1,5 +1,6 @@ from __future__ import annotations +import hashlib import secrets from collections.abc import Collection from datetime import timedelta @@ -22,16 +23,82 @@ from sentry.types.token import AuthTokenType DEFAULT_EXPIRATION = timedelta(days=30) +TOKEN_REDACTED = "***REDACTED***" def default_expiration(): return timezone.now() + DEFAULT_EXPIRATION -def generate_token(): +def generate_token(token_type: AuthTokenType | str | None = AuthTokenType.__empty__) -> str: + if token_type: + return f"{token_type}{secrets.token_hex(nbytes=32)}" + return secrets.token_hex(nbytes=32) +class PlaintextSecretAlreadyRead(Exception): + """the secret you are trying to read is read-once and cannot be accessed directly again""" + + pass + + +class NotSupported(Exception): + """the method you called is not supported by this token type""" + + pass + + +class ApiTokenManager(ControlOutboxProducingManager): + def create(self, *args, **kwargs): + token_type: AuthTokenType | None = kwargs.get("token_type", None) + + # Typically the .create() method is called with `refresh_token=None` as an + # argument when we specifically do not want a refresh_token. + # + # But if it is not None or not specified, we should generate a token since + # that is the expected behavior... the refresh_token field on ApiToken has + # a default of generate_token() + # + # TODO(mdtro): All of these if/else statements will be cleaned up at a later time + # to use a match statment on the AuthTokenType. Move each of the various token type + # create calls one at a time. + if "refresh_token" in kwargs: + plaintext_refresh_token = kwargs["refresh_token"] + else: + plaintext_refresh_token = generate_token() + + if token_type == AuthTokenType.USER: + plaintext_token = generate_token(token_type=AuthTokenType.USER) + plaintext_refresh_token = None # user auth tokens do not have refresh tokens + else: + # to maintain compatibility with current + # code that currently calls create with token= specified + if "token" in kwargs: + plaintext_token = kwargs["token"] + else: + plaintext_token = generate_token() + + if options.get("apitoken.save-hash-on-create"): + kwargs["hashed_token"] = hashlib.sha256(plaintext_token.encode()).hexdigest() + + if plaintext_refresh_token: + kwargs["hashed_refresh_token"] = hashlib.sha256( + plaintext_refresh_token.encode() + ).hexdigest() + + kwargs["token"] = plaintext_token + kwargs["refresh_token"] = plaintext_refresh_token + + api_token = super().create(*args, **kwargs) + + # Store the plaintext tokens for one-time retrieval + api_token._set_plaintext_token(token=plaintext_token) + api_token._set_plaintext_refresh_token(token=plaintext_refresh_token) + + return api_token + + @control_silo_only_model class ApiToken(ReplicatedControlModel, HasApiScopes): __relocation_scope__ = {RelocationScope.Global, RelocationScope.Config} @@ -50,7 +117,7 @@ class ApiToken(ReplicatedControlModel, HasApiScopes): expires_at = models.DateTimeField(null=True, default=default_expiration) date_added = models.DateTimeField(default=timezone.now) - objects: ClassVar[ControlOutboxProducingManager[ApiToken]] = ControlOutboxProducingManager( + objects: ClassVar[ControlOutboxProducingManager[ApiToken]] = ApiTokenManager( cache_fields=("token",) ) @@ -63,12 +130,117 @@ class Meta: def __str__(self): return force_str(self.token) + def _set_plaintext_token(self, token: str) -> None: + """Set the plaintext token for one-time reading + This function should only be called from the model's + manager class. + + :param token: A plaintext string of the token + :raises PlaintextSecretAlreadyRead: when the token has already been read once + """ + existing_token: str | None = None + try: + existing_token = self.__plaintext_token + except AttributeError: + self.__plaintext_token: str = token + + if existing_token == TOKEN_REDACTED: + raise PlaintextSecretAlreadyRead() + + def _set_plaintext_refresh_token(self, token: str) -> None: + """Set the plaintext refresh token for one-time reading + This function should only be called from the model's + manager class. + + :param token: A plaintext string of the refresh token + :raises PlaintextSecretAlreadyRead: if the token has already been read once + """ + existing_refresh_token: str | None = None + try: + existing_refresh_token = self.__plaintext_refresh_token + except AttributeError: + self.__plaintext_refresh_token: str = token + + if existing_refresh_token == TOKEN_REDACTED: + raise PlaintextSecretAlreadyRead() + + @property + def plaintext_token(self) -> str: + """The plaintext value of the token + To be called immediately after creation of a new `ApiToken` to return the + plaintext token to the user. After reading the token, the plaintext token + string will be set to `TOKEN_REDACTED` to prevent future accidental leaking + of the token in logs, exceptions, etc. + + :raises PlaintextSecretAlreadyRead: if the token has already been read once + :return: the plaintext value of the token + """ + token = self.__plaintext_token + if token == TOKEN_REDACTED: + raise PlaintextSecretAlreadyRead() + + self.__plaintext_token = TOKEN_REDACTED + + return token + + @property + def plaintext_refresh_token(self) -> str: + """The plaintext value of the refresh token + To be called immediately after creation of a new `ApiToken` to return the + plaintext token to the user. After reading the token, the plaintext token + string will be set to `TOKEN_REDACTED` to prevent future accidental leaking + of the token in logs, exceptions, etc. + + :raises PlaintextSecretAlreadyRead: if the refresh token has already been read once + :raises NotSupported: if called on a User Auth Token + :return: the plaintext value of the refresh token + """ + if not self.refresh_token and not self.hashed_refresh_token: + raise NotSupported("This API token type does not support refresh tokens") + + token = self.__plaintext_refresh_token + if token == TOKEN_REDACTED: + raise PlaintextSecretAlreadyRead() + + self.__plaintext_refresh_token = TOKEN_REDACTED + + return token + def save(self, *args: Any, **kwargs: Any) -> None: + if options.get("apitoken.save-hash-on-create"): + self.hashed_token = hashlib.sha256(self.token.encode()).hexdigest() + + if self.refresh_token: + self.hashed_refresh_token = hashlib.sha256(self.refresh_token.encode()).hexdigest() + else: + # The backup tests create a token with a refresh_token and then clear it out. + # So if the refresh_token is None, wipe out any hashed value that may exist too. + # https://github.com/getsentry/sentry/blob/1fc699564e79c62bff6cc3c168a49bfceadcac52/tests/sentry/backup/test_imports.py#L1306 + self.hashed_refresh_token = None + if options.get("apitoken.auto-add-last-chars"): token_last_characters = self.token[-4:] self.token_last_characters = token_last_characters - return super().save(**kwargs) + return super().save(*args, **kwargs) + + def update(self, *args: Any, **kwargs: Any) -> int: + # if the token or refresh_token was updated, we need to + # re-calculate the hashed values + if options.get("apitoken.save-hash-on-create"): + if "token" in kwargs: + kwargs["hashed_token"] = hashlib.sha256(kwargs["token"].encode()).hexdigest() + + if "refresh_token" in kwargs: + kwargs["hashed_refresh_token"] = hashlib.sha256( + kwargs["refresh_token"].encode() + ).hexdigest() + + if options.get("apitoken.auto-add-last-chars"): + if "token" in kwargs: + kwargs["token_last_characters"] = kwargs["token"][-4:] + + return super().update(*args, **kwargs) def outbox_region_names(self) -> Collection[str]: return list(find_all_region_names()) @@ -104,10 +276,16 @@ def get_allowed_origins(self): return () def refresh(self, expires_at=None): + if self.token_type == AuthTokenType.USER: + raise NotSupported("User auth tokens do not support refreshing the token") + if expires_at is None: expires_at = timezone.now() + DEFAULT_EXPIRATION - self.update(token=generate_token(), refresh_token=generate_token(), expires_at=expires_at) + new_token = generate_token(token_type=self.token_type) + new_refresh_token = generate_token(token_type=self.token_type) + + self.update(token=new_token, refresh_token=new_refresh_token, expires_at=expires_at) def get_relocation_scope(self) -> RelocationScope: if self.application_id is not None: @@ -125,9 +303,9 @@ def write_relocation_import( ) existing = self.__class__.objects.filter(query).first() if existing: - self.token = generate_token() + self.token = generate_token(token_type=self.token_type) if self.refresh_token is not None: - self.refresh_token = generate_token() + self.refresh_token = generate_token(token_type=self.token_type) if self.expires_at is not None: self.expires_at = timezone.now() + DEFAULT_EXPIRATION diff --git a/src/sentry/testutils/factories.py b/src/sentry/testutils/factories.py index 3201b60fbb14cc..12d47d3d6c487b 100644 --- a/src/sentry/testutils/factories.py +++ b/src/sentry/testutils/factories.py @@ -148,6 +148,7 @@ from sentry.types.activity import ActivityType from sentry.types.integrations import ExternalProviders from sentry.types.region import Region, get_local_region, get_region_by_name +from sentry.types.token import AuthTokenType from sentry.utils import json, loremipsum from sentry.utils.performance_issues.performance_problem import PerformanceProblem from social_auth.models import UserSocialAuth @@ -423,6 +424,7 @@ def create_user_auth_token(user, scope_list: list[str] | None = None, **kwargs) return ApiToken.objects.create( user=user, scope_list=scope_list, + token_type=AuthTokenType.USER, **kwargs, ) diff --git a/src/sentry/testutils/helpers/backups.py b/src/sentry/testutils/helpers/backups.py index ce2a6e4ea11fdd..581e34f1e28135 100644 --- a/src/sentry/testutils/helpers/backups.py +++ b/src/sentry/testutils/helpers/backups.py @@ -99,6 +99,7 @@ from sentry.testutils.cases import TransactionTestCase from sentry.testutils.factories import get_fixture_path from sentry.testutils.silo import assume_test_silo_mode +from sentry.types.token import AuthTokenType from sentry.utils import json from sentry.utils.json import JSONData @@ -632,7 +633,10 @@ def create_exhaustive_global_configs(self, owner: User): ControlOption.objects.create(key="bar", value="b") ApiAuthorization.objects.create(user=owner) ApiToken.objects.create( - user=owner, expires_at=None, name="create_exhaustive_global_configs" + user=owner, + expires_at=None, + name="create_exhaustive_global_configs", + token_type=AuthTokenType.USER, ) @assume_test_silo_mode(SiloMode.REGION) diff --git a/src/sentry/web/frontend/setup_wizard.py b/src/sentry/web/frontend/setup_wizard.py index a3e41c63fbb28d..4d8cc581ef29e8 100644 --- a/src/sentry/web/frontend/setup_wizard.py +++ b/src/sentry/web/frontend/setup_wizard.py @@ -24,6 +24,7 @@ from sentry.services.hybrid_cloud.project_key.model import ProjectKeyRole from sentry.services.hybrid_cloud.project_key.service import project_key_service from sentry.services.hybrid_cloud.user.model import RpcUser +from sentry.types.token import AuthTokenType from sentry.utils.http import absolute_uri from sentry.utils.security.orgauthtoken_token import ( SystemUrlPrefixMissingException, @@ -159,7 +160,7 @@ def get_token(mappings: list[OrganizationMapping], user: RpcUser): token = ApiToken.objects.create( user_id=user.id, scope_list=["project:releases"], - refresh_token=None, + token_type=AuthTokenType.USER, expires_at=None, ) return serialize(token) diff --git a/tests/sentry/api/serializers/test_apitoken.py b/tests/sentry/api/serializers/test_apitoken.py index 29d9827a283e1a..5ba411da47f194 100644 --- a/tests/sentry/api/serializers/test_apitoken.py +++ b/tests/sentry/api/serializers/test_apitoken.py @@ -1,5 +1,9 @@ from sentry.api.serializers import ApiTokenSerializer +from sentry.models.apitoken import ApiToken +from sentry.silo.base import SiloMode from sentry.testutils.cases import TestCase +from sentry.testutils.helpers.options import override_options +from sentry.testutils.silo import assume_test_silo_mode class TestApiTokenSerializer(TestCase): @@ -38,6 +42,33 @@ def test_when_flag_is_false(self) -> None: assert "token" not in serialized_object +class TestRefreshTokens(TestApiTokenSerializer): + def setUp(self) -> None: + super().setUp() + attrs = self._serializer.get_attrs(item_list=[self._token], user=self._user) + attrs["application"] = None + self._attrs = attrs + + def test_no_refresh_token_on_user_token(self) -> None: + serialized_object = self._serializer.serialize( + obj=self._token, user=self._user, attrs=self._attrs + ) + + assert "refreshToken" not in serialized_object + + @override_options({"apitoken.save-hash-on-create": True}) + def test_refresh_token_on_non_user_token(self) -> None: + with assume_test_silo_mode(SiloMode.CONTROL): + token = ApiToken.objects.create(user=self._user) + assert token.hashed_refresh_token is not None + + serialized_object = self._serializer.serialize( + obj=token, user=self._user, attrs=self._attrs + ) + + assert "refreshToken" in serialized_object + + class TestLastTokenCharacters(TestApiTokenSerializer): def test_field_is_returned(self) -> None: attrs = self._serializer.get_attrs(item_list=[self._token], user=self._user) diff --git a/tests/sentry/api/test_authentication.py b/tests/sentry/api/test_authentication.py index 698a8b8fe93961..55fb5dc39289db 100644 --- a/tests/sentry/api/test_authentication.py +++ b/tests/sentry/api/test_authentication.py @@ -181,11 +181,11 @@ def setUp(self): self.auth = UserAuthTokenAuthentication() self.org = self.create_organization(owner=self.user) - self.token = "abc123" self.api_token = ApiToken.objects.create( - token=self.token, + token_type=AuthTokenType.USER, user=self.user, ) + self.token = self.api_token.plaintext_token def test_authenticate(self): request = HttpRequest() diff --git a/tests/sentry/backup/snapshots/ReleaseTests/test_at_head.pysnap b/tests/sentry/backup/snapshots/ReleaseTests/test_at_head.pysnap index dbbd91c64af06b..d5b6536501e91f 100644 --- a/tests/sentry/backup/snapshots/ReleaseTests/test_at_head.pysnap +++ b/tests/sentry/backup/snapshots/ReleaseTests/test_at_head.pysnap @@ -1,18 +1,18 @@ --- -created: '2024-04-16T17:43:29.734524+00:00' +created: '2024-04-17T21:06:30.772376+00:00' creator: sentry source: tests/sentry/backup/test_releases.py --- - fields: key: bar - last_updated: '2024-04-16T17:43:29.423Z' + last_updated: '2024-04-17T21:06:30.398Z' last_updated_by: unknown value: '"b"' model: sentry.controloption pk: 1 - fields: - date_added: '2024-04-16T17:43:28.921Z' - date_updated: '2024-04-16T17:43:28.921Z' + date_added: '2024-04-17T21:06:29.693Z' + date_updated: '2024-04-17T21:06:29.693Z' external_id: slack:test-org metadata: {} name: Slack for test-org @@ -22,13 +22,13 @@ source: tests/sentry/backup/test_releases.py pk: 1 - fields: key: foo - last_updated: '2024-04-16T17:43:29.422Z' + last_updated: '2024-04-17T21:06:30.396Z' last_updated_by: unknown value: '"a"' model: sentry.option pk: 1 - fields: - date_added: '2024-04-16T17:43:28.567Z' + date_added: '2024-04-17T21:06:29.038Z' default_role: member flags: '1' is_test: false @@ -36,92 +36,92 @@ source: tests/sentry/backup/test_releases.py slug: test-org status: 0 model: sentry.organization - pk: 4553845489270784 + pk: 4553851949875200 - fields: - date_added: '2024-04-16T17:43:29.090Z' + date_added: '2024-04-17T21:06:29.916Z' default_role: member flags: '1' is_test: false - name: Tough Sunbird - slug: tough-sunbird + name: Top Bunny + slug: top-bunny status: 0 model: sentry.organization - pk: 4553845489336320 + pk: 4553851949875204 - fields: config: hello: hello - date_added: '2024-04-16T17:43:28.922Z' - date_updated: '2024-04-16T17:43:28.922Z' + date_added: '2024-04-17T21:06:29.696Z' + date_updated: '2024-04-17T21:06:29.696Z' default_auth_id: null grace_period_end: null integration: 1 - organization_id: 4553845489270784 + organization_id: 4553851949875200 status: 0 model: sentry.organizationintegration pk: 1 - fields: key: sentry:account-rate-limit - organization: 4553845489270784 + organization: 4553851949875200 value: 0 model: sentry.organizationoption pk: 1 - fields: - date_added: '2024-04-16T17:43:28.781Z' + date_added: '2024-04-17T21:06:29.389Z' first_event: null flags: '10' forced_color: null name: project-test-org - organization: 4553845489270784 + organization: 4553851949875200 platform: null public: false slug: project-test-org status: 0 model: sentry.project - pk: 4553845489270786 + pk: 4553851949875202 - fields: - date_added: '2024-04-16T17:43:28.949Z' + date_added: '2024-04-17T21:06:29.746Z' first_event: null flags: '10' forced_color: null name: other-project-test-org - organization: 4553845489270784 + organization: 4553851949875200 platform: null public: false slug: other-project-test-org status: 0 model: sentry.project - pk: 4553845489270787 + pk: 4553851949875203 - fields: - date_added: '2024-04-16T17:43:29.165Z' + date_added: '2024-04-17T21:06:30.166Z' first_event: null flags: '10' forced_color: null - name: Flexible Jennet - organization: 4553845489270784 + name: Together Toad + organization: 4553851949875200 platform: null public: false - slug: flexible-jennet + slug: together-toad status: 0 model: sentry.project - pk: 4553845489336321 + pk: 4553851949940736 - fields: - date_added: '2024-04-16T17:43:29.370Z' + date_added: '2024-04-17T21:06:30.355Z' first_event: null flags: '10' forced_color: null - name: Tough Haddock - organization: 4553845489270784 + name: Internal Seagull + organization: 4553851949875200 platform: null public: false - slug: tough-haddock + slug: internal-seagull status: 0 model: sentry.project - pk: 4553845489336322 + pk: 4553851949940737 - fields: config: hello: hello integration_id: 1 - project: 4553845489270786 + project: 4553851949875202 model: sentry.projectintegration pk: 1 - fields: @@ -129,14 +129,14 @@ source: tests/sentry/backup/test_releases.py dynamicSdkLoaderOptions: hasPerformance: true hasReplay: true - date_added: '2024-04-16T17:43:28.799Z' + date_added: '2024-04-17T21:06:29.431Z' label: Default - project: 4553845489270786 - public_key: e51aaf465d943edc5438b82ddc6d4ac5 + project: 4553851949875202 + public_key: f712990d1a63ad31e91d17a0a9155c37 rate_limit_count: null rate_limit_window: null roles: '1' - secret_key: 054b20f8d02c8b7b33610bc9b0491f25 + secret_key: 03203633548f44e3589981687fee994a status: 0 use_case: user model: sentry.projectkey @@ -146,14 +146,14 @@ source: tests/sentry/backup/test_releases.py dynamicSdkLoaderOptions: hasPerformance: true hasReplay: true - date_added: '2024-04-16T17:43:28.967Z' + date_added: '2024-04-17T21:06:29.772Z' label: Default - project: 4553845489270787 - public_key: 203e38ae36bade3a141fc226f5fbfd88 + project: 4553851949875203 + public_key: fe741a21769abf8b1a2e316b2b5e1e1e rate_limit_count: null rate_limit_window: null roles: '1' - secret_key: 827503dffbb7558c41b937b794e33f06 + secret_key: e1038d757b79a3a0bb07f6ae33101978 status: 0 use_case: user model: sentry.projectkey @@ -163,14 +163,14 @@ source: tests/sentry/backup/test_releases.py dynamicSdkLoaderOptions: hasPerformance: true hasReplay: true - date_added: '2024-04-16T17:43:29.187Z' + date_added: '2024-04-17T21:06:30.191Z' label: Default - project: 4553845489336321 - public_key: d76ecbbf3bf5720759b08e8c289b93ca + project: 4553851949940736 + public_key: d5c71d1927bd46628a2dafbeb397084e rate_limit_count: null rate_limit_window: null roles: '1' - secret_key: e5ee0e5c06ef4d2fa97d8b84badd36e8 + secret_key: 12f0766363d0b24e9c78145ad30fa8cd status: 0 use_case: user model: sentry.projectkey @@ -180,98 +180,98 @@ source: tests/sentry/backup/test_releases.py dynamicSdkLoaderOptions: hasPerformance: true hasReplay: true - date_added: '2024-04-16T17:43:29.391Z' + date_added: '2024-04-17T21:06:30.371Z' label: Default - project: 4553845489336322 - public_key: 16b8c8b0b638785eb3b0e272749a68e2 + project: 4553851949940737 + public_key: fe7663de18191d4123430e189a680b27 rate_limit_count: null rate_limit_window: null roles: '1' - secret_key: 67deff46bc489908bdd566fd62cf8472 + secret_key: 0ba52c5a25962b650189ab7ce9d8fb17 status: 0 use_case: user model: sentry.projectkey pk: 4 - fields: key: sentry:relay-rev - project: 4553845489270786 - value: '"574d3243e61642b98945f15cae65d407"' + project: 4553851949875202 + value: '"b7a64ae43df84c8bb641b94e278b7ee4"' model: sentry.projectoption pk: 1 - fields: key: sentry:relay-rev-lastchange - project: 4553845489270786 - value: '"2024-04-16T17:43:28.805362Z"' + project: 4553851949875202 + value: '"2024-04-17T21:06:29.441365Z"' model: sentry.projectoption pk: 2 - fields: key: sentry:option-epoch - project: 4553845489270786 + project: 4553851949875202 value: 12 model: sentry.projectoption pk: 3 - fields: key: sentry:relay-rev - project: 4553845489270787 - value: '"4c415b18b2644dab9b0eff619a2c8e8e"' + project: 4553851949875203 + value: '"ba31b7dd0fd547099377347b0a58548e"' model: sentry.projectoption pk: 4 - fields: key: sentry:relay-rev-lastchange - project: 4553845489270787 - value: '"2024-04-16T17:43:28.972657Z"' + project: 4553851949875203 + value: '"2024-04-17T21:06:29.784791Z"' model: sentry.projectoption pk: 5 - fields: key: sentry:option-epoch - project: 4553845489270787 + project: 4553851949875203 value: 12 model: sentry.projectoption pk: 6 - fields: key: sentry:relay-rev - project: 4553845489336321 - value: '"5919387f5ea443559ad40dc3c28d3b74"' + project: 4553851949940736 + value: '"f1860f216dd341b49c5ab14ac86b5cde"' model: sentry.projectoption pk: 7 - fields: key: sentry:relay-rev-lastchange - project: 4553845489336321 - value: '"2024-04-16T17:43:29.192779Z"' + project: 4553851949940736 + value: '"2024-04-17T21:06:30.195868Z"' model: sentry.projectoption pk: 8 - fields: key: sentry:option-epoch - project: 4553845489336321 + project: 4553851949940736 value: 12 model: sentry.projectoption pk: 9 - fields: key: sentry:relay-rev - project: 4553845489336322 - value: '"4545fee2399f4853af32c69cc503cff1"' + project: 4553851949940737 + value: '"4619cac6547146ac9ff367e8ccf78f71"' model: sentry.projectoption pk: 10 - fields: key: sentry:relay-rev-lastchange - project: 4553845489336322 - value: '"2024-04-16T17:43:29.396777Z"' + project: 4553851949940737 + value: '"2024-04-17T21:06:30.375987Z"' model: sentry.projectoption pk: 11 - fields: key: sentry:option-epoch - project: 4553845489336322 + project: 4553851949940737 value: 12 model: sentry.projectoption pk: 12 - fields: auto_assignment: true codeowners_auto_sync: true - date_created: '2024-04-16T17:43:28.820Z' + date_created: '2024-04-17T21:06:29.471Z' fallthrough: true is_active: true - last_updated: '2024-04-16T17:43:28.820Z' - project: 4553845489270786 + last_updated: '2024-04-17T21:06:29.471Z' + project: 4553851949875202 raw: '{"hello":"hello"}' schema: hello: hello @@ -279,9 +279,9 @@ source: tests/sentry/backup/test_releases.py model: sentry.projectownership pk: 1 - fields: - date_added: '2024-04-16T17:43:28.826Z' - organization: 4553845489270784 - project: 4553845489270786 + date_added: '2024-04-17T21:06:29.480Z' + organization: 4553851949875200 + project: 4553851949875202 redirect_slug: project_slug_in_test-org model: sentry.projectredirect pk: 1 @@ -289,26 +289,26 @@ source: tests/sentry/backup/test_releases.py first_seen: null is_internal: true last_seen: null - public_key: vgWCiyOgUdY27XPzW_MdF6IiDKAQE7NCCTxhwWuJex8 - relay_id: 544863fd-3bce-4967-91c0-3cd599a7a20e + public_key: 0KCttIVNPgxRmRhV5wguFY-ezRRjRfwxzaD2DUkBFxg + relay_id: f283b102-0968-4497-85da-d38184ec1533 model: sentry.relay pk: 1 - fields: - first_seen: '2024-04-16T17:43:29.421Z' - last_seen: '2024-04-16T17:43:29.421Z' - public_key: vgWCiyOgUdY27XPzW_MdF6IiDKAQE7NCCTxhwWuJex8 - relay_id: 544863fd-3bce-4967-91c0-3cd599a7a20e + first_seen: '2024-04-17T21:06:30.395Z' + last_seen: '2024-04-17T21:06:30.395Z' + public_key: 0KCttIVNPgxRmRhV5wguFY-ezRRjRfwxzaD2DUkBFxg + relay_id: f283b102-0968-4497-85da-d38184ec1533 version: 0.0.1 model: sentry.relayusage pk: 1 - fields: config: {} - date_added: '2024-04-16T17:43:29.074Z' + date_added: '2024-04-17T21:06:29.901Z' external_id: null integration_id: 1 languages: '[]' name: getsentry/getsentry - organization_id: 4553845489270784 + organization_id: 4553851949875200 provider: integrations:github status: 0 url: https://github.com/getsentry/getsentry @@ -316,18 +316,18 @@ source: tests/sentry/backup/test_releases.py pk: 1 - fields: actor: 1 - date_added: '2024-04-16T17:43:28.719Z' + date_added: '2024-04-17T21:06:29.265Z' idp_provisioned: false name: test_team_in_test-org - organization: 4553845489270784 + organization: 4553851949875200 slug: test_team_in_test-org status: 0 model: sentry.team - pk: 4553845489270785 + pk: 4553851949875201 - fields: avatar_type: 0 avatar_url: null - date_joined: '2024-04-16T17:43:28.490Z' + date_joined: '2024-04-17T21:06:28.949Z' email: owner flags: '0' is_active: true @@ -337,11 +337,11 @@ source: tests/sentry/backup/test_releases.py is_staff: true is_superuser: true is_unclaimed: false - last_active: '2024-04-16T17:43:28.490Z' + last_active: '2024-04-17T21:06:28.949Z' last_login: null - last_password_change: '2024-04-16T17:43:28.490Z' + last_password_change: '2024-04-17T21:06:28.949Z' name: '' - password: md5$aX87DM5qU0fs1W0AIn65xf$f79c484adf94871de5108a18438c94fa + password: md5$Ce6wyHNsspY9en2ZOuyjIM$20b5bac688726431077275dc745e5014 session_nonce: null username: owner model: sentry.user @@ -349,7 +349,7 @@ source: tests/sentry/backup/test_releases.py - fields: avatar_type: 0 avatar_url: null - date_joined: '2024-04-16T17:43:28.550Z' + date_joined: '2024-04-17T21:06:29.021Z' email: member flags: '0' is_active: true @@ -359,11 +359,11 @@ source: tests/sentry/backup/test_releases.py is_staff: false is_superuser: false is_unclaimed: false - last_active: '2024-04-16T17:43:28.550Z' + last_active: '2024-04-17T21:06:29.021Z' last_login: null - last_password_change: '2024-04-16T17:43:28.550Z' + last_password_change: '2024-04-17T21:06:29.021Z' name: '' - password: md5$Q6hWTx8KjUGMvTWHxKYdJ5$df3a04b3043ec3a83b50248866e8dfd5 + password: md5$LwwARiVUZMwNclrd72agWc$8074353927e5791de7e9766d48752ec9 session_nonce: null username: member model: sentry.user @@ -371,7 +371,7 @@ source: tests/sentry/backup/test_releases.py - fields: avatar_type: 0 avatar_url: null - date_joined: '2024-04-16T17:43:29.005Z' + date_joined: '2024-04-17T21:06:29.825Z' email: admin@localhost flags: '0' is_active: true @@ -381,11 +381,11 @@ source: tests/sentry/backup/test_releases.py is_staff: true is_superuser: true is_unclaimed: false - last_active: '2024-04-16T17:43:29.005Z' + last_active: '2024-04-17T21:06:29.825Z' last_login: null - last_password_change: '2024-04-16T17:43:29.005Z' + last_password_change: '2024-04-17T21:06:29.825Z' name: '' - password: md5$VGVqHEd5BH36StWkmG5AGP$dce353b9352f2131faf094d5cabfc0fb + password: md5$hnD08ZSNk3SlpPM13zsvYG$4246817a03b19b8d25da82685d159f97 session_nonce: null username: admin@localhost model: sentry.user @@ -393,8 +393,8 @@ source: tests/sentry/backup/test_releases.py - fields: avatar_type: 0 avatar_url: null - date_joined: '2024-04-16T17:43:29.077Z' - email: 3dab210859294586b020f9d66d0f3091@example.com + date_joined: '2024-04-17T21:06:29.904Z' + email: edb168d4cba54597894617f4b8184866@example.com flags: '0' is_active: true is_managed: false @@ -403,19 +403,19 @@ source: tests/sentry/backup/test_releases.py is_staff: false is_superuser: false is_unclaimed: false - last_active: '2024-04-16T17:43:29.077Z' + last_active: '2024-04-17T21:06:29.904Z' last_login: null - last_password_change: '2024-04-16T17:43:29.077Z' + last_password_change: '2024-04-17T21:06:29.904Z' name: '' - password: md5$FHAdBVPAmVYp3hiQzJ7Myv$ff2e3835e81349ec79ac56f28e9ec6e1 + password: md5$VwwgLETHxP7C3qD4b5R7G2$5bf54b897962fbdbd2bec37815f33cc5 session_nonce: null - username: 3dab210859294586b020f9d66d0f3091@example.com + username: edb168d4cba54597894617f4b8184866@example.com model: sentry.user pk: 4 - fields: avatar_type: 0 avatar_url: null - date_joined: '2024-04-16T17:43:29.149Z' + date_joined: '2024-04-17T21:06:30.139Z' email: '' flags: '0' is_active: true @@ -425,20 +425,20 @@ source: tests/sentry/backup/test_releases.py is_staff: false is_superuser: false is_unclaimed: false - last_active: '2024-04-16T17:43:29.149Z' + last_active: '2024-04-17T21:06:30.139Z' last_login: null last_password_change: null name: '' password: '' session_nonce: null - username: test-app-83f208bf-9543-4a23-8645-7b4b2a88e19c + username: test-app-43642984-53cf-4794-9d14-1e58c4136fe3 model: sentry.user pk: 5 - fields: avatar_type: 0 avatar_url: null - date_joined: '2024-04-16T17:43:29.359Z' - email: 64ece61201df4adebb0e490224b0ae40@example.com + date_joined: '2024-04-17T21:06:30.345Z' + email: f6fc01c937f44a08b3dfab8bf064116b@example.com flags: '0' is_active: true is_managed: false @@ -447,13 +447,13 @@ source: tests/sentry/backup/test_releases.py is_staff: false is_superuser: false is_unclaimed: false - last_active: '2024-04-16T17:43:29.359Z' + last_active: '2024-04-17T21:06:30.345Z' last_login: null - last_password_change: '2024-04-16T17:43:29.359Z' + last_password_change: '2024-04-17T21:06:30.345Z' name: '' - password: md5$x9Q76YB6yB3YAFL8FnsxOs$d271fb8ed896dda14cfff784cdee1345 + password: md5$YJDBW7MD07khBLaixv371l$8ebefbc74ffa773b60c3acd198586188 session_nonce: null - username: 64ece61201df4adebb0e490224b0ae40@example.com + username: f6fc01c937f44a08b3dfab8bf064116b@example.com model: sentry.user pk: 6 - fields: @@ -496,24 +496,24 @@ source: tests/sentry/backup/test_releases.py model: sentry.userpermission pk: 1 - fields: - date_added: '2024-04-16T17:43:28.519Z' - date_updated: '2024-04-16T17:43:28.519Z' + date_added: '2024-04-17T21:06:28.979Z' + date_updated: '2024-04-17T21:06:28.979Z' name: test-admin-role permissions: '[]' model: sentry.userrole pk: 1 - fields: - date_added: '2024-04-16T17:43:28.524Z' - date_updated: '2024-04-16T17:43:28.524Z' + date_added: '2024-04-17T21:06:28.983Z' + date_updated: '2024-04-17T21:06:28.983Z' role: 1 user: 1 model: sentry.userroleuser pk: 1 - fields: - date_added: '2024-04-16T17:43:29.065Z' + date_added: '2024-04-17T21:06:29.893Z' is_global: false name: Saved query for test-org - organization: 4553845489270784 + organization: 4553851949875200 owner_id: null query: saved query for test-org sort: date @@ -522,9 +522,9 @@ source: tests/sentry/backup/test_releases.py model: sentry.savedsearch pk: 1 - fields: - date_added: '2024-04-16T17:43:29.064Z' - last_seen: '2024-04-16T17:43:29.064Z' - organization: 4553845489270784 + date_added: '2024-04-17T21:06:29.892Z' + last_seen: '2024-04-17T21:06:29.892Z' + organization: 4553851949875200 query: some query for test-org query_hash: 7c69362cd42207b83f80087bc15ebccb type: 0 @@ -532,42 +532,42 @@ source: tests/sentry/backup/test_releases.py model: sentry.recentsearch pk: 1 - fields: - project: 4553845489270786 - team: 4553845489270785 + project: 4553851949875202 + team: 4553851949875201 model: sentry.projectteam pk: 1 - fields: - project: 4553845489270787 - team: 4553845489270785 + project: 4553851949875203 + team: 4553851949875201 model: sentry.projectteam pk: 2 - fields: - date_added: '2024-04-16T17:43:28.819Z' - project: 4553845489270786 + date_added: '2024-04-17T21:06:29.469Z' + project: 4553851949875202 user_id: 1 model: sentry.projectbookmark pk: 1 - fields: created_by: null - date_added: '2024-04-16T17:43:28.900Z' + date_added: '2024-04-17T21:06:29.644Z' date_deactivated: null date_last_used: null name: token 1 for test-org - organization_id: 4553845489270784 - project_last_used_id: 4553845489270786 + organization_id: 4553851949875200 + project_last_used_id: 4553851949875202 scope_list: '[''org:ci'']' token_hashed: ABCDEFtest-org token_last_characters: xyz1 model: sentry.orgauthtoken pk: 1 - fields: - date_added: '2024-04-16T17:43:28.620Z' + date_added: '2024-04-17T21:06:29.115Z' email: null flags: '0' has_global_access: true invite_status: 0 inviter_id: null - organization: 4553845489270784 + organization: 4553851949875200 role: owner token: null token_expires_at: null @@ -578,13 +578,13 @@ source: tests/sentry/backup/test_releases.py model: sentry.organizationmember pk: 1 - fields: - date_added: '2024-04-16T17:43:28.669Z' + date_added: '2024-04-17T21:06:29.180Z' email: null flags: '0' has_global_access: true invite_status: 0 inviter_id: null - organization: 4553845489270784 + organization: 4553851949875200 role: member token: null token_expires_at: null @@ -597,108 +597,108 @@ source: tests/sentry/backup/test_releases.py - fields: member: 2 requester_id: null - team: 4553845489270785 + team: 4553851949875201 model: sentry.organizationaccessrequest pk: 1 - fields: config: schedule: '* * * * *' schedule_type: 1 - date_added: '2024-04-16T17:43:28.945Z' - guid: a7371cb3-99e5-4596-8185-a2d956934d5d + date_added: '2024-04-17T21:06:29.741Z' + guid: 189f673b-bba8-4b5c-aef4-18f50fa3b5fb is_muted: false name: '' - organization_id: 4553845489270784 + organization_id: 4553851949875200 owner_team_id: null owner_user_id: null - project_id: 4553845489270786 - slug: 636e4a79e12f + project_id: 4553851949875202 + slug: 51f724d22ebd status: 0 type: 3 model: sentry.monitor pk: 1 - fields: - date_added: '2024-04-16T17:43:28.942Z' - name: nominally helpful koala - organization_id: 4553845489270784 + date_added: '2024-04-17T21:06:29.736Z' + name: randomly bold squirrel + organization_id: 4553851949875200 model: sentry.environment pk: 1 - fields: - date_added: '2024-04-16T17:43:28.496Z' + date_added: '2024-04-17T21:06:28.959Z' email: owner model: sentry.email pk: 1 - fields: - date_added: '2024-04-16T17:43:28.555Z' + date_added: '2024-04-17T21:06:29.025Z' email: member model: sentry.email pk: 2 - fields: - date_added: '2024-04-16T17:43:29.009Z' + date_added: '2024-04-17T21:06:29.830Z' email: admin@localhost model: sentry.email pk: 3 - fields: - date_added: '2024-04-16T17:43:29.082Z' - email: 3dab210859294586b020f9d66d0f3091@example.com + date_added: '2024-04-17T21:06:29.909Z' + email: edb168d4cba54597894617f4b8184866@example.com model: sentry.email pk: 4 - fields: - date_added: '2024-04-16T17:43:29.153Z' + date_added: '2024-04-17T21:06:30.150Z' email: '' model: sentry.email pk: 5 - fields: - date_added: '2024-04-16T17:43:29.363Z' - email: 64ece61201df4adebb0e490224b0ae40@example.com + date_added: '2024-04-17T21:06:30.349Z' + email: f6fc01c937f44a08b3dfab8bf064116b@example.com model: sentry.email pk: 6 - fields: - date_added: '2024-04-16T17:43:29.063Z' - organization: 4553845489270784 + date_added: '2024-04-17T21:06:29.891Z' + organization: 4553851949875200 slug: test-tombstone-in-test-org model: sentry.dashboardtombstone pk: 1 - fields: created_by_id: 1 - date_added: '2024-04-16T17:43:29.058Z' + date_added: '2024-04-17T21:06:29.886Z' filters: null - last_visited: '2024-04-16T17:43:29.058Z' - organization: 4553845489270784 + last_visited: '2024-04-17T21:06:29.886Z' + organization: 4553851949875200 title: Dashboard 1 for test-org visits: 1 model: sentry.dashboard pk: 1 - fields: condition: '{"op":"equals","name":"environment","value":"prod"}' - condition_hash: 094385319e50e942c9e52740b7b89a600384c2a5 + condition_hash: 4c354c450d55506886c977cb123159d3f911ad0a created_by_id: null - date_added: '2024-04-16T17:43:28.936Z' - end_date: '2024-04-16T18:43:28.932Z' + date_added: '2024-04-17T21:06:29.726Z' + end_date: '2024-04-17T22:06:29.720Z' is_active: true is_org_level: false notification_sent: false num_samples: 100 - organization: 4553845489270784 + organization: 4553851949875200 query: environment:prod event.type:transaction rule_id: 1 sample_rate: 0.5 - start_date: '2024-04-16T17:43:28.932Z' + start_date: '2024-04-17T21:06:29.720Z' model: sentry.customdynamicsamplingrule pk: 1 - fields: - project: 4553845489270786 + project: 4553851949875202 value: 1 model: sentry.counter pk: 1 - fields: config: {} - date_added: '2024-04-16T17:43:28.854Z' + date_added: '2024-04-17T21:06:29.526Z' default_global_access: true default_role: 50 flags: '0' last_sync: null - organization_id: 4553845489270784 + organization_id: 4553851949875200 provider: sentry sync_time: null model: sentry.authprovider @@ -714,16 +714,16 @@ source: tests/sentry/backup/test_releases.py - 3 key4: nested_key: nested_value - date_added: '2024-04-16T17:43:28.877Z' + date_added: '2024-04-17T21:06:29.584Z' ident: 123456789test-org - last_synced: '2024-04-16T17:43:28.877Z' - last_verified: '2024-04-16T17:43:28.877Z' + last_synced: '2024-04-17T21:06:29.584Z' + last_verified: '2024-04-17T21:06:29.584Z' user: 1 model: sentry.authidentity pk: 1 - fields: config: '""' - created_at: '2024-04-16T17:43:28.509Z' + created_at: '2024-04-17T21:06:28.971Z' last_used_at: null type: 1 user: 1 @@ -731,7 +731,7 @@ source: tests/sentry/backup/test_releases.py pk: 1 - fields: config: '""' - created_at: '2024-04-16T17:43:28.563Z' + created_at: '2024-04-17T21:06:29.034Z' last_used_at: null type: 1 user: 2 @@ -739,10 +739,10 @@ source: tests/sentry/backup/test_releases.py pk: 2 - fields: allowed_origins: null - date_added: '2024-04-16T17:43:28.834Z' - key: 50f496ff1c6147ecbda74de0c7eed863 + date_added: '2024-04-17T21:06:29.492Z' + key: 6b7a750b4cc74c68a659ed9daf441b99 label: Default - organization_id: 4553845489270784 + organization_id: 4553851949875200 scope_list: '[]' scopes: '0' status: 0 @@ -750,11 +750,11 @@ source: tests/sentry/backup/test_releases.py pk: 1 - fields: allowed_origins: '' - client_id: 5a6884091f14dce9c17169ff02494d46d125ffd310996be48ca1133f631ce071 - client_secret: 8373470793d692d35ca6ea8d3bb8c0b636ca1d73c2060ebd6ad38f574bf8685c - date_added: '2024-04-16T17:43:29.159Z' + client_id: 7a803be356e68af929df91f060cddf4f5f94bf72be212d59acf0fcc755c22867 + client_secret: 9e88759b970612046509ca2cdb95bf8e1c84c1dfd3df0093f2c8eb29adc6161f + date_added: '2024-04-17T21:06:30.156Z' homepage_url: null - name: Vocal Insect + name: Delicate Roughy owner: 5 privacy_url: null redirect_uris: '' @@ -763,63 +763,63 @@ source: tests/sentry/backup/test_releases.py model: sentry.apiapplication pk: 1 - fields: - team: 4553845489270785 + team: 4553851949875201 type: 0 user_id: null model: sentry.actor pk: 1 - fields: - date_hash_added: '2024-04-16T17:43:28.493Z' + date_hash_added: '2024-04-17T21:06:28.954Z' email: owner is_verified: true user: 1 - validation_hash: kb2PiQP3qlDAMZjficCiTAxJOYpwuHYB + validation_hash: msB1gyNJfOLSw4lgFaIxoqZpiR8mZZXH model: sentry.useremail pk: 1 - fields: - date_hash_added: '2024-04-16T17:43:28.552Z' + date_hash_added: '2024-04-17T21:06:29.023Z' email: member is_verified: true user: 2 - validation_hash: 8EAKA1WKIdrQjaRiWQYA86IsHvQehLCo + validation_hash: 2PxV8sD80nGpK79vqJDEFRWucID86bCi model: sentry.useremail pk: 2 - fields: - date_hash_added: '2024-04-16T17:43:29.007Z' + date_hash_added: '2024-04-17T21:06:29.827Z' email: admin@localhost is_verified: true user: 3 - validation_hash: 0m1SUzIUggB423Xoanzhl3gsyagmjiIc + validation_hash: FpXZDwcmnXWXflW9R903JlfZoah5XhSD model: sentry.useremail pk: 3 - fields: - date_hash_added: '2024-04-16T17:43:29.079Z' - email: 3dab210859294586b020f9d66d0f3091@example.com + date_hash_added: '2024-04-17T21:06:29.906Z' + email: edb168d4cba54597894617f4b8184866@example.com is_verified: true user: 4 - validation_hash: ifkWeOkUDMMTrmzJeUEpDq2UcUJIefYb + validation_hash: q96sIJIBvUUt1lKS0faZ1mcAlyQD2O8i model: sentry.useremail pk: 4 - fields: - date_hash_added: '2024-04-16T17:43:29.151Z' + date_hash_added: '2024-04-17T21:06:30.148Z' email: '' is_verified: false user: 5 - validation_hash: rt3dHlLT1WA5OY73ieRbG1QHscRQIsGJ + validation_hash: aqlhDviVjzAo4SNBIIyvhGsEOnZyQAZP model: sentry.useremail pk: 5 - fields: - date_hash_added: '2024-04-16T17:43:29.361Z' - email: 64ece61201df4adebb0e490224b0ae40@example.com + date_hash_added: '2024-04-17T21:06:30.347Z' + email: f6fc01c937f44a08b3dfab8bf064116b@example.com is_verified: true user: 6 - validation_hash: ONHn9F6pqDSZU40lCIDSGUIkDSIHuFLP + validation_hash: cCqAnpLYHissaFF8axP6fajeRnJEn3qK model: sentry.useremail pk: 6 - fields: aggregate: count() dataset: events - date_added: '2024-04-16T17:43:28.986Z' + date_added: '2024-04-17T21:06:29.801Z' environment: null query: level:error resolution: 60 @@ -830,7 +830,7 @@ source: tests/sentry/backup/test_releases.py - fields: aggregate: count() dataset: events - date_added: '2024-04-16T17:43:29.018Z' + date_added: '2024-04-17T21:06:29.839Z' environment: null query: level:error resolution: 60 @@ -841,7 +841,7 @@ source: tests/sentry/backup/test_releases.py - fields: aggregate: count() dataset: events - date_added: '2024-04-16T17:43:29.035Z' + date_added: '2024-04-17T21:06:29.857Z' environment: null query: test query resolution: 60 @@ -852,18 +852,18 @@ source: tests/sentry/backup/test_releases.py - fields: application: 1 author: A Company - creator_label: 3dab210859294586b020f9d66d0f3091@example.com + creator_label: edb168d4cba54597894617f4b8184866@example.com creator_user: 4 - date_added: '2024-04-16T17:43:29.160Z' + date_added: '2024-04-17T21:06:30.157Z' date_deleted: null date_published: null - date_updated: '2024-04-16T17:43:29.324Z' + date_updated: '2024-04-17T21:06:30.305Z' events: '[]' is_alertable: false metadata: {} name: test app overview: null - owner_id: 4553845489270784 + owner_id: 4553851949875200 popularity: 1 proxy_user: 5 redirect_url: null @@ -904,26 +904,26 @@ source: tests/sentry/backup/test_releases.py scopes: '0' slug: test-app status: 0 - uuid: c90dfd95-8b2f-4eec-ac3c-45da72e32c34 + uuid: 19d75199-a27e-4901-9c8d-015aeb0f7492 verify_install: true webhook_url: https://example.com/webhook model: sentry.sentryapp pk: 1 - fields: data: '{"conditions":[{"id":"sentry.rules.conditions.first_seen_event.FirstSeenEventCondition"},{"id":"sentry.rules.conditions.every_event.EveryEventCondition"}],"action_match":"all","filter_match":"all","actions":[{"id":"sentry.rules.actions.notify_event.NotifyEventAction"},{"id":"sentry.rules.actions.notify_event_service.NotifyEventServiceAction","service":"mail"}]}' - date_added: '2024-04-16T17:43:28.928Z' + date_added: '2024-04-17T21:06:29.712Z' environment_id: null label: '' owner: null - project: 4553845489270786 + project: 4553851949875202 source: 0 status: 0 model: sentry.rule pk: 1 - fields: - date_added: '2024-04-16T17:43:28.994Z' - date_updated: '2024-04-16T17:43:28.994Z' - project: 4553845489270786 + date_added: '2024-04-17T21:06:29.810Z' + date_updated: '2024-04-17T21:06:29.810Z' + project: 4553851949875202 query_extra: null snuba_query: 1 status: 1 @@ -932,9 +932,9 @@ source: tests/sentry/backup/test_releases.py model: sentry.querysubscription pk: 1 - fields: - date_added: '2024-04-16T17:43:29.025Z' - date_updated: '2024-04-16T17:43:29.025Z' - project: 4553845489270786 + date_added: '2024-04-17T21:06:29.846Z' + date_updated: '2024-04-17T21:06:29.846Z' + project: 4553851949875202 query_extra: null snuba_query: 2 status: 1 @@ -943,9 +943,9 @@ source: tests/sentry/backup/test_releases.py model: sentry.querysubscription pk: 2 - fields: - date_added: '2024-04-16T17:43:29.040Z' - date_updated: '2024-04-16T17:43:29.040Z' - project: 4553845489270786 + date_added: '2024-04-17T21:06:29.862Z' + date_updated: '2024-04-17T21:06:29.862Z' + project: 4553851949875202 query_extra: null snuba_query: 3 status: 1 @@ -954,9 +954,9 @@ source: tests/sentry/backup/test_releases.py model: sentry.querysubscription pk: 3 - fields: - date_added: '2024-04-16T17:43:29.169Z' - date_updated: '2024-04-16T17:43:29.169Z' - project: 4553845489336321 + date_added: '2024-04-17T21:06:30.172Z' + date_updated: '2024-04-17T21:06:30.172Z' + project: 4553851949940736 query_extra: null snuba_query: 1 status: 1 @@ -965,9 +965,9 @@ source: tests/sentry/backup/test_releases.py model: sentry.querysubscription pk: 4 - fields: - date_added: '2024-04-16T17:43:29.373Z' - date_updated: '2024-04-16T17:43:29.373Z' - project: 4553845489336322 + date_added: '2024-04-17T21:06:30.358Z' + date_updated: '2024-04-17T21:06:30.358Z' + project: 4553851949940737 query_extra: null snuba_query: 1 status: 1 @@ -979,12 +979,12 @@ source: tests/sentry/backup/test_releases.py is_active: true organizationmember: 1 role: null - team: 4553845489270785 + team: 4553851949875201 model: sentry.organizationmemberteam pk: 1 - fields: integration_id: null - organization: 4553845489270784 + organization: 4553851949875200 sentry_app_id: null target_display: Sentry User target_identifier: '1' @@ -995,7 +995,7 @@ source: tests/sentry/backup/test_releases.py pk: 1 - fields: integration_id: null - organization: 4553845489270784 + organization: 4553851949875200 sentry_app_id: 1 target_display: Sentry User target_identifier: '1' @@ -1005,23 +1005,23 @@ source: tests/sentry/backup/test_releases.py model: sentry.notificationaction pk: 2 - fields: - disable_date: '2024-04-16T17:43:28.931Z' + disable_date: '2024-04-17T21:06:29.718Z' opted_out: false - organization: 4553845489270784 + organization: 4553851949875200 rule: 1 - sent_final_email_date: '2024-04-16T17:43:28.931Z' - sent_initial_email_date: '2024-04-16T17:43:28.931Z' + sent_final_email_date: '2024-04-17T21:06:29.718Z' + sent_initial_email_date: '2024-04-17T21:06:29.718Z' model: sentry.neglectedrule pk: 1 - fields: environment: 1 is_hidden: null - project: 4553845489270786 + project: 4553851949875202 model: sentry.environmentproject pk: 1 - fields: dashboard: 1 - date_added: '2024-04-16T17:43:29.059Z' + date_added: '2024-04-17T21:06:29.887Z' description: null detail: null discover_widget_split: null @@ -1036,60 +1036,60 @@ source: tests/sentry/backup/test_releases.py pk: 1 - fields: custom_dynamic_sampling_rule: 1 - project: 4553845489270786 + project: 4553851949875202 model: sentry.customdynamicsamplingruleproject pk: 1 - fields: application: 1 - date_added: '2024-04-16T17:43:29.255Z' - expires_at: '2024-04-17T01:43:29.255Z' - hashed_refresh_token: null - hashed_token: null + date_added: '2024-04-17T21:06:30.260Z' + expires_at: '2024-04-18T05:06:30.259Z' + hashed_refresh_token: 957aa1c5f94bc1363f80562f047ffb8cac77d05c096b56464ed098625d9d1c3d + hashed_token: 65d1da6f916aa2f79405d38f46a6f078189adf6c904e78fa919cc33ebc344467 name: null - refresh_token: c5a78add578628580fcd03e551f4a0eaa8542484e7cab427f807478dcdd16e63 + refresh_token: 89e8834fd5235cdd623ee68a764dfbc4b7377da4ae33dfcb35c08946e3b8d1ac scope_list: '[]' scopes: '0' - token: 2555219a44359c6c10355bcf8d7a56cfbd3f0ac7aab3b5e50832226666a19573 - token_last_characters: '9573' + token: 1b172569d1e8e8ff5f611d10e3f47732249f0174102ebfe4e3b653dc7edb7d0f + token_last_characters: 7d0f token_type: null user: 5 model: sentry.apitoken pk: 1 - fields: application: 1 - date_added: '2024-04-16T17:43:29.334Z' + date_added: '2024-04-17T21:06:30.321Z' expires_at: null - hashed_refresh_token: null - hashed_token: null + hashed_refresh_token: fe6d5d30be912ae5fb69a10a266b9c43ac0b41254b70cf011dd9918954a7778b + hashed_token: f65e9e6c79077ac6343ef73125ef876bd90fda647127a63f6fbab01fd0fe42df name: create_exhaustive_sentry_app - refresh_token: cf612316fddf92dcd895a15392ecb34e68e258125cf5077e3ae06b5a439aa588 + refresh_token: 969f45dd8d79395b6892a79ecd9e425523e2b55c8dede2244649e639e73a43ed scope_list: '[]' scopes: '0' - token: 2534b64e3fa824f9a7d71048d16327100c00031f2c953c96cc5b3eff0468f594 - token_last_characters: f594 + token: 5f1b28d5df78a74dd9b64289731ab9f5c3b80907f2c57c85b367d6a473572bc2 + token_last_characters: 2bc2 token_type: null user: 1 model: sentry.apitoken pk: 2 - fields: application: null - date_added: '2024-04-16T17:43:29.425Z' + date_added: '2024-04-17T21:06:30.401Z' expires_at: null hashed_refresh_token: null - hashed_token: null + hashed_token: cadf3c1777cb5fcda39bc11dd0acad53d821cb8709575086121f05215d0203b9 name: create_exhaustive_global_configs - refresh_token: 77caaba576ff58dbb621ec18ef676603f3d928f38e7c630d9694e99aec01dfcc + refresh_token: null scope_list: '[]' scopes: '0' - token: b649b6a127755acbc568eea3648d1bc67ea99d0b057e0a2948099491f23288fd - token_last_characters: 88fd - token_type: null + token: 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 + token_last_characters: dec8 + token_type: sntryu_ user: 1 model: sentry.apitoken pk: 3 - fields: application: 1 - code: 134fef6e4b12c3459d755da5a5c118ed6a97ceec22d38e2bc432f348f23521da + code: ee243059197cac7c5e4781b06f91c38eef5742fe23e3fa6b22b2d6e1743413a8 expires_at: '2022-01-01T11:11:00.000Z' redirect_uri: https://example.com scope_list: '[''openid'', ''profile'', ''email'']' @@ -1099,7 +1099,7 @@ source: tests/sentry/backup/test_releases.py pk: 2 - fields: application: 1 - date_added: '2024-04-16T17:43:29.333Z' + date_added: '2024-04-17T21:06:30.319Z' scope_list: '[]' scopes: '0' user: 1 @@ -1107,7 +1107,7 @@ source: tests/sentry/backup/test_releases.py pk: 1 - fields: application: null - date_added: '2024-04-16T17:43:29.424Z' + date_added: '2024-04-17T21:06:30.399Z' scope_list: '[]' scopes: '0' user: 1 @@ -1115,12 +1115,12 @@ source: tests/sentry/backup/test_releases.py pk: 2 - fields: comparison_delta: null - date_added: '2024-04-16T17:43:28.989Z' - date_modified: '2024-04-16T17:43:28.989Z' + date_added: '2024-04-17T21:06:29.804Z' + date_modified: '2024-04-17T21:06:29.804Z' include_all_projects: true monitor_type: 0 - name: Premium Wallaby - organization: 4553845489270784 + name: Evolving Mako + organization: 4553851949875200 owner: null resolve_threshold: null snuba_query: 1 @@ -1133,12 +1133,12 @@ source: tests/sentry/backup/test_releases.py pk: 1 - fields: comparison_delta: null - date_added: '2024-04-16T17:43:29.020Z' - date_modified: '2024-04-16T17:43:29.020Z' + date_added: '2024-04-17T21:06:29.841Z' + date_modified: '2024-04-17T21:06:29.841Z' include_all_projects: false monitor_type: 1 - name: Still Grackle - organization: 4553845489270784 + name: Whole Moray + organization: 4553851949875200 owner: null resolve_threshold: null snuba_query: 2 @@ -1151,12 +1151,12 @@ source: tests/sentry/backup/test_releases.py pk: 2 - fields: comparison_delta: null - date_added: '2024-04-16T17:43:29.037Z' - date_modified: '2024-04-16T17:43:29.037Z' + date_added: '2024-04-17T21:06:29.859Z' + date_modified: '2024-04-17T21:06:29.859Z' include_all_projects: false monitor_type: 0 - name: Prepared Jackal - organization: 4553845489270784 + name: Apparent Colt + organization: 4553851949875200 owner: null resolve_threshold: null snuba_query: 3 @@ -1185,13 +1185,13 @@ source: tests/sentry/backup/test_releases.py - fields: api_grant: null api_token: 1 - date_added: '2024-04-16T17:43:29.208Z' + date_added: '2024-04-17T21:06:30.215Z' date_deleted: null - date_updated: '2024-04-16T17:43:29.233Z' - organization_id: 4553845489270784 + date_updated: '2024-04-17T21:06:30.237Z' + organization_id: 4553851949875200 sentry_app: 1 status: 1 - uuid: 23f9105d-2bea-4aa4-810e-deac30d43657 + uuid: df903a11-46ed-467e-88de-52ce810537f5 model: sentry.sentryappinstallation pk: 1 - fields: @@ -1229,12 +1229,12 @@ source: tests/sentry/backup/test_releases.py type: alert-rule-action sentry_app: 1 type: alert-rule-action - uuid: 62d758d7-0f0e-4895-b9e5-a03a046c2aff + uuid: 2dbd4717-9c1a-4fa6-aa14-773f2dd33e1c model: sentry.sentryappcomponent pk: 1 - fields: alert_rule: null - date_added: '2024-04-16T17:43:28.930Z' + date_added: '2024-04-17T21:06:29.716Z' owner_id: 1 rule: 1 until: null @@ -1242,7 +1242,7 @@ source: tests/sentry/backup/test_releases.py model: sentry.rulesnooze pk: 1 - fields: - date_added: '2024-04-16T17:43:28.929Z' + date_added: '2024-04-17T21:06:29.714Z' rule: 1 type: 1 user_id: null @@ -1250,26 +1250,26 @@ source: tests/sentry/backup/test_releases.py pk: 1 - fields: action: 1 - project: 4553845489270786 + project: 4553851949875202 model: sentry.notificationactionproject pk: 1 - fields: action: 2 - project: 4553845489270786 + project: 4553851949875202 model: sentry.notificationactionproject pk: 2 - fields: alert_rule: 3 - date_added: '2024-04-16T17:43:29.045Z' + date_added: '2024-04-17T21:06:29.867Z' date_closed: null - date_detected: '2024-04-16T17:43:29.043Z' - date_started: '2024-04-16T17:43:29.043Z' + date_detected: '2024-04-17T21:06:29.865Z' + date_started: '2024-04-17T21:06:29.865Z' detection_uuid: null identifier: 1 - organization: 4553845489270784 + organization: 4553851949875200 status: 1 status_method: 3 - title: Ideal Haddock + title: Feasible Cheetah type: 2 model: sentry.incident pk: 1 @@ -1277,8 +1277,8 @@ source: tests/sentry/backup/test_releases.py aggregates: null columns: null conditions: '' - date_added: '2024-04-16T17:43:29.060Z' - date_modified: '2024-04-16T17:43:29.060Z' + date_added: '2024-04-17T21:06:29.888Z' + date_modified: '2024-04-17T21:06:29.888Z' field_aliases: null fields: '[]' is_hidden: false @@ -1291,8 +1291,8 @@ source: tests/sentry/backup/test_releases.py - fields: alert_rule: 1 alert_threshold: 100.0 - date_added: '2024-04-16T17:43:29.001Z' - label: Tops Asp + date_added: '2024-04-17T21:06:29.821Z' + label: Cute Perch resolve_threshold: null threshold_type: null model: sentry.alertruletrigger @@ -1300,39 +1300,39 @@ source: tests/sentry/backup/test_releases.py - fields: alert_rule: 2 alert_threshold: 100.0 - date_added: '2024-04-16T17:43:29.032Z' - label: Genuine Leopard + date_added: '2024-04-17T21:06:29.853Z' + label: Coherent Anchovy resolve_threshold: null threshold_type: null model: sentry.alertruletrigger pk: 2 - fields: alert_rule: 1 - date_added: '2024-04-16T17:43:28.993Z' - project: 4553845489270786 + date_added: '2024-04-17T21:06:29.809Z' + project: 4553851949875202 model: sentry.alertruleprojects pk: 1 - fields: alert_rule: 2 - date_added: '2024-04-16T17:43:29.022Z' - project: 4553845489270786 + date_added: '2024-04-17T21:06:29.843Z' + project: 4553851949875202 model: sentry.alertruleprojects pk: 2 - fields: alert_rule: 3 - date_added: '2024-04-16T17:43:29.039Z' - project: 4553845489270786 + date_added: '2024-04-17T21:06:29.861Z' + project: 4553851949875202 model: sentry.alertruleprojects pk: 3 - fields: alert_rule: 1 - date_added: '2024-04-16T17:43:28.991Z' - project: 4553845489270787 + date_added: '2024-04-17T21:06:29.806Z' + project: 4553851949875203 model: sentry.alertruleexcludedprojects pk: 1 - fields: alert_rule: 1 - date_added: '2024-04-16T17:43:28.996Z' + date_added: '2024-04-17T21:06:29.812Z' previous_alert_rule: null type: 1 user_id: null @@ -1340,7 +1340,7 @@ source: tests/sentry/backup/test_releases.py pk: 1 - fields: alert_rule: 2 - date_added: '2024-04-16T17:43:29.023Z' + date_added: '2024-04-17T21:06:29.844Z' previous_alert_rule: null type: 1 user_id: null @@ -1348,7 +1348,7 @@ source: tests/sentry/backup/test_releases.py pk: 2 - fields: alert_rule: 3 - date_added: '2024-04-16T17:43:29.041Z' + date_added: '2024-04-17T21:06:29.863Z' previous_alert_rule: null type: 1 user_id: null @@ -1357,28 +1357,28 @@ source: tests/sentry/backup/test_releases.py - fields: alert_rule: 2 condition_type: 0 - date_added: '2024-04-16T17:43:29.021Z' + date_added: '2024-04-17T21:06:29.843Z' label: '' model: sentry.alertruleactivationcondition pk: 1 - fields: - date_added: '2024-04-16T17:43:29.051Z' - end: '2024-04-16T17:43:29.051Z' + date_added: '2024-04-17T21:06:29.880Z' + end: '2024-04-17T21:06:29.879Z' period: 1 - start: '2024-04-15T17:43:29.051Z' + start: '2024-04-16T21:06:29.879Z' values: '[[1.0, 2.0, 3.0], [1.5, 2.5, 3.5]]' model: sentry.timeseriessnapshot pk: 1 - fields: actor_id: 1 application_id: 1 - date_added: '2024-04-16T17:43:29.230Z' + date_added: '2024-04-17T21:06:30.234Z' events: '[]' - guid: 4e60f107eb6c4eb9b62ee6a1b8030962 + guid: 94cde944041a471a8c7bb3fc20605312 installation_id: 1 - organization_id: 4553845489270784 + organization_id: 4553851949875200 project_id: null - secret: 4b40911a32d622364c5f9f758b1d8413f6a94b80f487393771b71a4c894b684c + secret: 6ba63677fd5958b50afbbf85fd96450923e6a6fee421c7f981d7719a6d1cd2ea status: 0 url: https://example.com/webhook version: 0 @@ -1387,40 +1387,40 @@ source: tests/sentry/backup/test_releases.py - fields: actor_id: 6 application_id: 1 - date_added: '2024-04-16T17:43:29.407Z' + date_added: '2024-04-17T21:06:30.384Z' events: '[''event.created'']' - guid: 163ef35a2d0c4e3287a8075957d57a18 + guid: 925fc3cbd31f42968232fc9a0f8ffa92 installation_id: 1 - organization_id: 4553845489270784 - project_id: 4553845489336322 - secret: b2211903f96cb121d354268e7f8addb7906e79092b619c8350bc5a4606f96635 + organization_id: 4553851949875200 + project_id: 4553851949940737 + secret: 6d05bf386570faa86d07d81e3225a478de812ec34455bad5c1b59111a30af17c status: 0 url: https://example.com/sentry/webhook version: 0 model: sentry.servicehook pk: 2 - fields: - date_added: '2024-04-16T17:43:29.056Z' + date_added: '2024-04-17T21:06:29.885Z' incident: 1 - target_run_date: '2024-04-16T21:43:29.056Z' + target_run_date: '2024-04-18T01:06:29.884Z' model: sentry.pendingincidentsnapshot pk: 1 - fields: alert_rule_trigger: 1 - date_added: '2024-04-16T17:43:29.055Z' - date_modified: '2024-04-16T17:43:29.055Z' + date_added: '2024-04-17T21:06:29.883Z' + date_modified: '2024-04-17T21:06:29.883Z' incident: 1 status: 1 model: sentry.incidenttrigger pk: 1 - fields: - date_added: '2024-04-16T17:43:29.053Z' + date_added: '2024-04-17T21:06:29.882Z' incident: 1 user_id: 1 model: sentry.incidentsubscription pk: 1 - fields: - date_added: '2024-04-16T17:43:29.052Z' + date_added: '2024-04-17T21:06:29.881Z' event_stats_snapshot: 1 incident: 1 total_events: 1 @@ -1429,7 +1429,7 @@ source: tests/sentry/backup/test_releases.py pk: 1 - fields: comment: hello test-org - date_added: '2024-04-16T17:43:29.050Z' + date_added: '2024-04-17T21:06:29.877Z' incident: 1 notification_uuid: null previous_value: null @@ -1440,8 +1440,8 @@ source: tests/sentry/backup/test_releases.py pk: 1 - fields: dashboard_widget_query: 1 - date_added: '2024-04-16T17:43:29.062Z' - date_modified: '2024-04-16T17:43:29.062Z' + date_added: '2024-04-17T21:06:29.890Z' + date_modified: '2024-04-17T21:06:29.889Z' extraction_state: disabled:not-applicable spec_hashes: '[]' spec_version: null @@ -1449,13 +1449,13 @@ source: tests/sentry/backup/test_releases.py pk: 1 - fields: alert_rule_trigger: 1 - date_added: '2024-04-16T17:43:29.002Z' + date_added: '2024-04-17T21:06:29.822Z' query_subscription: 1 model: sentry.alertruletriggerexclusion pk: 1 - fields: alert_rule_trigger: 1 - date_added: '2024-04-16T17:43:29.016Z' + date_added: '2024-04-17T21:06:29.837Z' integration_id: null sentry_app_config: null sentry_app_id: null @@ -1468,7 +1468,7 @@ source: tests/sentry/backup/test_releases.py pk: 1 - fields: alert_rule_trigger: 2 - date_added: '2024-04-16T17:43:29.033Z' + date_added: '2024-04-17T21:06:29.856Z' integration_id: null sentry_app_config: null sentry_app_id: null diff --git a/tests/sentry/models/test_apitoken.py b/tests/sentry/models/test_apitoken.py index 88009e98efc116..72931dc9a9e5f8 100644 --- a/tests/sentry/models/test_apitoken.py +++ b/tests/sentry/models/test_apitoken.py @@ -1,10 +1,12 @@ +import hashlib from datetime import timedelta +import pytest from django.utils import timezone from sentry.conf.server import SENTRY_SCOPE_HIERARCHY_MAPPING, SENTRY_SCOPES from sentry.hybridcloud.models import ApiTokenReplica -from sentry.models.apitoken import ApiToken +from sentry.models.apitoken import ApiToken, NotSupported, PlaintextSecretAlreadyRead from sentry.models.integrations.sentry_app_installation import SentryAppInstallation from sentry.models.integrations.sentry_app_installation_token import SentryAppInstallationToken from sentry.silo import SiloMode @@ -12,6 +14,7 @@ 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.types.token import AuthTokenType @control_silo_test @@ -74,6 +77,103 @@ def test_last_chars_are_not_set(self): token = ApiToken.objects.create(user_id=user.id) assert token.token_last_characters is None + @override_options({"apitoken.save-hash-on-create": True}) + def test_hash_exists_on_token(self): + user = self.create_user() + token = ApiToken.objects.create(user_id=user.id) + assert token.hashed_token is not None + assert token.hashed_refresh_token is not None + + @override_options({"apitoken.save-hash-on-create": True}) + def test_hash_exists_on_user_token(self): + user = self.create_user() + token = ApiToken.objects.create(user_id=user.id, token_type=AuthTokenType.USER) + assert token.hashed_token is not None + assert len(token.hashed_token) == 64 # sha256 hash + assert token.hashed_refresh_token is None # user auth tokens don't have refresh tokens + + @override_options({"apitoken.save-hash-on-create": False}) + def test_hash_does_not_exist_on_user_token_with_option_off(self): + user = self.create_user() + token = ApiToken.objects.create(user_id=user.id, token_type=AuthTokenType.USER) + assert token.hashed_token is None + assert token.hashed_refresh_token is None # user auth tokens don't have refresh tokens + + @override_options({"apitoken.save-hash-on-create": False}) + def test_can_access_read_once_tokens_with_option_off(self): + user = self.create_user() + token = ApiToken.objects.create(user_id=user.id) + assert token.hashed_token is None + assert token.hashed_refresh_token is None + + assert token.plaintext_token is not None + assert token.plaintext_refresh_token is not None + + # we accessed the tokens above when we asserted it was not None + # accessing them again should throw an exception + with pytest.raises(PlaintextSecretAlreadyRead): + _ = token.plaintext_token + + with pytest.raises(PlaintextSecretAlreadyRead): + _ = token.plaintext_refresh_token + + @override_options({"apitoken.save-hash-on-create": True}) + def test_plaintext_values_only_available_immediately_after_create(self): + user = self.create_user() + token = ApiToken.objects.create(user_id=user.id) + assert token.plaintext_token is not None + assert token.plaintext_refresh_token is not None + + # we accessed the tokens above when we asserted it was not None + # accessing them again should throw an exception + with pytest.raises(PlaintextSecretAlreadyRead): + _ = token.plaintext_token + + with pytest.raises(PlaintextSecretAlreadyRead): + _ = token.plaintext_refresh_token + + @override_options({"apitoken.save-hash-on-create": True}) + def test_error_when_accessing_refresh_token_on_user_token(self): + user = self.create_user() + token = ApiToken.objects.create(user_id=user.id, token_type=AuthTokenType.USER) + + with pytest.raises(NotSupported): + assert token.plaintext_refresh_token is not None + + @override_options({"apitoken.save-hash-on-create": True}) + def test_user_auth_token_refresh_raises_error(self): + user = self.create_user() + token = ApiToken.objects.create(user_id=user.id, token_type=AuthTokenType.USER) + + with pytest.raises(NotSupported): + token.refresh() + + @override_options({"apitoken.save-hash-on-create": True}) + def test_user_auth_token_sha256_hash(self): + user = self.create_user() + token = ApiToken.objects.create(user_id=user.id, token_type=AuthTokenType.USER) + expected_hash = hashlib.sha256(token.plaintext_token.encode()).hexdigest() + assert expected_hash == token.hashed_token + + @override_options({"apitoken.save-hash-on-create": True}) + def test_hash_updated_when_calling_update(self): + user = self.create_user() + token = ApiToken.objects.create(user_id=user.id) + initial_expected_hash = hashlib.sha256(token.plaintext_token.encode()).hexdigest() + assert initial_expected_hash == token.hashed_token + + new_token = "abc1234" + new_token_expected_hash = hashlib.sha256(new_token.encode()).hexdigest() + + with assume_test_silo_mode(SiloMode.CONTROL): + with outbox_runner(): + token.update(token=new_token) + + token.refresh_from_db() + + assert token.token_last_characters == "1234" + assert token.hashed_token == new_token_expected_hash + @control_silo_test class ApiTokenInternalIntegrationTest(TestCase):