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 ''' ''' API client for bocal-site '''
import json import json
import urllib.request import urllib.request
import hmac import hmac
import hashlib import hashlib
from datetime import datetime from datetime import datetime, date
import argparse
import os.path
import sys
def sendReq(url): def sendReq(url):
def send(payload, host): def send(payload, host):
try: try:
req = urllib.request.Request('http://{}/{}'.format(host, url), req = urllib.request.Request('https://{}/{}'.format(host, url),
json.dumps(payload).encode('ascii')) json.dumps(payload).encode('ascii'))
req.add_header('Content-Type', 'application/json') req.add_header('Content-Type', 'application/json')
handle = urllib.request.urlopen(req) handle = urllib.request.urlopen(req)
@ -28,7 +33,8 @@ def sendReq(url):
mac = hmac.new(key.encode('utf-8'), mac = hmac.new(key.encode('utf-8'),
msg=str(int(time)).encode('utf-8'), msg=str(int(time)).encode('utf-8'),
digestmod=hashlib.sha256) digestmod=hashlib.sha256)
mac.update(json.dumps(payload).encode('utf-8')) payload_enc = json.dumps(payload)
mac.update(payload_enc.encode('utf-8'))
auth = { auth = {
'keyId': keyId, 'keyId': keyId,
@ -38,7 +44,7 @@ def sendReq(url):
return { return {
'auth': auth, 'auth': auth,
'req': payload, 'req': payload_enc,
} }
def decorator(fct): def decorator(fct):
@ -59,3 +65,85 @@ def publish(bocId, url, date):
'url': url, 'url': url,
'date': date.strftime('%Y-%m-%d'), '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 random
import string import string
class ApiKey(models.Model): class ApiKey(models.Model):
''' An API key, to login using the API ''' An API key, to login using the API
@ -32,7 +33,8 @@ class ApiKey(models.Model):
if not self.key: if not self.key:
KEY_SIZE = 64 KEY_SIZE = 64
KEY_CHARS = string.ascii_letters + string.digits 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 @property
def keyId(self): def keyId(self):

View file

@ -20,8 +20,7 @@ def authentify(data, payload):
except models.ApiKey.DoesNotExist: except models.ApiKey.DoesNotExist:
return response.HttpResponseForbidden('Bad authentication') return response.HttpResponseForbidden('Bad authentication')
normPayload = json.dumps(payload) if not key.isCorrect(data['timestamp'], data['hmac'], payload):
if not key.isCorrect(data['timestamp'], data['hmac'], normPayload):
return response.HttpResponseForbidden('Bad authentication') return response.HttpResponseForbidden('Bad authentication')
@ -30,22 +29,31 @@ def apiView(required=[]):
@csrf_exempt @csrf_exempt
def wrap(request, *args, **kwargs): def wrap(request, *args, **kwargs):
try: try:
data = json.loads(request.body) data = json.loads(request.body.decode('utf-8'))
except json.decoder.JSONDecoreError: except TypeError:
return response.HttpResponseBadRequest("Bad packet format")
except json.decoder.JSONDecodeError:
return response.HttpResponseBadRequest("Bad json") return response.HttpResponseBadRequest("Bad json")
try: try:
authData = data['auth'] authData = data['auth']
reqData = data['req'] reqDataOrig = data['req']
except KeyError: except KeyError:
return response.HttpResponseBadRequest("Bad request format") 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: for field in required:
if field not in reqData: if field not in reqData:
return response.HttpResponseBadRequest( return response.HttpResponseBadRequest(
"Missing field {}".format(field)) "Missing field {}".format(field))
authVal = authentify(authData, reqData) authVal = authentify(authData, reqDataOrig)
if authVal is not None: if authVal is not None:
return authVal 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 django_cas_ng.backends import CASBackend
from .models import CasUser from .models import CasUser
from . import rhosts
class BOcalCASBackend(CASBackend): class BOcalCASBackend(CASBackend):
@ -11,3 +10,4 @@ class BOcalCASBackend(CASBackend):
def configure_user(self, user): def configure_user(self, user):
casUser = CasUser(user=user) casUser = CasUser(user=user)
casUser.save() casUser.save()
return user

View file

@ -71,9 +71,11 @@ def grantBOcalPrivileges(user):
def requireCasUser(fct): def requireCasUser(fct):
def hasCas(user):
return CasUser.objects.filter(user=user).count() > 0
def wrap(user, *args, **kwargs): def wrap(user, *args, **kwargs):
qs = CasUser.objects.filter(user=user) if not hasCas(user):
if not qs.count() > 0:
return return
return fct(user, *args, **kwargs) return fct(user, *args, **kwargs)
return wrap 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` # ^ This is not a URLField because we need internal URLS, eg `/static/blah`
date = DateField('Publication') 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', is_special = BooleanField('Numéro spécial',
help_text='Numéro du BOcal non-numéroté', help_text='Numéro du BOcal non-numéroté',
default=False) default=False)

View file

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