add apiuser models and middleware (#2861)

This commit is contained in:
Thomas NOËL 2013-06-14 10:56:20 +02:00
parent bdb79920ad
commit 233f0c905e
8 changed files with 173 additions and 29 deletions

5
passerelle/base/admin.py Normal file
View File

@ -0,0 +1,5 @@
from django.contrib import admin
from .models import ApiUser
admin.site.register(ApiUser)

View File

@ -0,0 +1,53 @@
import hashlib
import datetime
from .models import ApiUser
from .signature import check_query
import logging
logger = logging.getLogger(__name__)
def get_apiuser_with_signature(orig, query):
"""
Returns ApiUser(username=orig) if the signature in the query string matches
"""
try:
apiuser = ApiUser.objects.get(keytype='SIGN', username=orig)
except ApiUser.DoesNotExist:
logger.warning('unknown user')
return None
try:
if check_query(query, apiuser.key):
return apiuser
except:
pass
logger.warning('invalid signature')
return None
class SearchApiUser(object):
def process_request(self, request):
request.apiuser = None
if 'orig' in request.GET and 'signature' in request.GET:
request.apiuser = get_apiuser_with_signature(
orig=request.GET['orig'],
query=request.META['QUERY_STRING'])
elif 'apikey' in request.GET:
try:
request.apiuser = ApiUser.objects.get(keytype='API',
key=request.GET['apikey'])
except ApiUser.DoesNotExist:
logger.warning('unknown apikey')
elif request.META.has_key('HTTP_AUTHORIZATION'):
(scheme, param) = request.META['HTTP_AUTHORIZATION'].split(' ',1)
if scheme.lower() == 'basic':
username, password = param.strip().decode('base64').split(':',1)
try:
request.apiuser = ApiUser.objects.get(keytype='SIGN',
username=username, key=password)
except ApiUser.DoesNotExist:
logger.warning('HTTP_AUTHORIZATION: unknown user/password')

35
passerelle/base/models.py Normal file
View File

@ -0,0 +1,35 @@
from django.db import models
from model_utils.managers import InheritanceManager
KEYTYPE_CHOICES = (
('API', 'API Key'),
('SIGN', 'Signature HMAC'),
)
class ApiUser(models.Model):
username = models.CharField(max_length=50)
key = models.CharField(max_length=256)
keytype = models.CharField(max_length=4, choices=KEYTYPE_CHOICES)
fullname = models.CharField(max_length=50)
description = models.TextField(blank=True)
def __unicode__(self):
return u'%s <%s>' % (self.fullname, self.username)
class BaseResource(models.Model):
title = models.CharField(max_length=50)
slug = models.SlugField()
description = models.TextField()
users = models.ManyToManyField(ApiUser, blank=True)
objects = InheritanceManager()
parameters = None
class Meta:
abstract = True
def __unicode__(self):
return self.title

View File

@ -0,0 +1,72 @@
import datetime
import base64
import hmac
import hashlib
import urllib
import random
import urlparse
'''Simple signature scheme for query strings'''
# from http://repos.entrouvert.org/portail-citoyen.git/tree/portail_citoyen/apps/data_source_plugin/signature.py
def sign_url(url, key, algo='sha256', timestamp=None, nonce=None):
parsed = urlparse.urlparse(url)
new_query = sign_query(parsed.query, key, algo, timestamp, nonce)
return urlparse.urlunparse(parsed[:4] + (new_query,) + parsed[5:])
def sign_query(query, key, algo='sha256', timestamp=None, nonce=None):
if timestamp is None:
timestamp = datetime.datetime.utcnow()
timestamp = timestamp.strftime('%Y-%m-%dT%H:%M:%SZ')
if nonce is None:
nonce = hex(random.getrandbits(128))[2:]
new_query = query
if new_query:
new_query += '&'
new_query += urllib.urlencode((
('algo', algo),
('timestamp', timestamp),
('nonce', nonce)))
signature = base64.b64encode(sign_string(new_query, key, algo=algo))
new_query += '&signature=' + urllib.quote(signature)
return new_query
def sign_string(s, key, algo='sha256', timedelta=30):
digestmod = getattr(hashlib, algo)
hash = hmac.HMAC(key, digestmod=digestmod, msg=s)
return hash.digest()
def check_url(url, key, known_nonce=None, timedelta=30):
parsed = urlparse.urlparse(url, 'https')
return check_query(parsed.query, key)
def check_query(query, key, known_nonce=None, timedelta=30):
parsed = urlparse.parse_qs(query)
signature = base64.b64decode(parsed['signature'][0])
algo = parsed['algo'][0]
timestamp = parsed['timestamp'][0]
timestamp = datetime.datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%SZ')
nonce = parsed['nonce']
unsigned_query = query.split('&signature=')[0]
if known_nonce is not None and known_nonce(nonce):
return False
print 'timedelta', datetime.datetime.utcnow() - timestamp
if abs(datetime.datetime.utcnow() - timestamp) > datetime.timedelta(seconds=timedelta):
return False
return check_string(unsigned_query, signature, key, algo=algo)
def check_string(s, signature, key, algo='sha256'):
# constant time compare
signature2 = sign_string(s, key, algo=algo)
if len(signature2) != len(signature):
return False
res = 0
for a, b in zip(signature, signature2):
res |= ord(a) ^ ord(b)
return res == 0
if __name__ == '__main__':
key = '12345'
signed_query = sign_query('NameId=_12345&orig=montpellier', key)
assert check_query(signed_query, key, timedelta=0) is False
assert check_query(signed_query, key) is True

View File

@ -1,20 +1,9 @@
import csv
from django.db import models
from passerelle.base.models import BaseResource
from model_utils.managers import InheritanceManager
class BaseDataSource(models.Model):
title = models.CharField(max_length=50)
slug = models.SlugField()
description = models.TextField()
parameters = None
objects = InheritanceManager()
def __unicode__(self):
return self.title
class BaseDataSource(BaseResource):
def get_data(self, **kwargs):
raise NotImplementedError

View File

@ -1,26 +1,13 @@
import csv
from django.db import models
from model_utils.managers import InheritanceManager
import json
from urllib2 import Request, urlopen
from django.db import models
from passerelle.base.models import BaseResource
class BaseRepost(models.Model):
title = models.CharField(max_length=50)
slug = models.SlugField()
description = models.TextField()
class BaseRepost(BaseResource):
url = models.CharField(max_length=200)
timeout = models.IntegerField(null=True, blank=True)
parameters = None
objects = InheritanceManager()
def __unicode__(self):
return self.title
def repost(self, data, **kwargs):
r = Request(self.url)
r.add_header('Accept', 'application/json')

View File

@ -93,6 +93,7 @@ MIDDLEWARE_CLASSES = (
'django.contrib.messages.middleware.MessageMiddleware',
# Uncomment the next line for simple clickjacking protection:
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'passerelle.base.middleware.SearchApiUser'
)
ROOT_URLCONF = 'passerelle.urls'
@ -127,6 +128,7 @@ INSTALLED_APPS = (
'django.contrib.admin',
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
'passerelle.base',
'passerelle.datasources',
'passerelle.repost',
'clicrdv',

View File

@ -1,4 +1,5 @@
django < 1.6
south
http://pypi.python.org/packages/source/d/django-jsonresponse/django-jsonresponse-0.5.tar.gz
django-model-utils
SOAPpy