From b0f42fbe4bfa8aae6fd8b56601a8c890b3077a70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Bastian?= Date: Sun, 24 Sep 2017 18:20:10 +0200 Subject: [PATCH] Fix publish API, add basic client --- api/admin.py | 12 ++++++- api/client/apiclient.py | 61 ++++++++++++++++++++++++++++++++++ api/migrations/0001_initial.py | 26 +++++++++++++++ api/models.py | 33 +++++++++++++----- api/urls.py | 1 + api/views.py | 55 ++++++++++++++++-------------- 6 files changed, 154 insertions(+), 34 deletions(-) create mode 100644 api/client/apiclient.py create mode 100644 api/migrations/0001_initial.py diff --git a/api/admin.py b/api/admin.py index 8c38f3f..9cccfc7 100644 --- a/api/admin.py +++ b/api/admin.py @@ -1,3 +1,13 @@ from django.contrib import admin +from . import models -# Register your models here. + +@admin.register(models.ApiKey) +class ApiKeyAdmin(admin.ModelAdmin): + list_display = ['name', 'last_used', 'displayValue'] + readonly_fields = ['keyId', 'key', 'last_used', 'displayValue'] + + def save_model(self, request, obj, form, change): + if not change: + obj.initialFill() + super(ApiKeyAdmin, self).save_model(request, obj, form, change) diff --git a/api/client/apiclient.py b/api/client/apiclient.py new file mode 100644 index 0000000..018a430 --- /dev/null +++ b/api/client/apiclient.py @@ -0,0 +1,61 @@ +''' API client for bocal-site ''' + +import json +import urllib.request +import hmac +import hashlib +from datetime import datetime + + +def sendReq(url): + def send(payload, host): + try: + req = urllib.request.Request('http://{}/{}'.format(host, url), + json.dumps(payload).encode('ascii')) + req.add_header('Content-Type', 'application/json') + handle = urllib.request.urlopen(req) + code = handle.getcode() + content = handle.read() + handle.close() + return (code, content.decode('utf-8')) + except urllib.error.HTTPError as e: + return (e.code, e.read().decode('utf-8')) + + def authentify(apiKey, payload): + keyId, key = apiKey.split('$') + keyId = int(keyId) + time = datetime.now().timestamp() + mac = hmac.new(key.encode('utf-8'), + msg=str(int(time)).encode('utf-8'), + digestmod=hashlib.sha256) + mac.update(json.dumps(payload).encode('utf-8')) + + auth = { + 'keyId': keyId, + 'timestamp': time, + 'hmac': mac.hexdigest(), + } + + return { + 'auth': auth, + 'req': payload, + } + + def decorator(fct): + ''' Decorator. Adds authentication layer. ''' + def wrap(host, apiKey, *args, **kwargs): + innerReq = fct(*args, **kwargs) + payload = authentify(apiKey, innerReq) + return send(payload, host) + return wrap + + return decorator + + +@sendReq(url='api/publish') +def publish(bocId, url, date): + return { + 'id': bocId, + 'url': url, + 'date': date.strftime('%Y-%m-%d'), + } diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py new file mode 100644 index 0000000..12e0ddf --- /dev/null +++ b/api/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2017-09-24 14:53 +from __future__ import unicode_literals + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='ApiKey', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.CharField(max_length=128, verbose_name='API key')), + ('name', models.CharField(help_text='Where is this key used from?', max_length=256, verbose_name='Key name')), + ('last_used', models.DateTimeField(default=datetime.datetime(1970, 1, 1, 1, 0), verbose_name='Last used')), + ], + ), + ] diff --git a/api/models.py b/api/models.py index 05db23c..2ea06c5 100644 --- a/api/models.py +++ b/api/models.py @@ -2,7 +2,8 @@ from django.db import models from datetime import datetime, timedelta import hmac import hashlib - +import random +import string class ApiKey(models.Model): ''' An API key, to login using the API @@ -11,10 +12,11 @@ class ApiKey(models.Model): containing any dollar ($) sign. It is saved on the client's machine as a string "keyId$key". An API token (to authentify a request) is a triplet (ts, kid, hmac) of - a timestamp `ts`, the key id `kid` and hmac = `HMAC(key, ts, sha256)`. + a timestamp `ts`, the key id `kid` and + hmac = `HMAC(key, ts + data, sha256)` + where `data` is the normalized (`json.dumps(json.loads(...))`) value of + the data part of the request. ''' - keyId = models.IntegerField("API key id", - primary_key=True) key = models.CharField("API key", max_length=128) name = models.CharField("Key name", @@ -26,16 +28,31 @@ class ApiKey(models.Model): def everUsed(self): return self.last_used > datetime.fromtimestamp(0) - def __str__(self): + def initialFill(self): + if not self.key: + KEY_SIZE = 64 + KEY_CHARS = string.ascii_letters + string.digits + self.key = ''.join(random.choices(KEY_CHARS, k=KEY_SIZE)) + + @property + def keyId(self): + return self.id + + @property + def displayValue(self): return "{}${}".format(self.keyId, self.key) - def isCorrect(self, timestamp, inpMac): + def __str__(self): + return self.displayValue + + def isCorrect(self, timestamp, inpMac, data): claimedDate = datetime.fromtimestamp(timestamp) if datetime.now() - timedelta(minutes=5) > claimedDate: return False - mac = hmac.new(self.key, - msg=int(claimedDate.timestamp()), + mac = hmac.new(self.key.encode('utf-8'), + msg=str(int(claimedDate.timestamp())).encode('utf-8'), digestmod=hashlib.sha256) + mac.update(data.encode('utf-8')) return hmac.compare_digest(mac.hexdigest(), inpMac) diff --git a/api/urls.py b/api/urls.py index e58c6bc..fcdf67a 100644 --- a/api/urls.py +++ b/api/urls.py @@ -4,4 +4,5 @@ from . import views app_name = 'manisite' urlpatterns = [ + url(r'^publish$', views.publishApiView), ] diff --git a/api/views.py b/api/views.py index 5bb5cb4..c2446dd 100644 --- a/api/views.py +++ b/api/views.py @@ -1,5 +1,6 @@ from django.http import response from django.core.exceptions import ValidationError +from django.views.decorators.csrf import csrf_exempt import json import datetime @@ -7,7 +8,7 @@ from . import models import mainsite.models as mainModels -def authentify(data): +def authentify(data, payload): ''' returns whether the request's authentification is correct ''' required = ['keyId', 'timestamp', 'hmac'] for field in required: @@ -15,38 +16,42 @@ def authentify(data): return response.HttpResponseForbidden( 'Missing required field "{}"'.format(field)) try: - key = models.ApiKey.objects.get(keyId=data['keyId']) + key = models.ApiKey.objects.get(id=data['keyId']) except models.ApiKey.DoesNotExist: - response.HttpResponseForbidden('Bad authentication') + return response.HttpResponseForbidden('Bad authentication') - if not key.isCorrect(data['timestamp'], data['hmac']): - response.HttpResponseForbidden('Bad authentication') + normPayload = json.dumps(payload) + if not key.isCorrect(data['timestamp'], data['hmac'], normPayload): + return response.HttpResponseForbidden('Bad authentication') -def apiView(fct, required=[]): - def wrap(request, *args, **kwargs): - try: - data = json.loads(request.body) - except json.decoder.JSONDecoreError: - return response.HttpResponseBadRequest("Bad json") +def apiView(required=[]): + def decorator(fct): + @csrf_exempt + def wrap(request, *args, **kwargs): + try: + data = json.loads(request.body) + except json.decoder.JSONDecoreError: + return response.HttpResponseBadRequest("Bad json") - try: - authData = data['auth'] - reqData = data['req'] - except KeyError: - return response.HttpResponseBadRequest("Bad request format") + try: + authData = data['auth'] + reqData = data['req'] + except KeyError: + return response.HttpResponseBadRequest("Bad request format") - for field in required: - if field not in reqData: - return response.HttpResponseBadRequest( - "Missing field {}".format(field)) + for field in required: + if field not in reqData: + return response.HttpResponseBadRequest( + "Missing field {}".format(field)) - authVal = authentify(authData) - if authVal is not None: - return authVal + authVal = authentify(authData, reqData) + if authVal is not None: + return authVal - return fct(request, reqData, *args, **kwargs) - return wrap + return fct(request, reqData, *args, **kwargs) + return wrap + return decorator @apiView(required=["id", "url", "date"])