From a995b9558fd551b884d024e2527f31ff30a5943b Mon Sep 17 00:00:00 2001 From: mdtro <20070360+mdtro@users.noreply.github.com> Date: Tue, 2 Apr 2024 22:34:51 -0500 Subject: [PATCH 01/17] feat: improved user auth tokens --- src/sentry/api/endpoints/api_tokens.py | 3 +- src/sentry/api/serializers/models/apitoken.py | 5 +- src/sentry/models/apitoken.py | 88 ++++++++++++++++++- src/sentry/testutils/factories.py | 2 + src/sentry/testutils/helpers/backups.py | 6 +- src/sentry/web/frontend/setup_wizard.py | 3 +- tests/sentry/models/test_apitoken.py | 37 ++++++++ 7 files changed, 137 insertions(+), 7 deletions(-) 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..01197de5983738 100644 --- a/src/sentry/api/serializers/models/apitoken.py +++ b/src/sentry/api/serializers/models/apitoken.py @@ -30,9 +30,8 @@ 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["refreshToken"] = obj.refresh_token + data["token"] = obj._plaintext_token + 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..c2231084157452 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 @@ -32,6 +33,57 @@ def generate_token(): return secrets.token_hex(nbytes=32) +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 = f"{token_type}{generate_token()}" + plaintext_refresh_token = None # user auth tokens do not have refresh tokens + 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 is not None: + kwargs["hashed_refresh_token"] = hashlib.sha256( + plaintext_refresh_token.encode() + ).hexdigest() + + kwargs["token"] = plaintext_token + kwargs["refresh_token"] = plaintext_refresh_token + + if plaintext_refresh_token is not None: + kwargs["refresh_token"] = plaintext_refresh_token + kwargs["hashed_refresh_token"] = hashlib.sha256( + plaintext_refresh_token.encode() + ).hexdigest() + + api_token = super().create(*args, **kwargs) + + # Store the plaintext tokens for one-time retrieval + api_token.__plaintext_token = plaintext_token + api_token.__plaintext_refresh_token = plaintext_refresh_token + + return api_token + + @control_silo_only_model class ApiToken(ReplicatedControlModel, HasApiScopes): __relocation_scope__ = {RelocationScope.Global, RelocationScope.Config} @@ -50,7 +102,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,6 +115,40 @@ class Meta: def __str__(self): return force_str(self.token) + @property + def _plaintext_token(self): + """ + To be called immediately after creation of a new token to return the + plaintext token to the user. After reading the token, it will be set + to `None` to prevent future accidental leaking of the token in logs, + exceptions, etc. + """ + manager_class_name = self.objects.__class__.__name__ + plaintext_token: str | None = getattr(self, f"_{manager_class_name}__plaintext_token", None) + + if plaintext_token is not None: + setattr(self, f"_{manager_class_name}__plaintext_token", None) + + return plaintext_token + + @property + def _plaintext_refresh_token(self): + """ + To be called immediately after creation of a new token to return the + plaintext refresh token to the user. After reading the refresh token, it will be set + to `None` to prevent future accidental leaking of the refresh token in logs, + exceptions, etc. + """ + manager_class_name = self.objects.__class__.__name__ + plaintext_refresh_token: str | None = getattr( + self, f"_{manager_class_name}__plaintext_refresh_token", None + ) + + if plaintext_refresh_token is not None: + setattr(self, f"_{manager_class_name}__plaintext_refresh_token", None) + + return plaintext_refresh_token + def save(self, *args: Any, **kwargs: Any) -> None: if options.get("apitoken.auto-add-last-chars"): token_last_characters = self.token[-4:] 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/models/test_apitoken.py b/tests/sentry/models/test_apitoken.py index 88009e98efc116..ddcbc5b8236160 100644 --- a/tests/sentry/models/test_apitoken.py +++ b/tests/sentry/models/test_apitoken.py @@ -1,3 +1,4 @@ +import hashlib from datetime import timedelta from django.utils import timezone @@ -12,6 +13,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 +76,41 @@ 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_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": True}) + def test_plaintext_values_only_available_immediately_after_create(self): + user = self.create_user() + token = ApiToken.objects.create(user_id=user.id, token_type=AuthTokenType.USER) + assert token._plaintext_token is not None + assert token._plaintext_refresh_token is None # user auth tokens don't have refresh tokens + + _ = token._plaintext_token + + # we read the value above so now it should + # now be None as it is a "read once" property + assert token._plaintext_token is None + + @override_options({"apitoken.save-hash-on-create": True}) + def test_user_auth_token_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 + @control_silo_test class ApiTokenInternalIntegrationTest(TestCase): From c9f1ce1125749eb7de85860b20fafab38eb36893 Mon Sep 17 00:00:00 2001 From: mdtro <20070360+mdtro@users.noreply.github.com> Date: Tue, 2 Apr 2024 23:11:14 -0500 Subject: [PATCH 02/17] custom exception when attempting to read more than once --- src/sentry/models/apitoken.py | 21 ++++++++++++++++++++- tests/sentry/models/test_apitoken.py | 12 ++++++------ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/sentry/models/apitoken.py b/src/sentry/models/apitoken.py index c2231084157452..2fe3cb83d8b806 100644 --- a/src/sentry/models/apitoken.py +++ b/src/sentry/models/apitoken.py @@ -33,6 +33,14 @@ def generate_token(): return secrets.token_hex(nbytes=32) +class PlaintextSecretAlreadyRead(Exception): + def __init__( + self, + message="the secret you are trying to read is read-once and cannot be accessed directly again", + ): + super().__init__(message) + + class ApiTokenManager(ControlOutboxProducingManager): def create(self, *args, **kwargs): token_type: AuthTokenType | None = kwargs.get("token_type", None) @@ -128,6 +136,8 @@ def _plaintext_token(self): if plaintext_token is not None: setattr(self, f"_{manager_class_name}__plaintext_token", None) + else: + raise PlaintextSecretAlreadyRead() return plaintext_token @@ -144,9 +154,18 @@ def _plaintext_refresh_token(self): self, f"_{manager_class_name}__plaintext_refresh_token", None ) - if plaintext_refresh_token is not None: + if plaintext_refresh_token: setattr(self, f"_{manager_class_name}__plaintext_refresh_token", None) + # some token types do not have refresh tokens, so we check to see + # if there's a hash value that exists for the refresh token. + # + # if there is a hash value, then a refresh token is expected + # and if the plaintext_refresh_token is None, then it has already + # been read once so we should throw the exception + if not plaintext_refresh_token and self.refresh_token: + raise PlaintextSecretAlreadyRead() + return plaintext_refresh_token def save(self, *args: Any, **kwargs: Any) -> None: diff --git a/tests/sentry/models/test_apitoken.py b/tests/sentry/models/test_apitoken.py index ddcbc5b8236160..a84adfbccd31d8 100644 --- a/tests/sentry/models/test_apitoken.py +++ b/tests/sentry/models/test_apitoken.py @@ -1,11 +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, 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 @@ -98,11 +99,10 @@ def test_plaintext_values_only_available_immediately_after_create(self): assert token._plaintext_token is not None assert token._plaintext_refresh_token is None # user auth tokens don't have refresh tokens - _ = token._plaintext_token - - # we read the value above so now it should - # now be None as it is a "read once" property - assert token._plaintext_token is None + # we accessed the plaintext token above when we asserted it was not None + # accessing it again should throw an exception + with pytest.raises(PlaintextSecretAlreadyRead): + _ = token._plaintext_token @override_options({"apitoken.save-hash-on-create": True}) def test_user_auth_token_hash(self): From 696c84524b43f716adf0f6cedb661eb9be7390fc Mon Sep 17 00:00:00 2001 From: mdtro <20070360+mdtro@users.noreply.github.com> Date: Tue, 2 Apr 2024 23:18:50 -0500 Subject: [PATCH 03/17] remove duplicate code --- src/sentry/models/apitoken.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/sentry/models/apitoken.py b/src/sentry/models/apitoken.py index 2fe3cb83d8b806..3e3cc94b1f017d 100644 --- a/src/sentry/models/apitoken.py +++ b/src/sentry/models/apitoken.py @@ -69,7 +69,7 @@ def create(self, *args, **kwargs): if options.get("apitoken.save-hash-on-create"): kwargs["hashed_token"] = hashlib.sha256(plaintext_token.encode()).hexdigest() - if plaintext_refresh_token is not None: + if plaintext_refresh_token: kwargs["hashed_refresh_token"] = hashlib.sha256( plaintext_refresh_token.encode() ).hexdigest() @@ -77,12 +77,6 @@ def create(self, *args, **kwargs): kwargs["token"] = plaintext_token kwargs["refresh_token"] = plaintext_refresh_token - if plaintext_refresh_token is not None: - kwargs["refresh_token"] = plaintext_refresh_token - kwargs["hashed_refresh_token"] = hashlib.sha256( - plaintext_refresh_token.encode() - ).hexdigest() - api_token = super().create(*args, **kwargs) # Store the plaintext tokens for one-time retrieval From fef4018301b12b6d02c718f095fa1aa32f2ca2ce Mon Sep 17 00:00:00 2001 From: mdtro <20070360+mdtro@users.noreply.github.com> Date: Wed, 3 Apr 2024 09:53:50 -0500 Subject: [PATCH 04/17] fix test --- tests/sentry/api/test_authentication.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/sentry/api/test_authentication.py b/tests/sentry/api/test_authentication.py index 698a8b8fe93961..90c4e2b970ed2c 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() From c68c59d61b8ae7d86a5cd76c0c29ef6cc83c46ab Mon Sep 17 00:00:00 2001 From: mdtro <20070360+mdtro@users.noreply.github.com> Date: Wed, 3 Apr 2024 14:26:57 -0500 Subject: [PATCH 05/17] overload update method --- src/sentry/models/apitoken.py | 44 ++++++++++++++++++++---- tests/sentry/models/test_apitoken.py | 50 +++++++++++++++++++++++++--- 2 files changed, 83 insertions(+), 11 deletions(-) diff --git a/src/sentry/models/apitoken.py b/src/sentry/models/apitoken.py index 3e3cc94b1f017d..1a235be828e29e 100644 --- a/src/sentry/models/apitoken.py +++ b/src/sentry/models/apitoken.py @@ -29,7 +29,10 @@ def default_expiration(): return timezone.now() + DEFAULT_EXPIRATION -def generate_token(): +def generate_token(token_type: AuthTokenType | None = AuthTokenType.__empty__) -> str: + if token_type: + return f"{token_type}{secrets.token_hex(nbytes=32)}" + return secrets.token_hex(nbytes=32) @@ -61,10 +64,15 @@ def create(self, *args, **kwargs): plaintext_refresh_token = generate_token() if token_type == AuthTokenType.USER: - plaintext_token = f"{token_type}{generate_token()}" + plaintext_token = generate_token(token_type=AuthTokenType.USER) plaintext_refresh_token = None # user auth tokens do not have refresh tokens else: - plaintext_token = generate_token() + # 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() @@ -85,6 +93,25 @@ def create(self, *args, **kwargs): return api_token + # This does not work... it's never called? + def update(self, *args, **kwargs) -> int: + raise Exception + # 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 "token" in kwargs: + kwargs["token_last_characters"] = kwargs["token"][-4:] + + return super().update(*args, **kwargs) + @control_silo_only_model class ApiToken(ReplicatedControlModel, HasApiScopes): @@ -157,7 +184,10 @@ def _plaintext_refresh_token(self): # if there is a hash value, then a refresh token is expected # and if the plaintext_refresh_token is None, then it has already # been read once so we should throw the exception - if not plaintext_refresh_token and self.refresh_token: + # + # we check for either the hashed or plaintext refresh token stored in the DB + # for backwards compatibility + if not plaintext_refresh_token and (self.hashed_refresh_token or self.refresh_token): raise PlaintextSecretAlreadyRead() return plaintext_refresh_token @@ -167,7 +197,7 @@ def save(self, *args: Any, **kwargs: Any) -> None: token_last_characters = self.token[-4:] self.token_last_characters = token_last_characters - return super().save(**kwargs) + return super().save(*args, **kwargs) def outbox_region_names(self) -> Collection[str]: return list(find_all_region_names()) @@ -224,9 +254,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/tests/sentry/models/test_apitoken.py b/tests/sentry/models/test_apitoken.py index a84adfbccd31d8..00f4a4f78ed69f 100644 --- a/tests/sentry/models/test_apitoken.py +++ b/tests/sentry/models/test_apitoken.py @@ -92,18 +92,39 @@ def test_hash_does_not_exist_on_user_token_with_option_off(self): 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, token_type=AuthTokenType.USER) + token = ApiToken.objects.create(user_id=user.id) assert token._plaintext_token is not None - assert token._plaintext_refresh_token is None # user auth tokens don't have refresh tokens + assert token._plaintext_refresh_token is not None - # we accessed the plaintext token above when we asserted it was not None - # accessing it again should throw an exception + # 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_user_auth_token_hash(self): user = self.create_user() @@ -111,6 +132,27 @@ def test_user_auth_token_hash(self): 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 + + initial_last_four = token.token_last_characters + + new_token = "abc1234" + new_token_expected_hash = hashlib.sha256(new_token.encode()).hexdigest() + + with assume_test_silo_mode(SiloMode.CONTROL): + with outbox_runner(): + to_update = ApiToken.objects.filter(id=token.id).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): From 43fa3ce6f8f61b8b5676f8d99600faad1a2c83e2 Mon Sep 17 00:00:00 2001 From: mdtro <20070360+mdtro@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:43:07 -0500 Subject: [PATCH 06/17] ref: revamp read-once tokens --- src/sentry/models/apitoken.py | 108 ++++++++++++++------------- tests/sentry/models/test_apitoken.py | 4 +- 2 files changed, 59 insertions(+), 53 deletions(-) diff --git a/src/sentry/models/apitoken.py b/src/sentry/models/apitoken.py index 1a235be828e29e..dac9164c83e86a 100644 --- a/src/sentry/models/apitoken.py +++ b/src/sentry/models/apitoken.py @@ -85,32 +85,35 @@ def create(self, *args, **kwargs): 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.__plaintext_token = plaintext_token - api_token.__plaintext_refresh_token = plaintext_refresh_token + self.__plaintext_token = plaintext_token + self.__plaintext_refresh_token = plaintext_refresh_token - return api_token + return super().create(*args, **kwargs) - # This does not work... it's never called? - def update(self, *args, **kwargs) -> int: - raise Exception - # 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() + @property + def plaintext_token(self): + plaintext_token = getattr(self, "_ApiTokenManager__plaintext_token", None) - if "refresh_token" in kwargs: - kwargs["hashed_refresh_token"] = hashlib.sha256( - kwargs["refresh_token"].encode() - ).hexdigest() + if plaintext_token: + setattr(self, "_ApiTokenManager__plaintext_token", None) + else: + raise PlaintextSecretAlreadyRead() - if "token" in kwargs: - kwargs["token_last_characters"] = kwargs["token"][-4:] + return plaintext_token - return super().update(*args, **kwargs) + @property + def plaintext_refresh_token(self): + plaintext_refresh_token: str | None = getattr( + self, "_ApiTokenManager__plaintext_refresh_token", None + ) + + if plaintext_refresh_token: + setattr(self, "_ApiTokenManager__plaintext_refresh_token", None) + else: + raise PlaintextSecretAlreadyRead() + + return plaintext_refresh_token @control_silo_only_model @@ -152,15 +155,7 @@ def _plaintext_token(self): to `None` to prevent future accidental leaking of the token in logs, exceptions, etc. """ - manager_class_name = self.objects.__class__.__name__ - plaintext_token: str | None = getattr(self, f"_{manager_class_name}__plaintext_token", None) - - if plaintext_token is not None: - setattr(self, f"_{manager_class_name}__plaintext_token", None) - else: - raise PlaintextSecretAlreadyRead() - - return plaintext_token + return ApiToken.objects.plaintext_token @property def _plaintext_refresh_token(self): @@ -170,35 +165,42 @@ def _plaintext_refresh_token(self): to `None` to prevent future accidental leaking of the refresh token in logs, exceptions, etc. """ - manager_class_name = self.objects.__class__.__name__ - plaintext_refresh_token: str | None = getattr( - self, f"_{manager_class_name}__plaintext_refresh_token", None - ) - - if plaintext_refresh_token: - setattr(self, f"_{manager_class_name}__plaintext_refresh_token", None) + if self.refresh_token or self.hashed_refresh_token: + return ApiToken.objects.plaintext_refresh_token + else: + raise NotImplementedError("This API token type does not support refresh tokens") - # some token types do not have refresh tokens, so we check to see - # if there's a hash value that exists for the refresh token. - # - # if there is a hash value, then a refresh token is expected - # and if the plaintext_refresh_token is None, then it has already - # been read once so we should throw the exception - # - # we check for either the hashed or plaintext refresh token stored in the DB - # for backwards compatibility - if not plaintext_refresh_token and (self.hashed_refresh_token or self.refresh_token): - raise PlaintextSecretAlreadyRead() + 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() - return plaintext_refresh_token + if self.refresh_token: + self.hashed_refresh_token = hashlib.sha256(self.refresh_token.encode()).hexdigest() - def save(self, *args: Any, **kwargs: Any) -> 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(*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()) @@ -233,10 +235,16 @@ def get_allowed_origins(self): return () def refresh(self, expires_at=None): + if self.token_type == AuthTokenType.USER: + raise NotImplementedError("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: diff --git a/tests/sentry/models/test_apitoken.py b/tests/sentry/models/test_apitoken.py index 00f4a4f78ed69f..020a0da391687f 100644 --- a/tests/sentry/models/test_apitoken.py +++ b/tests/sentry/models/test_apitoken.py @@ -139,14 +139,12 @@ def test_hash_updated_when_calling_update(self): initial_expected_hash = hashlib.sha256(token._plaintext_token.encode()).hexdigest() assert initial_expected_hash == token.hashed_token - initial_last_four = token.token_last_characters - new_token = "abc1234" new_token_expected_hash = hashlib.sha256(new_token.encode()).hexdigest() with assume_test_silo_mode(SiloMode.CONTROL): with outbox_runner(): - to_update = ApiToken.objects.filter(id=token.id).update(token=new_token) + token.update(token=new_token) token.refresh_from_db() From ed604909227132fdab2405f70d52c64ade7f9ba2 Mon Sep 17 00:00:00 2001 From: mdtro <20070360+mdtro@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:47:04 -0500 Subject: [PATCH 07/17] test: error on access refresh token on user token --- tests/sentry/models/test_apitoken.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/sentry/models/test_apitoken.py b/tests/sentry/models/test_apitoken.py index 020a0da391687f..d5d4462242dd55 100644 --- a/tests/sentry/models/test_apitoken.py +++ b/tests/sentry/models/test_apitoken.py @@ -125,6 +125,14 @@ def test_plaintext_values_only_available_immediately_after_create(self): 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(NotImplementedError): + assert token._plaintext_refresh_token is not None + @override_options({"apitoken.save-hash-on-create": True}) def test_user_auth_token_hash(self): user = self.create_user() From 2255c62f437ea38ad7457152343ff8728e21d699 Mon Sep 17 00:00:00 2001 From: mdtro <20070360+mdtro@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:56:46 -0500 Subject: [PATCH 08/17] only send refresh token if it is not a user auth token --- src/sentry/api/serializers/models/apitoken.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sentry/api/serializers/models/apitoken.py b/src/sentry/api/serializers/models/apitoken.py index 01197de5983738..68dc117a8d1ff0 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) @@ -31,7 +32,9 @@ def serialize(self, obj, attrs, user, **kwargs): include_token = kwargs.get("include_token", True) if include_token: data["token"] = obj._plaintext_token - data["refreshToken"] = obj._plaintext_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 From 6516671f61b4e3291762f47b92bb9b5db108eb72 Mon Sep 17 00:00:00 2001 From: mdtro <20070360+mdtro@users.noreply.github.com> Date: Wed, 3 Apr 2024 18:23:41 -0500 Subject: [PATCH 09/17] handle case when refresh_token is set to None after --- src/sentry/models/apitoken.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/sentry/models/apitoken.py b/src/sentry/models/apitoken.py index dac9164c83e86a..55ab0f45af0209 100644 --- a/src/sentry/models/apitoken.py +++ b/src/sentry/models/apitoken.py @@ -176,6 +176,11 @@ def save(self, *args: Any, **kwargs: Any) -> None: 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:] From 67e87d08c42e73221e8f73347708b4fd1caf7d37 Mon Sep 17 00:00:00 2001 From: mdtro <20070360+mdtro@users.noreply.github.com> Date: Wed, 3 Apr 2024 18:42:44 -0500 Subject: [PATCH 10/17] fix typing --- src/sentry/models/apitoken.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/models/apitoken.py b/src/sentry/models/apitoken.py index 55ab0f45af0209..6a455fd1e11802 100644 --- a/src/sentry/models/apitoken.py +++ b/src/sentry/models/apitoken.py @@ -29,7 +29,7 @@ def default_expiration(): return timezone.now() + DEFAULT_EXPIRATION -def generate_token(token_type: AuthTokenType | None = AuthTokenType.__empty__) -> str: +def generate_token(token_type: AuthTokenType | str | None = AuthTokenType.__empty__) -> str: if token_type: return f"{token_type}{secrets.token_hex(nbytes=32)}" From 6ceabf8209d63aed93b0b0f8db0fd2a3843d4d16 Mon Sep 17 00:00:00 2001 From: mdtro <20070360+mdtro@users.noreply.github.com> Date: Fri, 5 Apr 2024 12:19:42 -0500 Subject: [PATCH 11/17] use more appropriate exception name --- src/sentry/models/apitoken.py | 9 +++++++-- tests/sentry/models/test_apitoken.py | 21 ++++++++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/sentry/models/apitoken.py b/src/sentry/models/apitoken.py index 6a455fd1e11802..56c538a356de39 100644 --- a/src/sentry/models/apitoken.py +++ b/src/sentry/models/apitoken.py @@ -44,6 +44,11 @@ def __init__( super().__init__(message) +class NotSupported(Exception): + def __init__(self, message="the method you called is not supported by this token type"): + super().__init__(message) + + class ApiTokenManager(ControlOutboxProducingManager): def create(self, *args, **kwargs): token_type: AuthTokenType | None = kwargs.get("token_type", None) @@ -168,7 +173,7 @@ def _plaintext_refresh_token(self): if self.refresh_token or self.hashed_refresh_token: return ApiToken.objects.plaintext_refresh_token else: - raise NotImplementedError("This API token type does not support refresh tokens") + raise NotSupported("This API token type does not support refresh tokens") def save(self, *args: Any, **kwargs: Any) -> None: if options.get("apitoken.save-hash-on-create"): @@ -241,7 +246,7 @@ def get_allowed_origins(self): def refresh(self, expires_at=None): if self.token_type == AuthTokenType.USER: - raise NotImplementedError("User auth tokens do not support refreshing the token") + raise NotSupported("User auth tokens do not support refreshing the token") if expires_at is None: expires_at = timezone.now() + DEFAULT_EXPIRATION diff --git a/tests/sentry/models/test_apitoken.py b/tests/sentry/models/test_apitoken.py index d5d4462242dd55..251eb3db9c561e 100644 --- a/tests/sentry/models/test_apitoken.py +++ b/tests/sentry/models/test_apitoken.py @@ -6,7 +6,7 @@ from sentry.conf.server import SENTRY_SCOPE_HIERARCHY_MAPPING, SENTRY_SCOPES from sentry.hybridcloud.models import ApiTokenReplica -from sentry.models.apitoken import ApiToken, PlaintextSecretAlreadyRead +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 @@ -77,6 +77,13 @@ 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() @@ -130,11 +137,19 @@ 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(NotImplementedError): + 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_hash(self): + 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() From 54abc5dda9c77118e2372efe192a5e308dd78771 Mon Sep 17 00:00:00 2001 From: mdtro <20070360+mdtro@users.noreply.github.com> Date: Fri, 5 Apr 2024 12:43:47 -0500 Subject: [PATCH 12/17] test refresh tokens serialization --- tests/sentry/api/serializers/test_apitoken.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) 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) From fd768b76dc7b77287d40941b81b1279cd6b42d78 Mon Sep 17 00:00:00 2001 From: mdtro <20070360+mdtro@users.noreply.github.com> Date: Wed, 10 Apr 2024 16:22:37 -0500 Subject: [PATCH 13/17] simplify exception classes --- src/sentry/models/apitoken.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/sentry/models/apitoken.py b/src/sentry/models/apitoken.py index 56c538a356de39..7c00e3a15ce878 100644 --- a/src/sentry/models/apitoken.py +++ b/src/sentry/models/apitoken.py @@ -37,16 +37,15 @@ def generate_token(token_type: AuthTokenType | str | None = AuthTokenType.__empt class PlaintextSecretAlreadyRead(Exception): - def __init__( - self, - message="the secret you are trying to read is read-once and cannot be accessed directly again", - ): - super().__init__(message) + """the secret you are trying to read is read-once and cannot be accessed directly again""" + + pass class NotSupported(Exception): - def __init__(self, message="the method you called is not supported by this token type"): - super().__init__(message) + """the method you called is not supported by this token type""" + + pass class ApiTokenManager(ControlOutboxProducingManager): From d5057c68667358fe29e7917bad099316309e152b Mon Sep 17 00:00:00 2001 From: mdtro <20070360+mdtro@users.noreply.github.com> Date: Thu, 11 Apr 2024 18:10:30 -0500 Subject: [PATCH 14/17] ref: read-once logic on apitoken - Setting the plaintext values on the manager class is wrong and would cause issues when creating multiple instances of ApiToken. - it results in the plaintext value always being the latest instance of ApiToken that was created - moving this to the model fixes the issue - introduce setter functions for the plaintext token values - add docstrings - remove leading `_` on property functions that return the the plaintext token values - after reading, set the token to string value stored in `TOKEN_REDACTED` so it can still be printed and we can search for the string in log data where accidental leaks may happen - we still throw `PlaintextSecretAlreadyRead` when attempting to read the value more than once --- src/sentry/api/serializers/models/apitoken.py | 4 +- src/sentry/models/apitoken.py | 120 +++++++++++------- tests/sentry/models/test_apitoken.py | 22 ++-- 3 files changed, 89 insertions(+), 57 deletions(-) diff --git a/src/sentry/api/serializers/models/apitoken.py b/src/sentry/api/serializers/models/apitoken.py index 68dc117a8d1ff0..13d206a8feb86c 100644 --- a/src/sentry/api/serializers/models/apitoken.py +++ b/src/sentry/api/serializers/models/apitoken.py @@ -31,10 +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._plaintext_token + data["token"] = obj.plaintext_token if not obj.token_type == AuthTokenType.USER: - data["refreshToken"] = obj._plaintext_refresh_token + 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 7c00e3a15ce878..3f988cda6cf193 100644 --- a/src/sentry/models/apitoken.py +++ b/src/sentry/models/apitoken.py @@ -23,6 +23,7 @@ from sentry.types.token import AuthTokenType DEFAULT_EXPIRATION = timedelta(days=30) +TOKEN_REDACTED = "***REDACTED***" def default_expiration(): @@ -60,8 +61,8 @@ def create(self, *args, **kwargs): # 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. + # 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: @@ -89,35 +90,13 @@ def create(self, *args, **kwargs): kwargs["token"] = plaintext_token kwargs["refresh_token"] = plaintext_refresh_token - # Store the plaintext tokens for one-time retrieval - self.__plaintext_token = plaintext_token - self.__plaintext_refresh_token = plaintext_refresh_token - - return super().create(*args, **kwargs) + api_token = super().create(*args, **kwargs) - @property - def plaintext_token(self): - plaintext_token = getattr(self, "_ApiTokenManager__plaintext_token", None) - - if plaintext_token: - setattr(self, "_ApiTokenManager__plaintext_token", None) - else: - raise PlaintextSecretAlreadyRead() - - return plaintext_token - - @property - def plaintext_refresh_token(self): - plaintext_refresh_token: str | None = getattr( - self, "_ApiTokenManager__plaintext_refresh_token", None - ) - - if plaintext_refresh_token: - setattr(self, "_ApiTokenManager__plaintext_refresh_token", None) - else: - raise PlaintextSecretAlreadyRead() + # 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 plaintext_refresh_token + return api_token @control_silo_only_model @@ -151,29 +130,82 @@ class Meta: def __str__(self): return force_str(self.token) - @property - def _plaintext_token(self): + 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 """ - To be called immediately after creation of a new token to return the - plaintext token to the user. After reading the token, it will be set - to `None` to prevent future accidental leaking of the token in logs, - exceptions, etc. + existing_token = None + try: + existing_token = self.__plaintext_token + except AttributeError: + self.__plaintext_token = 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 """ - return ApiToken.objects.plaintext_token + existing_refresh_token = None + try: + existing_refresh_token = self.__plaintext_refresh_token + except AttributeError: + self.__plaintext_refresh_token = token + + if existing_refresh_token == TOKEN_REDACTED: + raise PlaintextSecretAlreadyRead() @property - def _plaintext_refresh_token(self): + 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 """ - To be called immediately after creation of a new token to return the - plaintext refresh token to the user. After reading the refresh token, it will be set - to `None` to prevent future accidental leaking of the refresh token in logs, - exceptions, etc. + 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 self.refresh_token or self.hashed_refresh_token: - return ApiToken.objects.plaintext_refresh_token - else: + 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() diff --git a/tests/sentry/models/test_apitoken.py b/tests/sentry/models/test_apitoken.py index 251eb3db9c561e..72931dc9a9e5f8 100644 --- a/tests/sentry/models/test_apitoken.py +++ b/tests/sentry/models/test_apitoken.py @@ -106,31 +106,31 @@ def test_can_access_read_once_tokens_with_option_off(self): 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 + 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 + _ = token.plaintext_token with pytest.raises(PlaintextSecretAlreadyRead): - _ = token._plaintext_refresh_token + _ = 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 + 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 + _ = token.plaintext_token with pytest.raises(PlaintextSecretAlreadyRead): - _ = token._plaintext_refresh_token + _ = token.plaintext_refresh_token @override_options({"apitoken.save-hash-on-create": True}) def test_error_when_accessing_refresh_token_on_user_token(self): @@ -138,7 +138,7 @@ def test_error_when_accessing_refresh_token_on_user_token(self): token = ApiToken.objects.create(user_id=user.id, token_type=AuthTokenType.USER) with pytest.raises(NotSupported): - assert token._plaintext_refresh_token is not None + 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): @@ -152,14 +152,14 @@ def test_user_auth_token_refresh_raises_error(self): 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() + 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() + initial_expected_hash = hashlib.sha256(token.plaintext_token.encode()).hexdigest() assert initial_expected_hash == token.hashed_token new_token = "abc1234" From 7f8b9da5c27f4ef92f05310632ebf0ee41bc1d84 Mon Sep 17 00:00:00 2001 From: mdtro <20070360+mdtro@users.noreply.github.com> Date: Mon, 15 Apr 2024 10:43:33 -0500 Subject: [PATCH 15/17] mypy fix and test fixes - update tests to access correct property - ignore typing error --- src/sentry/models/apitoken.py | 4 ++-- tests/sentry/api/test_authentication.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sentry/models/apitoken.py b/src/sentry/models/apitoken.py index 3f988cda6cf193..beaa4632c340b9 100644 --- a/src/sentry/models/apitoken.py +++ b/src/sentry/models/apitoken.py @@ -140,7 +140,7 @@ def _set_plaintext_token(self, token: str) -> None: """ existing_token = None try: - existing_token = self.__plaintext_token + existing_token = self.__plaintext_token # type: ignore[has-type] except AttributeError: self.__plaintext_token = token @@ -157,7 +157,7 @@ def _set_plaintext_refresh_token(self, token: str) -> None: """ existing_refresh_token = None try: - existing_refresh_token = self.__plaintext_refresh_token + existing_refresh_token = self.__plaintext_refresh_token # type: ignore[has-type] except AttributeError: self.__plaintext_refresh_token = token diff --git a/tests/sentry/api/test_authentication.py b/tests/sentry/api/test_authentication.py index 90c4e2b970ed2c..55fb5dc39289db 100644 --- a/tests/sentry/api/test_authentication.py +++ b/tests/sentry/api/test_authentication.py @@ -185,7 +185,7 @@ def setUp(self): token_type=AuthTokenType.USER, user=self.user, ) - self.token = self.api_token._plaintext_token + self.token = self.api_token.plaintext_token def test_authenticate(self): request = HttpRequest() From ac3e130b35d622375643cdd23f0c7651356c0709 Mon Sep 17 00:00:00 2001 From: mdtro <20070360+mdtro@users.noreply.github.com> Date: Wed, 17 Apr 2024 12:55:47 -0500 Subject: [PATCH 16/17] add type hints --- src/sentry/models/apitoken.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/sentry/models/apitoken.py b/src/sentry/models/apitoken.py index beaa4632c340b9..fef4baaed74120 100644 --- a/src/sentry/models/apitoken.py +++ b/src/sentry/models/apitoken.py @@ -138,11 +138,11 @@ def _set_plaintext_token(self, token: str) -> None: :param token: A plaintext string of the token :raises PlaintextSecretAlreadyRead: when the token has already been read once """ - existing_token = None + existing_token: str | None = None try: - existing_token = self.__plaintext_token # type: ignore[has-type] + existing_token = self.__plaintext_token except AttributeError: - self.__plaintext_token = token + self.__plaintext_token: str = token if existing_token == TOKEN_REDACTED: raise PlaintextSecretAlreadyRead() @@ -155,11 +155,11 @@ def _set_plaintext_refresh_token(self, token: str) -> None: :param token: A plaintext string of the refresh token :raises PlaintextSecretAlreadyRead: if the token has already been read once """ - existing_refresh_token = None + existing_refresh_token: str | None = None try: - existing_refresh_token = self.__plaintext_refresh_token # type: ignore[has-type] + existing_refresh_token = self.__plaintext_refresh_token except AttributeError: - self.__plaintext_refresh_token = token + self.__plaintext_refresh_token: str = token if existing_refresh_token == TOKEN_REDACTED: raise PlaintextSecretAlreadyRead() From 42bb344a976860ff365c91e0cf16668b7007511f Mon Sep 17 00:00:00 2001 From: mdtro <20070360+mdtro@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:07:45 -0500 Subject: [PATCH 17/17] regenerate backup snapshot --- .../ReleaseTests/test_at_head.pysnap | 632 +++++++++--------- 1 file changed, 316 insertions(+), 316 deletions(-) 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