add apiuser models and middleware (#2861)
This commit is contained in:
parent
bdb79920ad
commit
233f0c905e
|
@ -0,0 +1,5 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import ApiUser
|
||||
|
||||
admin.site.register(ApiUser)
|
|
@ -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')
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue