Merge branch 'master' into Evarin/apparence

This commit is contained in:
Evarin 2017-10-19 10:47:31 +02:00
commit ce694f617b
10 changed files with 146 additions and 15 deletions

View file

@ -1,16 +1,21 @@
#!/usr/bin/env python3
''' API client for bocal-site '''
import json
import urllib.request
import hmac
import hashlib
from datetime import datetime
from datetime import datetime, date
import argparse
import os.path
import sys
def sendReq(url):
def send(payload, host):
try:
req = urllib.request.Request('http://{}/{}'.format(host, url),
req = urllib.request.Request('https://{}/{}'.format(host, url),
json.dumps(payload).encode('ascii'))
req.add_header('Content-Type', 'application/json')
handle = urllib.request.urlopen(req)
@ -28,7 +33,8 @@ def sendReq(url):
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'))
payload_enc = json.dumps(payload)
mac.update(payload_enc.encode('utf-8'))
auth = {
'keyId': keyId,
@ -38,7 +44,7 @@ def sendReq(url):
return {
'auth': auth,
'req': payload,
'req': payload_enc,
}
def decorator(fct):
@ -59,3 +65,85 @@ def publish(bocId, url, date):
'url': url,
'date': date.strftime('%Y-%m-%d'),
}
###############################################################################
TOKEN_DFT_FILE = os.path.expanduser("~/.bocal_api_token")
DFT_HOST = 'bocal.cof.ens.fr'
def read_token(path):
token = ''
try:
with open(path, 'r') as handle:
token = handle.readline().strip()
except FileNotFoundError:
print("[Erreur] Fichier d'identifiants absent (`{}`).".format(path),
file=sys.stderr)
sys.exit(1)
return token
def cmd(func):
def wrap(parse_args, *args, **kwargs):
token = read_token(parse_args.creds)
return func(token, parse_args, *args, **kwargs)
return wrap
@cmd
def cmd_publish(token, args):
if not args.date:
publish_date = date.today()
else:
year, month, day = [int(x) for x in args.date.strip().split('-')]
publish_date = date(year=year, month=month, day=day)
(ret_code, ret_str) = publish(args.host,
token,
args.numero,
args.url,
publish_date)
if ret_code == 200:
print("Succès :)")
else:
print("[Erreur :c] {} : {}".format(ret_code, ret_str))
sys.exit(1)
def setup_argparse():
parser = argparse.ArgumentParser()
parser.add_argument('--host',
help=("Adresse du site à contacter (par défaut, "
"`{}`).".format(DFT_HOST)))
parser.add_argument('--creds',
help=("Fichier contenant le token API à utiliser "
"(par défaut, `{}`)".format(TOKEN_DFT_FILE)))
parser.set_defaults(creds=TOKEN_DFT_FILE, host=DFT_HOST)
subparsers = parser.add_subparsers()
parser_publish = subparsers.add_parser('publier',
help='Publier un numéro du BOcal')
parser_publish.add_argument('numero',
help='Numéro du BOcal')
parser_publish.add_argument('url',
help='Adresse (locale) du PDF du BOcal')
parser_publish.add_argument('-d', '--date',
help="Date de publication indiquée")
parser_publish.set_defaults(func=cmd_publish)
out_args = parser.parse_args()
if 'func' not in out_args: # No subcommand provided
print("You must provide a command.", file=sys.stderr)
print(parser.parse_args(['-h']), file=sys.stderr)
sys.exit(1)
return out_args
def main():
args = setup_argparse()
args.func(args)
if __name__ == '__main__':
main()

View file

@ -5,6 +5,7 @@ import hashlib
import random
import string
class ApiKey(models.Model):
''' An API key, to login using the API
@ -32,7 +33,8 @@ class ApiKey(models.Model):
if not self.key:
KEY_SIZE = 64
KEY_CHARS = string.ascii_letters + string.digits
self.key = ''.join(random.choices(KEY_CHARS, k=KEY_SIZE))
self.key = ''.join(
[random.choice(KEY_CHARS) for _ in range(KEY_SIZE)])
@property
def keyId(self):

View file

@ -20,8 +20,7 @@ def authentify(data, payload):
except models.ApiKey.DoesNotExist:
return response.HttpResponseForbidden('Bad authentication')
normPayload = json.dumps(payload)
if not key.isCorrect(data['timestamp'], data['hmac'], normPayload):
if not key.isCorrect(data['timestamp'], data['hmac'], payload):
return response.HttpResponseForbidden('Bad authentication')
@ -30,22 +29,31 @@ def apiView(required=[]):
@csrf_exempt
def wrap(request, *args, **kwargs):
try:
data = json.loads(request.body)
except json.decoder.JSONDecoreError:
data = json.loads(request.body.decode('utf-8'))
except TypeError:
return response.HttpResponseBadRequest("Bad packet format")
except json.decoder.JSONDecodeError:
return response.HttpResponseBadRequest("Bad json")
try:
authData = data['auth']
reqData = data['req']
reqDataOrig = data['req']
except KeyError:
return response.HttpResponseBadRequest("Bad request format")
try:
reqData = json.loads(reqDataOrig)
except TypeError:
return response.HttpResponseBadRequest("Bad packet format")
except json.decoder.JSONDecodeError:
return response.HttpResponseBadRequest("Bad inner json")
for field in required:
if field not in reqData:
return response.HttpResponseBadRequest(
"Missing field {}".format(field))
authVal = authentify(authData, reqData)
authVal = authentify(authData, reqDataOrig)
if authVal is not None:
return authVal

1
bocal/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
settings.py

View file

@ -1 +0,0 @@
settings_dev.py

View file

@ -1,6 +1,5 @@
from django_cas_ng.backends import CASBackend
from .models import CasUser
from . import rhosts
class BOcalCASBackend(CASBackend):
@ -11,3 +10,4 @@ class BOcalCASBackend(CASBackend):
def configure_user(self, user):
casUser = CasUser(user=user)
casUser.save()
return user

View file

@ -71,9 +71,11 @@ def grantBOcalPrivileges(user):
def requireCasUser(fct):
def hasCas(user):
return CasUser.objects.filter(user=user).count() > 0
def wrap(user, *args, **kwargs):
qs = CasUser.objects.filter(user=user)
if not qs.count() > 0:
if not hasCas(user):
return
return fct(user, *args, **kwargs)
return wrap

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2017-10-17 14:08
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainsite', '0007_siteconfiguration_specialpublisdescr'),
]
operations = [
migrations.AddField(
model_name='publication',
name='unknown_date',
field=models.BooleanField(default=False, help_text="La date de publication du BOcal est inconnue parce qu'il est trop vieux. La date indiquée ne servira qu'à le ranger dans une année et à ordonner les BOcals.", verbose_name='Date inconnue'),
),
]

View file

@ -32,6 +32,13 @@ class Publication(models.Model):
# ^ This is not a URLField because we need internal URLS, eg `/static/blah`
date = DateField('Publication')
unknown_date = BooleanField('Date inconnue',
help_text=("La date de publication du BOcal "
"est inconnue parce qu'il est "
"trop vieux. La date indiquée ne "
"servira qu'à le ranger dans une "
"année et à ordonner les BOcals."),
default=False)
is_special = BooleanField('Numéro spécial',
help_text='Numéro du BOcal non-numéroté',
default=False)

View file

@ -16,7 +16,11 @@ Millésime {{ year_range }}
<table class="publication-list">
{% for bocal in publications %}
<tr class="publication-entry">
{% if bocal.unknown_date %}
<td class="publication-date">Date inconnue</td>
{% else %}
<td class="publication-date">{{ bocal.date | date:"d/m/Y" }}</td>
{% endif %}
<td class="publication-body">
<a href="{{ bocal.url }}" title="Lire le BOcal !">{{ bocal }}</a>