Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

APDS-61 - [BE] add API to list and delete permanent tokens #1

Merged
merged 14 commits into from
Aug 1, 2017
15 changes: 7 additions & 8 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
python: '3.5'

language: python

cache: pip
Expand Down Expand Up @@ -27,12 +25,6 @@ env:
- TOX_ENV=py27-django1.11-drf3.4
- TOX_ENV=py27-django1.11-drf3.5

- TOX_ENV=py33-django1.8-drf3.1
- TOX_ENV=py33-django1.8-drf3.2
- TOX_ENV=py33-django1.8-drf3.3
- TOX_ENV=py33-django1.8-drf3.4
- TOX_ENV=py33-django1.8-drf3.5

- TOX_ENV=py34-django1.8-drf3.1
- TOX_ENV=py34-django1.8-drf3.2
- TOX_ENV=py34-django1.8-drf3.3
Expand Down Expand Up @@ -60,6 +52,13 @@ env:
matrix:
fast_finish: true

addons:
apt:
sources:
- deadsnakes
packages:
- python3.3

install:
- pip install tox

Expand Down
32 changes: 32 additions & 0 deletions rest_framework_jwt/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-07-21 08:06
from __future__ import unicode_literals
import uuid

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='Device',
fields=[
('permanent_token', models.CharField(max_length=255, unique=True, serialize=False)),
('jwt_secret', models.UUIDField(default=uuid.uuid4, editable=False)),
('created', models.DateTimeField(auto_now_add=True)),
('name', models.CharField(max_length=255, verbose_name='Device name')),
('details', models.CharField(blank=True, max_length=255, verbose_name='Device details')),
('last_request_datetime', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
Empty file.
19 changes: 18 additions & 1 deletion rest_framework_jwt/models.py
Original file line number Diff line number Diff line change
@@ -1 +1,18 @@
# Just to keep things like ./manage.py test happy
import uuid

from django.conf import settings
from django.db import models
from django.utils.translation import ugettext_lazy as _


class Device(models.Model):
"""
Device model used for permanent token authentication
"""
permanent_token = models.CharField(max_length=255, unique=True)
jwt_secret = models.UUIDField(default=uuid.uuid4, editable=False)
created = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
name = models.CharField(_('Device name'), max_length=255)
details = models.CharField(_('Device details'), max_length=255, blank=True)
last_request_datetime = models.DateTimeField(auto_now=True)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing empty line at the end of file.
Pylint and pep8 will complain.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitHub does not show the last empty line as far as I remember. The flake8 task in tox did not complain, see: https://travis-ci.org/ArabellaTech/django-rest-framework-jwt/jobs/256993179

7 changes: 7 additions & 0 deletions rest_framework_jwt/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from rest_framework import serializers
from .compat import Serializer

from rest_framework_jwt.models import Device
from rest_framework_jwt.settings import api_settings
from rest_framework_jwt.compat import get_username_field, PasswordField

Expand Down Expand Up @@ -169,3 +170,9 @@ def validate(self, attrs):
'token': jwt_encode_handler(new_payload),
'user': user
}


class DeviceSerializer(serializers.ModelSerializer):
class Meta:
model = Device
fields = ['id', 'created', 'name', 'details', 'last_request_datetime']
15 changes: 13 additions & 2 deletions rest_framework_jwt/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from rest_framework import mixins, status, viewsets
from rest_framework.views import APIView
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from datetime import datetime

from .models import Device
from .settings import api_settings
from .serializers import (
JSONWebTokenSerializer, RefreshJSONWebTokenSerializer,
DeviceSerializer, JSONWebTokenSerializer, RefreshJSONWebTokenSerializer,
VerifyJSONWebTokenSerializer
)

Expand Down Expand Up @@ -99,6 +101,15 @@ class RefreshJSONWebToken(JSONWebTokenAPIView):
serializer_class = RefreshJSONWebTokenSerializer


class DeviceViewSet(mixins.ListModelMixin, mixins.DestroyModelMixin, viewsets.GenericViewSet):
queryset = Device.objects.all()
serializer_class = DeviceSerializer
permission_classes = [IsAuthenticated]

def get_queryset(self):
return self.queryset.filter(user=self.request.user)


obtain_jwt_token = ObtainJSONWebToken.as_view()
refresh_jwt_token = RefreshJSONWebToken.as_view()
verify_jwt_token = VerifyJSONWebToken.as_view()
7 changes: 7 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,18 @@ def pytest_configure():
'django.contrib.messages',
'django.contrib.staticfiles',

'rest_framework',
'rest_framework_jwt',
'tests',
),
PASSWORD_HASHERS=(
'django.contrib.auth.hashers.MD5PasswordHasher',
),
REST_FRAMEWORK={
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_jwt.authentication.JSONWebTokenAuthentication'
]
}
)

