From 865a2490026aa2362590cc25ebddf4b7ffd47d9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Bastian?= Date: Sun, 15 Oct 2017 17:09:14 +0200 Subject: [PATCH 1/9] Fix first CAS connection problem `user` was not returned by configure_user --- bocal_auth/cas_backend.py | 2 +- bocal_auth/rhosts.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/bocal_auth/cas_backend.py b/bocal_auth/cas_backend.py index ed50d2b..af8e0b8 100644 --- a/bocal_auth/cas_backend.py +++ b/bocal_auth/cas_backend.py @@ -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 diff --git a/bocal_auth/rhosts.py b/bocal_auth/rhosts.py index 2a17323..1b9230e 100644 --- a/bocal_auth/rhosts.py +++ b/bocal_auth/rhosts.py @@ -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 From 87071ef526ff822c3c39fa23eebc69c35c5985a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Bastian?= Date: Mon, 16 Oct 2017 17:04:51 +0200 Subject: [PATCH 2/9] Make API client a standalone script --- api/client/apiclient.py | 83 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/api/client/apiclient.py b/api/client/apiclient.py index 018a430..526795e 100644 --- a/api/client/apiclient.py +++ b/api/client/apiclient.py @@ -4,7 +4,10 @@ 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): @@ -59,3 +62,81 @@ 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): + publish_date = date.today() if not args.date else args.date + (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) + 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() From 6892b04742b72c48540c23e401ef86f7073a2ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Bastian?= Date: Mon, 16 Oct 2017 17:23:56 +0200 Subject: [PATCH 3/9] Don't use `random.choices` (new with python 3.6) Poor Debian users. --- api/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/models.py b/api/models.py index 2ea06c5..1ce36ca 100644 --- a/api/models.py +++ b/api/models.py @@ -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): From fcdbf515e93cb9d88cb1151818707db0aec56494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Bastian?= Date: Mon, 16 Oct 2017 19:47:17 +0200 Subject: [PATCH 4/9] API client: fix date input --- api/client/apiclient.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/api/client/apiclient.py b/api/client/apiclient.py index 526795e..95e7af9 100644 --- a/api/client/apiclient.py +++ b/api/client/apiclient.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 + ''' API client for bocal-site ''' import json @@ -91,7 +93,11 @@ def cmd(func): @cmd def cmd_publish(token, args): - publish_date = date.today() if not args.date else args.date + 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, From 97705834fd716ec9a63e86352e3fe6807945aae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Bastian?= Date: Mon, 16 Oct 2017 19:54:45 +0200 Subject: [PATCH 5/9] API client: add default host --- api/client/apiclient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/client/apiclient.py b/api/client/apiclient.py index 95e7af9..fa8825e 100644 --- a/api/client/apiclient.py +++ b/api/client/apiclient.py @@ -118,7 +118,7 @@ def setup_argparse(): 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) + parser.set_defaults(creds=TOKEN_DFT_FILE, host=DFT_HOST) subparsers = parser.add_subparsers() parser_publish = subparsers.add_parser('publier', From f01c0b5594904a5c6ffe8073ce6e49364ab4c445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Bastian?= Date: Mon, 16 Oct 2017 20:06:56 +0200 Subject: [PATCH 6/9] Do not ship settings.py as a symlink --- bocal/.gitignore | 1 + bocal/settings.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 bocal/.gitignore delete mode 120000 bocal/settings.py diff --git a/bocal/.gitignore b/bocal/.gitignore new file mode 100644 index 0000000..fce19e4 --- /dev/null +++ b/bocal/.gitignore @@ -0,0 +1 @@ +settings.py diff --git a/bocal/settings.py b/bocal/settings.py deleted file mode 120000 index f1c999f..0000000 --- a/bocal/settings.py +++ /dev/null @@ -1 +0,0 @@ -settings_dev.py \ No newline at end of file From 2264b0886f34e66995c4a480ab3a9d784e552e34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Bastian?= Date: Mon, 16 Oct 2017 20:02:36 +0200 Subject: [PATCH 7/9] Change API protocol to fix json normalization A dictionnary's order is not deterministic (eg. when switching python versions), thus the previous protocol would often fail to authenticate a legitimate request. --- api/client/apiclient.py | 5 +++-- api/views.py | 20 ++++++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/api/client/apiclient.py b/api/client/apiclient.py index fa8825e..e6016db 100644 --- a/api/client/apiclient.py +++ b/api/client/apiclient.py @@ -33,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, @@ -43,7 +44,7 @@ def sendReq(url): return { 'auth': auth, - 'req': payload, + 'req': payload_enc, } def decorator(fct): diff --git a/api/views.py b/api/views.py index 200ff06..f015373 100644 --- a/api/views.py +++ b/api/views.py @@ -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 From 32716892d7e50a52ef3fff85dc01c82a3703c022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Bastian?= Date: Mon, 16 Oct 2017 21:23:10 +0200 Subject: [PATCH 8/9] API client: Enforce https --- api/client/apiclient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/client/apiclient.py b/api/client/apiclient.py index e6016db..2d01d7c 100644 --- a/api/client/apiclient.py +++ b/api/client/apiclient.py @@ -15,7 +15,7 @@ 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) From e6cdf40fdc363f19e8d3442060f99847fb95db79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Bastian?= Date: Tue, 17 Oct 2017 16:29:42 +0200 Subject: [PATCH 9/9] Add possibility to have an unknown publication date --- .../0008_publication_unknown_date.py | 20 +++++++++++++++++++ mainsite/models.py | 7 +++++++ .../mainsite/publications_list_view.html | 4 ++++ 3 files changed, 31 insertions(+) create mode 100644 mainsite/migrations/0008_publication_unknown_date.py diff --git a/mainsite/migrations/0008_publication_unknown_date.py b/mainsite/migrations/0008_publication_unknown_date.py new file mode 100644 index 0000000..810d600 --- /dev/null +++ b/mainsite/migrations/0008_publication_unknown_date.py @@ -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'), + ), + ] diff --git a/mainsite/models.py b/mainsite/models.py index 522d786..76fba29 100644 --- a/mainsite/models.py +++ b/mainsite/models.py @@ -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) diff --git a/mainsite/templates/mainsite/publications_list_view.html b/mainsite/templates/mainsite/publications_list_view.html index f2b2ee3..3f7f574 100644 --- a/mainsite/templates/mainsite/publications_list_view.html +++ b/mainsite/templates/mainsite/publications_list_view.html @@ -16,7 +16,11 @@ Millésime {{ year_range }} {% for bocal in publications %} + {% if bocal.unknown_date %} + + {% else %} + {% endif %}
Date inconnue{{ bocal.date | date:"d/m/Y" }} {{ bocal }}