New JWT token based RFID view

This commit is contained in:
Sam Collins 2018-10-17 19:16:00 +01:00 committed by Oliver Marks
parent fb661f32d1
commit cb29f28a78
12 changed files with 122 additions and 54 deletions

View File

@ -620,3 +620,5 @@ ST_UPLOAD_FILE_ENABLED = True
ST_TESTS_RATELIMIT_NEVER_EXPIRE = False ST_TESTS_RATELIMIT_NEVER_EXPIRE = False
ST_BASE_DIR = os.path.dirname(__file__) ST_BASE_DIR = os.path.dirname(__file__)
# ST is Spirit forum software config # ST is Spirit forum software config
RFID_SECRET = env("RFID_SECRET")

View File

@ -38,7 +38,7 @@ router.register(r'categories', CategoryViewSet, base_name='categories')
router.register(r'feeds', FeedViewSet, 'feeds') router.register(r'feeds', FeedViewSet, 'feeds')
router.register(r'articles', ArticleViewSet, base_name='articles') router.register(r'articles', ArticleViewSet, base_name='articles')
router.register(r'rfid', DeviceViewSet, base_name='rfid_device') router.register(r'rfid', DeviceViewSet, base_name='rfid_device')
router.register(r'rfidAuth', AuthUserWithDeviceViewSet, base_name='device_auth') router.register(r'rfid_auth', AuthUserWithDeviceViewSet, base_name='device_auth')
sitemaps = { sitemaps = {

View File

@ -14,7 +14,7 @@ from mhackspace.base.models import BannerImage
from mhackspace.feeds.models import Article, Feed from mhackspace.feeds.models import Article, Feed
from mhackspace.users.models import User, Rfid, Membership, MEMBERSHIP_STATUS_CHOICES from mhackspace.users.models import User, Rfid, Membership, MEMBERSHIP_STATUS_CHOICES
from mhackspace.blog.models import Category, Post from mhackspace.blog.models import Category, Post
from mhackspace.rfid.models import Device, DeviceAuth from mhackspace.rfid.models import Device
class ImageFixture(AutoFixture): class ImageFixture(AutoFixture):
@ -80,7 +80,6 @@ class Command(BaseCommand):
Rfid.objects.all().delete() Rfid.objects.all().delete()
Device.objects.all().delete() Device.objects.all().delete()
DeviceAuth.objects.all().delete()
rfid = AutoFixture( rfid = AutoFixture(
Rfid, Rfid,
@ -94,9 +93,6 @@ class Command(BaseCommand):
}) })
device.create(5) device.create(5)
deviceauth = AutoFixture(DeviceAuth)
deviceauth.create(5)
feed = AutoFixture(Feed) feed = AutoFixture(Feed)
feed.create(10) feed.create(10)

View File

@ -1,9 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.contrib import admin from django.contrib import admin
from django.contrib.admin import ModelAdmin from django.contrib.admin import ModelAdmin
from django.forms.models import ModelChoiceField
from mhackspace.rfid.models import Device, DeviceAuth from mhackspace.rfid.models import Device, AccessLog
@admin.register(Device) @admin.register(Device)
@ -11,12 +10,7 @@ class DeviceAdmin(ModelAdmin):
list_display = ('name', 'identifier') list_display = ('name', 'identifier')
@admin.register(DeviceAuth) @admin.register(AccessLog)
class DeviceAuthAdmin(ModelAdmin): class AccessLogAdmin(ModelAdmin):
list_display = ('device', 'rfid_code', 'rfid_user', 'device_id') list_display = ('rfid', 'device', 'success')
list_filter = ('success',)
def rfid_code(self, x):
return x.rfid.code
def rfid_user(self, x):
return x.rfid.user

View File

