Просмотр исходного кода

Add API Tokens support (can be created, doesn't do anything useful yet)

David Burke 4 лет назад
Родитель
Сommit
0595742b67

+ 0 - 0
api_tokens/__init__.py


+ 17 - 0
api_tokens/admin.py

@@ -0,0 +1,17 @@
+from django.contrib import admin
+from bitfield import BitField
+from bitfield.forms import BitFieldCheckboxSelectMultiple
+from .models import APIToken
+
+
+class APITokenAdmin(admin.ModelAdmin):
+    raw_id_fields = ("user",)
+    list_display = ("token", "user", "label")
+    readonly_fields = ("token",)
+    formfield_overrides = {
+        BitField: {"widget": BitFieldCheckboxSelectMultiple},
+    }
+
+
+admin.site.register(APIToken, APITokenAdmin)
+

+ 30 - 0
api_tokens/migrations/0001_initial.py

@@ -0,0 +1,30 @@
+# Generated by Django 3.1 on 2020-08-16 15:56
+
+import api_tokens.models
+import bitfield.models
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='APIToken',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('token', models.CharField(default=api_tokens.models.generate_token, max_length=40, unique=True)),
+                ('label', models.CharField(blank=True, max_length=255)),
+                ('scopes', bitfield.models.BitField(('project:read', 'project:write', 'project:admin', 'project:releases', 'team:read', 'team:write', 'team:admin', 'event:read', 'event:write', 'event:admin', 'org:read', 'org:write', 'org:admin', 'member:read', 'member:write', 'member:admin'), default=None)),
+                ('created', models.DateTimeField(auto_now_add=True)),
+                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+            ],
+        ),
+    ]

+ 0 - 0
api_tokens/migrations/__init__.py


+ 47 - 0
api_tokens/models.py

@@ -0,0 +1,47 @@
+import binascii
+import os
+
+from django.conf import settings
+from django.db import models
+
+from bitfield import BitField
+
+
+def generate_token():
+    return binascii.hexlify(os.urandom(20)).decode()
+
+
+class APIToken(models.Model):
+    """
+    Ideas borrowed from rest_framework.authtoken and sentry.apitoken
+    """
+
+    token = models.CharField(
+        max_length=40, unique=True, editable=False, default=generate_token
+    )
+    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
+    label = models.CharField(max_length=255, blank=True)
+    scopes = BitField(
+        flags=(
+            "project:read",
+            "project:write",
+            "project:admin",
+            "project:releases",
+            "team:read",
+            "team:write",
+            "team:admin",
+            "event:read",
+            "event:write",
+            "event:admin",
+            "org:read",
+            "org:write",
+            "org:admin",
+            "member:read",
+            "member:write",
+            "member:admin",
+        )
+    )
+    created = models.DateTimeField(auto_now_add=True)
+
+    def __str__(self):
+        return self.token

+ 35 - 0
api_tokens/serializers.py

@@ -0,0 +1,35 @@
+from rest_framework import serializers
+from bitfield.types import BitHandler
+from .models import APIToken
+
+
+class BitFieldSerializer(serializers.Field):
+    """
+    BitField model field serializer
+
+    Displays as list of true flag
+
+    Semi inspired from django-bitfield bitfield/forms.py
+    """
+
+    def to_internal_value(self, data):
+        model_field = getattr(self.root.Meta.model, self.source)
+        result = BitHandler(0, model_field.keys())
+        for k in data:
+            try:
+                setattr(result, str(k), True)
+            except AttributeError:
+                raise serializers.ValidationError("Unknown choice: %r" % (k,))
+        return result
+
+    def to_representation(self, value):
+        return [i[0] for i in value.items() if i[1] is True]
+
+
+class APITokenSerializer(serializers.ModelSerializer):
+    dateCreated = serializers.DateTimeField(source="created", read_only=True)
+    scopes = BitFieldSerializer()
+
+    class Meta:
+        model = APIToken
+        fields = ("scopes", "dateCreated", "label", "token", "id")

+ 50 - 0
api_tokens/tests.py

@@ -0,0 +1,50 @@
+from django.urls import reverse
+from rest_framework.test import APITestCase
+from model_bakery import baker
+
+
+class APITokenTests(APITestCase):
+    def setUp(self):
+        self.user = baker.make("users.user")
+
+    def test_create(self):
+        self.client.force_login(self.user)
+        url = reverse("api-tokens-list")
+        scope_name = "member:read"
+        data = {"scopes": [scope_name]}
+        res = self.client.post(url, data, format="json")
+        self.assertContains(res, scope_name, status_code=201)
+
+    def test_list(self):
+        self.client.force_login(self.user)
+        api_token = baker.make("api_tokens.APIToken", user=self.user)
+        other_api_token = baker.make("api_tokens.APIToken")
+        url = reverse("api-tokens-list")
+        res = self.client.get(url)
+        self.assertContains(res, api_token.token)
+        self.assertNotContains(res, other_api_token.token)
+
+    def test_retrieve(self):
+        self.client.force_login(self.user)
+        api_token = baker.make("api_tokens.APIToken", user=self.user)
+        url = reverse("api-tokens-detail", args=[api_token.id])
+        res = self.client.get(url)
+        self.assertContains(res, api_token.token)
+
+        other_api_token = baker.make("api_tokens.APIToken")
+        res = self.client.get(reverse("api-tokens-detail", args=[other_api_token.id]))
+        self.assertEqual(res.status_code, 404)
+
+    def test_destroy(self):
+        self.client.force_login(self.user)
+        api_token = baker.make("api_tokens.APIToken", user=self.user)
+        url = reverse("api-tokens-detail", args=[api_token.id])
+        self.assertTrue(self.user.apitoken_set.exists())
+        res = self.client.delete(url)
+        self.assertEqual(res.status_code, 204)
+        self.assertFalse(self.user.apitoken_set.exists())
+
+        other_api_token = baker.make("api_tokens.APIToken")
+        url = reverse("api-tokens-detail", args=[other_api_token.id])
+        res = self.client.delete(url)
+        self.assertEqual(res.status_code, 404)

+ 11 - 0
api_tokens/urls.py

@@ -0,0 +1,11 @@
+from django.urls import path, include
+from rest_framework import routers
+from .views import APITokenViewSet
+
+router = routers.SimpleRouter()
+router.register(r"api-tokens", APITokenViewSet, basename="api-tokens")
+
+
+urlpatterns = [
+    path("", include(router.urls)),
+]

+ 17 - 0
api_tokens/views.py

@@ -0,0 +1,17 @@
+from rest_framework import viewsets
+from .models import APIToken
+from .serializers import APITokenSerializer
+
+
+class APITokenViewSet(viewsets.ModelViewSet):
+    queryset = APIToken.objects.all()
+    serializer_class = APITokenSerializer
+
+    def get_queryset(self):
+        if not self.request.user.is_authenticated:
+            return self.queryset.none()
+        return self.queryset.filter(user=self.request.user)
+
+    def perform_create(self, serializer):
+        serializer.save(user=self.request.user)
+

+ 1 - 0
glitchtip/settings.py

@@ -133,6 +133,7 @@ INSTALLED_APPS = [
     "storages",
     "glitchtip",
     "alerts",
+    "api_tokens",
     "organizations_ext",
     "event_store",
     "issues",

Некоторые файлы не были показаны из-за большого количества измененных файлов