Skip to content

Commit 20b91f7

Browse files
Turn prefix and hashed_key into proper APIKey fields
1 parent 290548c commit 20b91f7

File tree

10 files changed

+168
-24
lines changed

10 files changed

+168
-24
lines changed

rest_framework_api_key/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88
class APIKeyModelAdmin(admin.ModelAdmin):
99
list_display = (
10-
"name",
1110
"prefix",
11+
"name",
1212
"created",
1313
"expiry_date",
1414
"_has_expired",

rest_framework_api_key/crypto.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@
77
from .models import APIKey
88

99

10+
def concatenate(left: str, right: str) -> str:
11+
return "{}.{}".format(left, right)
12+
13+
14+
def split(concatenated: str) -> Tuple[str, str]:
15+
left, _, right = concatenated.partition(".")
16+
return left, right
17+
18+
1019
class KeyGenerator:
1120
def __init__(self, prefix_length: int = 8, secret_key_length: int = 32):
1221
self.prefix_length = prefix_length
@@ -18,16 +27,15 @@ def get_prefix(self) -> str:
1827
def get_secret_key(self) -> str:
1928
return get_random_string(self.secret_key_length)
2029

21-
@staticmethod
22-
def concatenate(left: str, right: str) -> str:
23-
return "{}.{}".format(left, right)
30+
def hash(self, value: str) -> str:
31+
return make_password(value)
2432

25-
def generate(self) -> Tuple[str, str]:
33+
def generate(self) -> Tuple[str, str, str]:
2634
prefix = self.get_prefix()
2735
secret_key = self.get_secret_key()
28-
key = self.concatenate(prefix, secret_key)
29-
hashed_key = self.concatenate(prefix, make_password(key))
30-
return key, hashed_key
36+
key = concatenate(prefix, secret_key)
37+
hashed_key = self.hash(key)
38+
return key, prefix, hashed_key
3139

3240
def verify(self, key: str, hashed_key: str) -> bool:
3341
return check_password(key, hashed_key)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Generated by Django 2.2.2 on 2019-06-29 10:38
2+
3+
from django.db import migrations, models
4+
5+
APP_NAME = "rest_framework_api_key"
6+
MODEL_NAME = "apikey"
7+
DEPENDENCIES = [(APP_NAME, "0003_auto_20190623_1952")]
8+
9+
10+
def populate_prefix_hashed_key(apps, schema_editor):
11+
model = apps.get_model(APP_NAME, MODEL_NAME)
12+
13+
for api_key in model.objects.all():
14+
prefix, _, hashed_key = api_key.id.partition(".")
15+
api_key.prefix = prefix
16+
api_key.hashed_key = hashed_key
17+
api_key.save()
18+
19+
20+
class Migration(migrations.Migration):
21+
22+
dependencies = DEPENDENCIES
23+
24+
operations = [
25+
migrations.AddField(
26+
model_name=MODEL_NAME,
27+
name="hashed_key",
28+
field=models.CharField(max_length=100, null=True),
29+
),
30+
migrations.AddField(
31+
model_name=MODEL_NAME,
32+
name="prefix",
33+
field=models.CharField(max_length=8, unique=True, null=True),
34+
),
35+
migrations.RunPython(
36+
populate_prefix_hashed_key, migrations.RunPython.noop
37+
),
38+
migrations.AlterField(
39+
model_name=MODEL_NAME,
40+
name="hashed_key",
41+
field=models.CharField(max_length=100, editable=False),
42+
),
43+
migrations.AlterField(
44+
model_name=MODEL_NAME,
45+
name="prefix",
46+
field=models.CharField(max_length=8, unique=True, editable=False),
47+
),
48+
]

rest_framework_api_key/models.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,26 @@
44
from django.db import models
55
from django.utils import timezone
66

7-
from .crypto import KeyGenerator
7+
from .crypto import concatenate, KeyGenerator, split
88

99

1010
class BaseAPIKeyManager(models.Manager):
1111
key_generator = KeyGenerator()
1212

1313
def assign_key(self, obj: "AbstractAPIKey") -> str:
14-
key, hashed_key = self.key_generator.generate()
15-
obj.id = hashed_key
14+
try:
15+
key, prefix, hashed_key = self.key_generator.generate()
16+
except TypeError: # Compatibility with < 1.4
17+
key, hashed_key = self.key_generator.generate()
18+
pk = hashed_key
19+
prefix, hashed_key = split(hashed_key)
20+
else:
21+
pk = concatenate(prefix, hashed_key)
22+
23+
obj.id = pk
24+
obj.prefix = prefix
25+
obj.hashed_key = hashed_key
26+
1627
return key
1728

1829
def create_key(self, **kwargs) -> Tuple["AbstractAPIKey", str]:
@@ -32,10 +43,8 @@ def is_valid(self, key: str) -> bool:
3243
queryset = self.get_usable_keys()
3344

3445
try:
35-
api_key = queryset.get(
36-
id__startswith=prefix
37-
) # type: AbstractAPIKey
38-
except (self.model.DoesNotExist, self.model.MultipleObjectsReturned):
46+
api_key = queryset.get(prefix=prefix) # type: AbstractAPIKey
47+
except self.model.DoesNotExist:
3948
return False
4049

4150
if not api_key.is_valid(key):
@@ -57,6 +66,8 @@ class AbstractAPIKey(models.Model):
5766
id = models.CharField(
5867
max_length=100, unique=True, primary_key=True, editable=False
5968
)
69+
prefix = models.CharField(max_length=8, unique=True, editable=False)
70+
hashed_key = models.CharField(max_length=100, editable=False)
6071
created = models.DateTimeField(auto_now_add=True, db_index=True)
6172
name = models.CharField(
6273
max_length=50,
@@ -94,12 +105,6 @@ def __init__(self, *args, **kwargs):
94105
# Store the initial value of `revoked` to detect changes.
95106
self._initial_revoked = self.revoked
96107

97-
def _prefix(self) -> str:
98-
return self.pk.partition(".")[0]
99-
100-
_prefix.short_description = "Prefix"
101-
prefix = property(_prefix)
102-
103108
def _has_expired(self) -> bool:
104109
if self.expiry_date is None:
105110
return False
@@ -110,8 +115,7 @@ def _has_expired(self) -> bool:
110115
has_expired = property(_has_expired)
111116

112117
def is_valid(self, key: str) -> bool:
113-
_, _, hashed_key = self.pk.partition(".")
114-
return type(self).objects.key_generator.verify(key, hashed_key)
118+
return type(self).objects.key_generator.verify(key, self.hashed_key)
115119

116120
def clean(self):
117121
self._validate_revoked()

tests/legacy/__init__.py

Whitespace-only changes.

tests/legacy/crypto.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from typing import Tuple
2+
from rest_framework_api_key.crypto import concatenate, KeyGenerator
3+
4+
5+
class LegacyKeyGenerator(KeyGenerator):
6+
"""Pre-1.4 key generator."""
7+
8+
def generate(self) -> Tuple[str, str, str]:
9+
prefix = self.get_prefix()
10+
secret_key = self.get_secret_key()
11+
key = concatenate(prefix, secret_key)
12+
hashed_key = concatenate(prefix, self.hash(key))
13+
return key, hashed_key

tests/legacy/permissions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from rest_framework_api_key.permissions import HasAPIKey
2+
from .crypto import LegacyKeyGenerator
3+
4+
5+
class HasAPIKeyWithLegacyKeyGenerator(HasAPIKey):
6+
key_generator = LegacyKeyGenerator()
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Generated by Django 2.2.2 on 2019-06-29 10:38
2+
3+
from django.db import migrations, models
4+
5+
APP_NAME = "heroes"
6+
MODEL_NAME = "heroapikey"
7+
DEPENDENCIES = [(APP_NAME, "0001_initial")]
8+
9+
10+
def populate_prefix_hashed_key(apps, schema_editor):
11+
model = apps.get_model(APP_NAME, MODEL_NAME)
12+
13+
for api_key in model.objects.all():
14+
prefix, _, hashed_key = api_key.id.partition(".")
15+
api_key.prefix = prefix
16+
api_key.hashed_key = hashed_key
17+
api_key.save()
18+
19+
20+
class Migration(migrations.Migration):
21+
22+
dependencies = DEPENDENCIES
23+
24+
operations = [
25+
migrations.AddField(
26+
model_name=MODEL_NAME,
27+
name="hashed_key",
28+
field=models.CharField(max_length=100, null=True),
29+
),
30+
migrations.AddField(
31+
model_name=MODEL_NAME,
32+
name="prefix",
33+
field=models.CharField(max_length=8, unique=True, null=True),
34+
),
35+
migrations.RunPython(
36+
populate_prefix_hashed_key, migrations.RunPython.noop
37+
),
38+
migrations.AlterField(
39+
model_name=MODEL_NAME,
40+
name="hashed_key",
41+
field=models.CharField(max_length=100, editable=False),
42+
),
43+
migrations.AlterField(
44+
model_name=MODEL_NAME,
45+
name="prefix",
46+
field=models.CharField(max_length=8, unique=True, editable=False),
47+
),
48+
]

tests/test_model.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313

1414
def test_key_generation():
1515
api_key, generated_key = APIKey.objects.create_key(name="test")
16-
prefix, _, hashed_key = api_key.id.partition(".")
16+
prefix = api_key.prefix
17+
hashed_key = api_key.hashed_key
1718

1819
assert prefix and hashed_key
1920

tests/test_permissions_legacy.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import pytest
2+
3+
from .legacy.permissions import HasAPIKeyWithLegacyKeyGenerator
4+
5+
pytestmark = pytest.mark.django_db
6+
7+
8+
@pytest.fixture(name="view")
9+
def fixture_view(view_with_permissions):
10+
return view_with_permissions(HasAPIKeyWithLegacyKeyGenerator)
11+
12+
13+
def test_api_key_granted(create_request, view):
14+
request = create_request()
15+
response = view(request)
16+
assert response.status_code == 200

0 commit comments

Comments
 (0)