@ -0,0 +1,54 @@
# Generated by Django 2.1.1 on 2018-10-17 17:41
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('users', '0012_rfid_users'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('rfid', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='AccessLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('success', models.BooleanField()),
],
),
migrations.RemoveField(
model_name='deviceauth',
name='device',
),
migrations.RemoveField(
model_name='deviceauth',
name='rfid',
),
migrations.RemoveField(
model_name='device',
name='members',
),
migrations.AddField(
model_name='device',
name='users',
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
),
migrations.DeleteModel(
name='DeviceAuth',
),
migrations.AddField(
model_name='accesslog',
name='device',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rfid.Device'),
),
migrations.AddField(
model_name='accesslog',
name='rfid',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.Rfid'),
),
]

View File

@ -8,31 +8,23 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from mhackspace.users.models import Rfid from mhackspace.users.models import Rfid
# just brainstorming so we can start playing with this,
# be nice to make this a 3rd party django installable app ?
# description of a device like door, print, laser cutter
class Device(models.Model): class Device(models.Model):
# user = models.ManyToMany(settings.AUTH_USER_MODEL)
name = models.CharField(_('Device name'), max_length=255)
identifier = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) identifier = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
users = models.ManyToManyField(settings.AUTH_USER_MODEL)
name = models.CharField(_('Device name'), max_length=255)
description = models.CharField(_('Short description of what the device does'), blank=True, max_length=255) description = models.CharField(_('Short description of what the device does'), blank=True, max_length=255)
added_date = models.DateTimeField(default=timezone.now, editable=False) added_date = models.DateTimeField(default=timezone.now, editable=False)
members = models.ManyToManyField(Rfid, through='DeviceAuth')
def __str__(self): def __str__(self):
return self.name return self.name
# http://stackoverflow.com/questions/4443190/djangos-manytomany-relationship-with-additional-fields class AccessLog(models.Model):
# many to many, lookup user from rfid model then get there user_id and the device to check if auth allowed rfid = models.ForeignKey(Rfid, on_delete=models.CASCADE)
class DeviceAuth(models.Model): device = models.ForeignKey(Device, on_delete=models.CASCADE)
rfid = models.ForeignKey( success = models.BooleanField()
Rfid,
on_delete=models.CASCADE
)
device = models.ForeignKey( def __str__(self):
Device, return f"{self.rfid.user} {self.device}"
on_delete=models.CASCADE
)

View File

@ -19,6 +19,5 @@ class DeviceSerializer(serializers.ModelSerializer):
class AuthSerializer(serializers.Serializer): class AuthSerializer(serializers.Serializer):
name = serializers.CharField(max_length=255) name = serializers.CharField(max_length=255)
rfid = serializers.CharField(max_length=255) rfid = serializers.CharField(max_length=255)
# device = serializers.UUIDField(format='hex_verbose')
device = serializers.CharField(max_length=255) device = serializers.CharField(max_length=255)

View File

@ -1,13 +1,11 @@
import sys import sys
import requests
from io import StringIO from io import StringIO
from django.core.management import call_command from django.core.management import call_command
from test_plus.test import TestCase from test_plus.test import TestCase
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from rest_framework.test import RequestsClient from rest_framework.test import RequestsClient
from mhackspace.rfid.models import Device, DeviceAuth from mhackspace.rfid.models import Device
from mhackspace.users.models import User, Rfid from mhackspace.users.models import User, Rfid
@ -24,7 +22,6 @@ class MigrationTestCase(TestCase):
class ApiTests(TestCase): class ApiTests(TestCase):
maxDiff = None
def setUp(self): def setUp(self):
self.user = User(name='User01') self.user = User(name='User01')
self.user.save() self.user.save()
@ -34,8 +31,6 @@ class ApiTests(TestCase):
self.device.save() self.device.save()
self.rfid = Rfid(code='1', user=self.user) self.rfid = Rfid(code='1', user=self.user)
self.rfid.save() self.rfid.save()
self.auth = DeviceAuth(rfid=self.rfid, device=self.device)
self.auth.save()
def testAuth(self): def testAuth(self):
factory = APIRequestFactory() factory = APIRequestFactory()

