From c864e93fde68abf3da2ec56e5c7c4b389e583216 Mon Sep 17 00:00:00 2001 From: Will Kahn-Greene Date: Thu, 16 May 2024 08:58:35 -0400 Subject: [PATCH] Fix token admin headers and add createtoken command This fixes the token admin headers like we did with Tecken. Further, it adds a createtoken command like Tecken has allowing us to create tokens from the command line in the local dev environment. --- webapp/crashstats/tokens/admin.py | 3 + .../crashstats/tokens/management/__init__.py | 3 + .../tokens/management/commands/__init__.py | 3 + .../tokens/management/commands/createtoken.py | 57 +++++++++++++++++++ .../tokens/tests/test_createtoken.py | 42 ++++++++++++++ 5 files changed, 108 insertions(+) create mode 100644 webapp/crashstats/tokens/management/__init__.py create mode 100644 webapp/crashstats/tokens/management/commands/__init__.py create mode 100644 webapp/crashstats/tokens/management/commands/createtoken.py create mode 100644 webapp/crashstats/tokens/tests/test_createtoken.py diff --git a/webapp/crashstats/tokens/admin.py b/webapp/crashstats/tokens/admin.py index a2a209f5eb..14d5f34cad 100644 --- a/webapp/crashstats/tokens/admin.py +++ b/webapp/crashstats/tokens/admin.py @@ -20,11 +20,14 @@ class TokenAdmin(admin.ModelAdmin): list_filter = ["permissions"] search_fields = ["user__email", "notes"] + @admin.display(description="Key") def key_truncated(self, obj): return obj.key[:12] + "..." + @admin.display(description="Permissions") def get_permissions(self, obj): return ", ".join(perm.codename for perm in obj.permissions.all()) + @admin.display(description="Email") def get_user_email(self, obj): return obj.user.email diff --git a/webapp/crashstats/tokens/management/__init__.py b/webapp/crashstats/tokens/management/__init__.py new file mode 100644 index 0000000000..448bb8652d --- /dev/null +++ b/webapp/crashstats/tokens/management/__init__.py @@ -0,0 +1,3 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. diff --git a/webapp/crashstats/tokens/management/commands/__init__.py b/webapp/crashstats/tokens/management/commands/__init__.py new file mode 100644 index 0000000000..448bb8652d --- /dev/null +++ b/webapp/crashstats/tokens/management/commands/__init__.py @@ -0,0 +1,3 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. diff --git a/webapp/crashstats/tokens/management/commands/createtoken.py b/webapp/crashstats/tokens/management/commands/createtoken.py new file mode 100644 index 0000000000..b188376e55 --- /dev/null +++ b/webapp/crashstats/tokens/management/commands/createtoken.py @@ -0,0 +1,57 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from django.contrib.auth.models import Permission, User +from django.core.management.base import BaseCommand, CommandError + +from crashstats.tokens.models import make_key, Token + + +class Command(BaseCommand): + help = "Create an API token." + + def add_arguments(self, parser): + parser.add_argument("email") + parser.add_argument("token_key", default=None, nargs="?") + parser.add_argument( + "--try-upload", + action="store_true", + help="If true, create the token with Upload Try Symbols", + ) + + def handle(self, *args, **options): + email = options["email"] + + token_key = options["token_key"] + if not token_key: + token_key = make_key() + + try: + user = User.objects.get(email__iexact=email) + except User.DoesNotExist: + raise CommandError(f"Account {email!r} does not exist.") from None + + if Token.objects.filter(user=user, key=token_key).exists(): + raise CommandError(f"Token with key {token_key!r} already exists") + + # Add permissions to token that user has + permissions_to_add = [ + "view_pii", + "view_rawdump", + "reprocess_crashes", + ] + permissions = [ + Permission.objects.get(codename=permission) + for permission in permissions_to_add + if user.has_perm(permission) + ] + + token = Token.objects.create( + user=user, + key=token_key, + ) + self.stdout.write(self.style.SUCCESS(f"{token_key} created")) + for permission in permissions: + token.permissions.add(permission) + self.stdout.write(self.style.SUCCESS(f"{permission} added")) diff --git a/webapp/crashstats/tokens/tests/test_createtoken.py b/webapp/crashstats/tokens/tests/test_createtoken.py new file mode 100644 index 0000000000..506c8a8a78 --- /dev/null +++ b/webapp/crashstats/tokens/tests/test_createtoken.py @@ -0,0 +1,42 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from io import StringIO + +import pytest + +from django.core.management import call_command +from django.core.management.base import CommandError + +from crashstats.tokens.models import make_key, Token + + +@pytest.mark.django_db +def test_createtoken_no_key_command(): + assert Token.objects.all().count() == 0 + stdout = StringIO() + call_command("makesuperuser", "foo@example.com", stdout=stdout) + stdout = StringIO() + call_command("createtoken", "foo@example.com", stdout=stdout) + + assert Token.objects.all().count() == 1 + + +@pytest.mark.django_db +def test_createtoken_with_key_command(): + assert Token.objects.all().count() == 0 + stdout = StringIO() + call_command("makesuperuser", "foo@example.com", stdout=stdout) + stdout = StringIO() + token_key = make_key() + call_command("createtoken", "foo@example.com", token_key, stdout=stdout) + + assert Token.objects.filter(key=token_key).count() == 1 + + +@pytest.mark.django_db +def test_createtoken_command_no_user(): + with pytest.raises(CommandError): + stdout = StringIO() + call_command("createtoken", "foo@example.com", stdout=stdout)