Skip to content

Commit

Permalink
Add custom media type, add /auth/register/ endpoint, add more test he…
Browse files Browse the repository at this point in the history
…lpers.

* Implement registration on /auth/register/
* Add custom application/vnd.keybar+json media-type in preparation
  for api-versioning
* Add more test-helpers for testing the API
* Lots of cleanups along the way
  • Loading branch information
EnTeQuAk committed Mar 20, 2016
1 parent ace50fd commit a4a958c
Show file tree
Hide file tree
Showing 12 changed files with 188 additions and 13 deletions.
1 change: 0 additions & 1 deletion src/keybar/api/parsers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from django.conf import settings
from rest_framework.parsers import JSONParser


Expand Down
5 changes: 5 additions & 0 deletions src/keybar/api/renderers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from rest_framework import renderers


class JSONRenderer(renderers.JSONRenderer):
media_type = 'application/vnd.keybar+json'
17 changes: 13 additions & 4 deletions src/keybar/api/serializers/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,20 @@


class RegisterSerializer(serializers.ModelSerializer):
device_name = serializers.CharField(max_length=256, required=False)
public_key = serializers.CharField(required=True)
device_name = serializers.CharField(
max_length=256, required=False, write_only=True)
public_key = serializers.CharField(
required=True, write_only=True)

# Device ID will be inserted into the response.
device_id = serializers.UUIDField(source='_saved_device.id', read_only=True)

class Meta:
model = User
fields = ('email', 'password', 'device_name', 'public_key')
fields = ('email', 'password', 'device_name', 'device_id', 'public_key')
extra_kwargs = {
'password': {'write_only': True}
}

def validate_password(self, password):
password_validation.validate_password(password)
Expand All @@ -35,9 +43,10 @@ def create(self, validated_data):
user.set_password(password)
user.save()

Device.objects.create(
device = Device.objects.create(
public_key=public_key,
name=device_name,
user=user,
authorized=True)
user._saved_device = device
return user
9 changes: 6 additions & 3 deletions src/keybar/api/urls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from django.conf.urls import url

from .endpoints.dummy import AuthenticatedDummyEndpoint

from .endpoints.dummy import AuthenticatedDummyView
from .endpoints.registration import RegisterView

urlpatterns = [
url(r'dummy/$', AuthenticatedDummyEndpoint.as_view())
url(r'auth/register/$', RegisterView.as_view(), name='register'),

# Dummy view, used for testing.
url(r'dummy/$', AuthenticatedDummyView.as_view()),
]
2 changes: 1 addition & 1 deletion src/keybar/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def __str__(self):

class Client(requests.Session):
"""Proof of concept client implementation."""
content_type = 'application/json'
content_type = 'application/vnd.keybar+json'

host = 'keybar.me'
port = '443'
Expand Down
4 changes: 2 additions & 2 deletions src/keybar/conf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@
'keybar.api.parsers.JSONParser',
),
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
'keybar.api.renderers.JSONRenderer',
),
}

Expand Down Expand Up @@ -239,6 +239,6 @@ def _default_ca_certs():
KEYBAR_HOST = None

# NOTE: CHANGING THOSE VALUES REQUIRES RE-ENCRYPTION OF EVERYTHING
# AND HAS SOME DEEP IMPACT. !! JUST DONT !!
# AND HAS SOME HUGE IMPACT. !! JUST DONT !!
# In 2013 100,000 was the recommended value, so we settle with one million for now.
KEYBAR_KDF_ITERATIONS = 1000000
Empty file.
100 changes: 100 additions & 0 deletions src/keybar/tests/api/endpoints/test_registration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import mock
import pytest
from django.core.urlresolvers import reverse

from keybar.models.user import User
from keybar.models.device import Device
from keybar.utils.test import APIClient
from keybar.utils.crypto import serialize_public_key
from keybar.tests.factories.device import PUBLIC_KEY


@pytest.mark.django_db
class TestRegistration:

def test_simple_register(self):
url = reverse('api:register')

response = APIClient().post(url, data={
'email': 'test@example.com',
'password': 'test123456',
'public_key': serialize_public_key(PUBLIC_KEY)
})

assert response.status_code == 201
assert response.json() == {
'email': 'test@example.com',
'device_id': mock.ANY
}
assert User.objects.filter(email='test@example.com').exists()

def test_register_invalid_password(self):
url = reverse('api:register')

response = APIClient().post(url, data={
'email': 'test@example.com',
'password': '1234',
'public_key': serialize_public_key(PUBLIC_KEY)
})

assert response.status_code == 400
assert response.json() == {
'password': [
'This password is too short. It must contain at least 10 characters.',
'This password is too common.',
'This password is entirely numeric.'
]
}

# Make sure we never created a user or device
assert User.objects.all().count() == 0
assert Device.objects.all().count() == 0

def test_register_invalid_email(self):
url = reverse('api:register')

response = APIClient().post(url, data={
'email': 'test',
'password': 'test123456',
'public_key': serialize_public_key(PUBLIC_KEY)
})

assert response.status_code == 400
assert response.json() == {
'email': ['Enter a valid email address.']
}

assert User.objects.all().count() == 0
assert Device.objects.all().count() == 0

def test_register_invalid_public_key(self):
url = reverse('api:register')

response = APIClient().post(url, data={
'email': 'test@test.test',
'password': 'test123456',
'public_key': 'invalid'
})

assert response.status_code == 400
assert response.json() == {
'public_key': ['Invalid public key']
}

assert User.objects.all().count() == 0
assert Device.objects.all().count() == 0

def test_register_invalid_email_unique(self):
url = reverse('api:register')

User.objects.create(email='test@test.test')
response = APIClient().post(url, data={
'email': 'test@test.test',
'password': 'test123456',
'public_key': serialize_public_key(PUBLIC_KEY)
})

assert response.status_code == 400
assert response.json() == {
'email': ['User with this Email already exists.']
}
9 changes: 9 additions & 0 deletions src/keybar/tests/factories/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,12 @@ def _prepare(cls, create, **kwargs):
if 'password' not in kwargs:
kwargs['password'] = make_password(raw_password, hasher='pbkdf2_sha256')
return super(UserFactory, cls)._prepare(create, **kwargs)


class APIUserFactory(UserFactory):
"""
User factory that creates a user with predefined credentials for usage
with :class:`~keybar.utils.test.APIClient`
"""
email = 'test@test.test'
raw_password = 'test123456'
2 changes: 1 addition & 1 deletion src/keybar/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def test_simple_authorized(self):
response = client.get(endpoint)

assert response.status_code == 200
assert response.content == b'"{\\"dummy\\":\\"ok\\"}"'
assert response.content == b'"{\\"dummy\\": \\"ok\\"}"'

def test_simple_wrong_device_secret(self, settings):
user = UserFactory.create(is_superuser=True)
Expand Down
2 changes: 1 addition & 1 deletion src/keybar/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

urlpatterns = [
# Hookup our REST Api
url(r'^api/', include('keybar.api.urls', namespace='keybar-api')),
url(r'^api/', include('keybar.api.urls', namespace='api')),
url(r'^api/docs/', include('rest_framework.urls', namespace='rest_framework')),

# Admin
Expand Down
50 changes: 50 additions & 0 deletions src/keybar/utils/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import json

from rest_framework.test import APIClient as BaseAPIClient

from keybar.models.device import Device
from keybar.utils.jwt import encode_token


class APIClient(BaseAPIClient):
"""
Subclass to handle our custom accept headers required
for proper versioning and data parsing.
Requires a test-user with the following credentials to be setup in
case of using an authenticated endpoint:
* E-mail: test@test.test
* Password: test123456
"""
content_type = 'application/vnd.keybar+json'
default_format = 'json'

def __init__(self, *args, **kwargs):
self.device_id = kwargs.pop('device_id', None)
super().__init__(*args, **kwargs)

def generic(self, method, path, data='', content_type=None, secure=False, **extra):
extra.update({
'HTTP_HOST': 'testserver',
'HTTP_ACCEPT': self.content_type,
})

if extra.get('authorized', False):
device = Device.objects.get(pk=self.device_id)
token = encode_token(device.id, device.public_key)
extra['HTTP_AUTHORIZATION'] = 'JWT {}'.format(token)

return super().generic(method, path, data, self.content_type, secure, **extra)

def _parse_json(self, response, **extra):
content_type = response.get('Content-Type')
types = ('application/json', self.content_type)

if not any(type in content_type for type in types):
raise ValueError(
'Content-Type header is "{0}", not "application/json"'
.format(response.get('Content-Type'))
)

return json.loads(response.content.decode(), **extra)

0 comments on commit a4a958c

Please sign in to comment.