View File

@ -1,15 +1,19 @@
import logging import logging
import jwt
from jwt import ExpiredSignatureError
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework import status from rest_framework import status
from django.views.generic import ListView, DeleteView, CreateView from django.views.generic import ListView, DeleteView, CreateView
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.conf import settings
from mhackspace.base.tasks import matrix_message
from mhackspace.users.models import Rfid from mhackspace.users.models import Rfid
from mhackspace.rfid.models import Device, DeviceAuth from mhackspace.rfid.models import Device, AccessLog
from mhackspace.rfid.serializers import DeviceSerializer, AuthSerializer from mhackspace.rfid.serializers import DeviceSerializer, AuthSerializer
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -31,18 +35,31 @@ class AuthUserWithDeviceViewSet(viewsets.ViewSet):
def post(self, request, format=None): def post(self, request, format=None):
try: try:
rfid = Rfid.objects.get(code=request.data.get('rfid')) data = jwt.decode(request.data["data"], settings.RFID_SECRET, algorithms=['HS256'])
device = Device.objects.get(identifier=request.data.get('device')) except ExpiredSignatureError:
deviceAuth = DeviceAuth.objects.get(device=device.identifier, rfid=rfid.id) data = jwt.decode(request.data["data"], settings.RFID_SECRET, algorithms=['HS256'], verify=False)
logger.warn(f"Signature expired for {data.get('rfid_code')} on device {data.get('device_id')}")
return Response(jwt.encode({"authenticated": False}, settings.RFID_SECRET), status=status.HTTP_403_FORBIDDEN)
if data.get("rfid_code") is None or data.get("rfid_code") is None:
return Response(status=status.HTTP_400_BAD_REQUEST)
# print(data)
try:
rfid = Rfid.objects.get(code=data["rfid_code"])
device = Device.objects.get(identifier=data["device_id"])
except ObjectDoesNotExist: except ObjectDoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND) return Response(status=status.HTTP_404_NOT_FOUND)
except ValidationError as e:
# except: try:
# logger.exception("An error occurred") member = device.users.get(pk=rfid.user_id)
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) matrix_message.delay(
serializer = AuthSerializer( f"{member.username} has just entered {device.name}"
instance={'name': device.name, 'rfid': rfid.code, 'device': device.identifier}) )
return Response(serializer.data, status=200) AccessLog.objects.create(rfid=rfid, device=device, success=True)
return Response(jwt.encode({"authenticated": True, "username": member.username}, settings.RFID_SECRET))
except ObjectDoesNotExist:
AccessLog.objects.create(rfid=rfid, device=device, success=False)
return Response(jwt.encode({"authenticated": False}, settings.RFID_SECRET), status=status.HTTP_403_FORBIDDEN)
class RfidCardsListView(LoginRequiredMixin, ListView): class RfidCardsListView(LoginRequiredMixin, ListView):

View File

@ -0,0 +1,18 @@
# Generated by Django 2.1.1 on 2018-10-17 17:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0011_auto_20181011_0752'),
]
operations = [
migrations.AlterField(
model_name='membership',
name='payment_date',
field=models.DateTimeField(default=None),
),
]

View File

@ -125,7 +125,7 @@ class Membership(models.Model):
class Rfid(models.Model): class Rfid(models.Model):
code = models.CharField(max_length=7) code = models.CharField(max_length=7, unique=True)
description = models.CharField( description = models.CharField(
_("Short rfid description"), blank=True, max_length=255 _("Short rfid description"), blank=True, max_length=255
) )

View File

@ -100,3 +100,4 @@ ldap3==2.5.1
bcrypt==3.1.4 bcrypt==3.1.4
python-twitter==3.4.2 python-twitter==3.4.2
feedparser==5.2.1 feedparser==5.2.1
PyJWT==1.6.4