Fix publish API, add basic client

This commit is contained in:
Théophile Bastian 2017-09-24 18:20:10 +02:00
parent 25102a8233
commit b0f42fbe4b
6 changed files with 154 additions and 34 deletions

View file

@ -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)

61
api/client/apiclient.py Normal file
View file

@ -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'),
}

View file

@ -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')),
],
),
]

View file

@ -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)

View file

@ -4,4 +4,5 @@ from . import views
app_name = 'manisite'
urlpatterns = [
url(r'^publish$', views.publishApiView),
]

View file

@ -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,15 +16,18 @@ 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 apiView(required=[]):
def decorator(fct):
@csrf_exempt
def wrap(request, *args, **kwargs):
try:
data = json.loads(request.body)
@ -41,12 +45,13 @@ def apiView(fct, required=[]):
return response.HttpResponseBadRequest(
"Missing field {}".format(field))
authVal = authentify(authData)
authVal = authentify(authData, reqData)
if authVal is not None:
return authVal
return fct(request, reqData, *args, **kwargs)
return wrap
return decorator
@apiView(required=["id", "url", "date"])