try:
Expand Down
44 changes: 44 additions & 0 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from rest_framework_jwt import utils, views
from rest_framework_jwt.compat import get_user_model
from rest_framework_jwt.models import Device
from rest_framework_jwt.settings import api_settings, DEFAULTS

from . import utils as test_utils
Expand Down Expand Up @@ -492,3 +493,46 @@ def test_refresh_jwt_after_refresh_expiration(self):
def tearDown(self):
# Restore original settings
api_settings.JWT_ALLOW_REFRESH = DEFAULTS['JWT_ALLOW_REFRESH']


class DeviceViewTests(TokenTestCase):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add devices for not your user to make sure we are not showing it and can't delete.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in the CR commit.

def setUp(self):
super(DeviceViewTests, self).setUp()
self.device = Device.objects.create(
user=self.user, permanent_token='somestring2', name='Android',
last_request_datetime=datetime.now())
self.user2 = User.objects.create_user(email='[email protected]', username='jsmith', password='password')
self.device2 = Device.objects.create(
user=self.user2, permanent_token='somestring98', name='Android',
last_request_datetime=datetime.now())

def _login(self, client):
client.credentials(HTTP_AUTHORIZATION='JWT {}'.format(self.get_token()))
return client.login(**self.data)

def test_device_delete(self):
client = APIClient(enforce_csrf_checks=True)
# test accessing without being logged in
response = client.delete('/devices/{}/'.format(self.device.id))
self.assertEqual(response.status_code, 401)

self._login(client)
# try removing device linked to other user
response = client.delete('/devices/{}/'.format(self.device2.id))
self.assertEqual(response.status_code, 404)
# test regular case
self.assertEqual(Device.objects.filter(id=self.device.id).count(), 1)
response = client.delete('/devices/{}/'.format(self.device.id))
self.assertEqual(response.status_code, 204)
self.assertEqual(Device.objects.filter(id=self.device.id).count(), 0)

def test_device_list(self):
client = APIClient(enforce_csrf_checks=True)
self._login(client)
response = client.get('/devices/', format='json')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 1)
self.assertEqual(set(response.data[0].keys()), {
'id', 'created', 'name', 'details', 'last_request_datetime'
})
self.assertEqual(response.data[0]['id'], self.device.id)
7 changes: 5 additions & 2 deletions tests/urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.conf.urls import url
from django.http import HttpResponse
from rest_framework import permissions
from rest_framework import permissions, routers
from rest_framework.views import APIView
try:
from rest_framework_oauth.authentication import OAuth2Authentication
Expand All @@ -24,6 +24,9 @@ def post(self, request):
return HttpResponse({'a': 1, 'b': 2, 'c': 3})


router = routers.SimpleRouter()
router.register(r'devices', views.DeviceViewSet)

urlpatterns = [
url(r'^auth-token/$', views.obtain_jwt_token),
url(r'^auth-token-refresh/$', views.refresh_jwt_token),
Expand All @@ -37,4 +40,4 @@ def post(self, request):
url(r'^oauth2-jwt/$', MockView.as_view(
authentication_classes=[
OAuth2Authentication, JSONWebTokenAuthentication])),
]
] + router.urls
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[tox]
envlist =
py27-{flake8,docs},
{py27,py33,py34,py35}-django{1.8,1.9,1.10,1.11}-drf{3.1,3.2,3.3,3.4,3.5}
{py27,py34,py35,py36}-django{1.8,1.9,1.10,1.11}-drf{3.1,3.2,3.3,3.4,3.5}

[testenv]
commands = ./runtests.py --fast {posargs} --verbose
Expand Down