diff --git a/api/models.py b/api/models.py index 71a8362..05db23c 100644 --- a/api/models.py +++ b/api/models.py @@ -1,3 +1,41 @@ from django.db import models +from datetime import datetime, timedelta +import hmac +import hashlib -# Create your models here. + +class ApiKey(models.Model): + ''' An API key, to login using the API + + An API key consists in a somewhat long chunk of ascii text, *not* + 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)`. + ''' + keyId = models.IntegerField("API key id", + primary_key=True) + key = models.CharField("API key", + max_length=128) + name = models.CharField("Key name", + max_length=256, + help_text="Where is this key used from?") + last_used = models.DateTimeField("Last used", + default=datetime.fromtimestamp(0)) + + def everUsed(self): + return self.last_used > datetime.fromtimestamp(0) + + def __str__(self): + return "{}${}".format(self.keyId, self.key) + + def isCorrect(self, timestamp, inpMac): + claimedDate = datetime.fromtimestamp(timestamp) + if datetime.now() - timedelta(minutes=5) > claimedDate: + return False + + mac = hmac.new(self.key, + msg=int(claimedDate.timestamp()), + digestmod=hashlib.sha256) + + return hmac.compare_digest(mac.hexdigest(), inpMac) diff --git a/api/views.py b/api/views.py index 91ea44a..5bb5cb4 100644 --- a/api/views.py +++ b/api/views.py @@ -1,3 +1,76 @@ -from django.shortcuts import render +from django.http import response +from django.core.exceptions import ValidationError +import json +import datetime -# Create your views here. +from . import models +import mainsite.models as mainModels + + +def authentify(data): + ''' returns whether the request's authentification is correct ''' + required = ['keyId', 'timestamp', 'hmac'] + for field in required: + if field not in data: + return response.HttpResponseForbidden( + 'Missing required field "{}"'.format(field)) + try: + key = models.ApiKey.objects.get(keyId=data['keyId']) + except models.ApiKey.DoesNotExist: + response.HttpResponseForbidden('Bad authentication') + + if not key.isCorrect(data['timestamp'], data['hmac']): + 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") + + 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)) + + authVal = authentify(authData) + if authVal is not None: + return authVal + + return fct(request, reqData, *args, **kwargs) + return wrap + + +@apiView(required=["id", "url", "date"]) +def publishApiView(request, data): + ''' Publish a BOcal, and create the corresponding year if needed ''' + if mainModels.Publication.objects.filter(num=data['id']).count() > 0: + return response.HttpResponseBadRequest( + "Un BOcal du même numéro est déjà présent ! Ajoutez celui-ci à la " + "main si vous voulez vraiment faire ça.") + + try: + year, month, day = [int(x) for x in data['date'].split('-')] + date = datetime.date(year, month, day) + except: + return response.HttpResponseBadRequest("Bad date") + + pub = mainModels.Publication(num=data['id'], + url=data['url'], + date=date) + try: + pub.full_clean() + except ValidationError as e: + return response.HttpResponseBadRequest( + "Invalid data: {}".format(e)) + pub.save() + + return response.HttpResponse("OK") diff --git a/bocal/settings_base.py b/bocal/settings_base.py index c2cd606..5a3657e 100644 --- a/bocal/settings_base.py +++ b/bocal/settings_base.py @@ -30,6 +30,7 @@ INSTALLED_APPS = [ 'solo', 'markdownx', 'mainsite', + 'api', ] MIDDLEWARE = [