Merge branch 'master' into Evarin/apparence
This commit is contained in:
commit
ce694f617b
10 changed files with 146 additions and 15 deletions
|
@ -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()
|
||||||
|
|
|
@ -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):
|
||||||
|
|
20
api/views.py
20
api/views.py
|
@ -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
1
bocal/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
settings.py
|
|
@ -1 +0,0 @@
|
||||||
settings_dev.py
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
20
mainsite/migrations/0008_publication_unknown_date.py
Normal file
20
mainsite/migrations/0008_publication_unknown_date.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue