diff --git a/migrations_lockfile.txt b/migrations_lockfile.txt index 35434f9975e725..0b4246e12a976f 100644 --- a/migrations_lockfile.txt +++ b/migrations_lockfile.txt @@ -9,5 +9,5 @@ feedback: 0003_feedback_add_env hybridcloud: 0007_add_orgauthtokenreplica nodestore: 0002_nodestore_no_dictfield replays: 0003_add_size_to_recording_segment -sentry: 0583_add_early_adopter_to_organization_mapping +sentry: 0585_apitoken_backfill_last_chars social_auth: 0002_default_auto_field diff --git a/src/sentry/migrations/0584_apitoken_add_name_and_last_four.py b/src/sentry/migrations/0584_apitoken_add_name_and_last_four.py new file mode 100644 index 00000000000000..dfb22c2601fa40 --- /dev/null +++ b/src/sentry/migrations/0584_apitoken_add_name_and_last_four.py @@ -0,0 +1,36 @@ +# Generated by Django 3.2.20 on 2023-10-24 23:47 + +from django.db import migrations, models + +from sentry.new_migrations.migrations import CheckedMigration + + +class Migration(CheckedMigration): + # This flag is used to mark that a migration shouldn't be automatically run in production. For + # the most part, this should only be used for operations where it's safe to run the migration + # after your code has deployed. So this should not be used for most operations that alter the + # schema of a table. + # Here are some things that make sense to mark as dangerous: + # - Large data migrations. Typically we want these to be run manually by ops so that they can + # be monitored and not block the deploy for a long period of time while they run. + # - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to + # have ops run this and not block the deploy. Note that while adding an index is a schema + # change, it's completely safe to run the operation after the code has deployed. + is_dangerous = False + + dependencies = [ + ("sentry", "0583_add_early_adopter_to_organization_mapping"), + ] + + operations = [ + migrations.AddField( + model_name="apitoken", + name="name", + field=models.CharField(max_length=255, null=True), + ), + migrations.AddField( + model_name="apitoken", + name="token_last_characters", + field=models.CharField(max_length=4, null=True), + ), + ] diff --git a/src/sentry/migrations/0585_apitoken_backfill_last_chars.py b/src/sentry/migrations/0585_apitoken_backfill_last_chars.py new file mode 100644 index 00000000000000..ae9f0b9444a562 --- /dev/null +++ b/src/sentry/migrations/0585_apitoken_backfill_last_chars.py @@ -0,0 +1,40 @@ +# Generated by Django 3.2.20 on 2023-10-24 23:51 + +from django.db import migrations + +from sentry.new_migrations.migrations import CheckedMigration +from sentry.utils.query import RangeQuerySetWrapperWithProgressBar + + +def backfill_last_token_characters(apps, _): + ApiToken = apps.get_model("sentry", "ApiToken") + for api_token in RangeQuerySetWrapperWithProgressBar(ApiToken.objects.all()): + last_four = api_token.token[-4:] + api_token.token_last_characters = last_four + api_token.save() + + +class Migration(CheckedMigration): + # This flag is used to mark that a migration shouldn't be automatically run in production. For + # the most part, this should only be used for operations where it's safe to run the migration + # after your code has deployed. So this should not be used for most operations that alter the + # schema of a table. + # Here are some things that make sense to mark as dangerous: + # - Large data migrations. Typically we want these to be run manually by ops so that they can + # be monitored and not block the deploy for a long period of time while they run. + # - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to + # have ops run this and not block the deploy. Note that while adding an index is a schema + # change, it's completely safe to run the operation after the code has deployed. + is_dangerous = False + + dependencies = [ + ("sentry", "0584_apitoken_add_name_and_last_four"), + ] + + operations = [ + migrations.RunPython( + backfill_last_token_characters, + migrations.RunPython.noop, + hints={"tables": ["sentry_apitoken"]}, + ), + ] diff --git a/src/sentry/models/apitoken.py b/src/sentry/models/apitoken.py index 3c71b85170c878..bb50afac44cf1d 100644 --- a/src/sentry/models/apitoken.py +++ b/src/sentry/models/apitoken.py @@ -37,7 +37,9 @@ class ApiToken(ReplicatedControlModel, HasApiScopes): # users can generate tokens without being application-bound application = FlexibleForeignKey("sentry.ApiApplication", null=True) user = FlexibleForeignKey("sentry.User") + name = models.CharField(max_length=255, null=True) token = models.CharField(max_length=64, unique=True, default=generate_token) + token_last_characters = models.CharField(max_length=4, null=True) refresh_token = models.CharField(max_length=64, unique=True, null=True, default=generate_token) expires_at = models.DateTimeField(null=True, default=default_expiration) date_added = models.DateTimeField(default=timezone.now) diff --git a/tests/sentry/migrations/test_0585_apitoken_backfill_last_chars.py b/tests/sentry/migrations/test_0585_apitoken_backfill_last_chars.py new file mode 100644 index 00000000000000..45adcae78b89c2 --- /dev/null +++ b/tests/sentry/migrations/test_0585_apitoken_backfill_last_chars.py @@ -0,0 +1,32 @@ +from django.db import router + +from sentry.silo import unguarded_write +from sentry.testutils.cases import TestMigrations + + +class NameLastCharsApiTokenMigrationTest(TestMigrations): + migrate_from = "0584_apitoken_add_name_and_last_four" + migrate_to = "0585_apitoken_backfill_last_chars" + connection = "control" + + def setUp(self): + from sentry.models.apitoken import ApiToken + + with unguarded_write(using=router.db_for_write(ApiToken)): + super().setUp() + + def setup_before_migration(self, apps): + ApiToken = apps.get_model("sentry", "ApiToken") + self.api_token = ApiToken.objects.create( + user_id=self.user.id, + refresh_token=None, + ) + self.api_token.save() + + def test(self): + from sentry.models.apitoken import ApiToken + + api_tokens = ApiToken.objects.all() + for api_token in api_tokens: + assert api_token.name is None + assert api_token.token_last_characters == api_token.token[-4:]