Skip to content

Commit

Permalink
Add ability to reject requests missing x-annotator auth headers
Browse files Browse the repository at this point in the history
--HG--
extra : convert_revision : ac80ff9980c864119af7139066c973c341d6e162
  • Loading branch information
nickstenning committed Dec 10, 2010
1 parent 4b94184 commit 4c816d2
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 16 deletions.
3 changes: 2 additions & 1 deletion annotator.cfg
Expand Up @@ -5,4 +5,5 @@ DEBUG = True
ROOT = os.path.dirname(os.path.abspath( __file__ ))
DB = "sqlite:///%s/db/annotator.sqlite" % ROOT

MOUNTPOINT = "/store/annotations"
MOUNTPOINT = "/store/annotations"
AUTH_ON = True
25 changes: 23 additions & 2 deletions annotator/auth.py
Expand Up @@ -3,7 +3,7 @@

import iso8601

__all__ = ["consumers", "verify_token", "Consumer"]
__all__ = ["consumers", "verify_token", "verify_request", "Consumer"]

# Hard-code this for the moment. It can go into the database later.
consumers = {
Expand Down Expand Up @@ -40,6 +40,27 @@ def verify_token(token, key, userId, issueTime):

return True

def verify_request(request):
pre = 'x-annotator-'

required = ['auth-token', 'auth-token-issue-time', 'consumer-key', 'user-id']
headers = [pre + key for key in required]

rh = request.headers

# False if not all the required headers have been provided
if not set(headers) <= set(rh):
return False

result = verify_token(
rh[pre + 'auth-token'],
rh[pre + 'consumer-key'],
rh[pre + 'user-id'],
rh[pre + 'auth-token-issue-time']
)

return result

class Consumer():
def __init__(self, key):
self.data = consumers[key]
Expand All @@ -50,4 +71,4 @@ def secret(self):

@property
def ttl(self):
return self.data['ttl']
return self.data['ttl']
33 changes: 21 additions & 12 deletions annotator/store.py
@@ -1,31 +1,40 @@
from flask import Flask, Module
from flask import abort, current_app, json, redirect, request, url_for
from flask import abort, json, redirect, request, url_for

from .model import Annotation, Range, session
from . import auth

__all__ = ["app", "store", "setup_app"]

app = Flask(__name__)
app = Flask('annotator')
store = Module(__name__)

def setup_app():
app.register_module(store, url_prefix=app.config['MOUNTPOINT'])

# We define our own jsonify rather than using flask.jsonify because we wish
# to jsonify arbitrary objects (e.g. index returns a list) rather than kwargs.
def jsonify(obj):
def jsonify(obj, *args, **kwargs):
res = json.dumps(obj, indent=None if request.is_xhr else 2)
return current_app.response_class(res, mimetype='application/json')
return app.response_class(res, mimetype='application/json', *args, **kwargs)

def unjsonify(str):
return json.loads(str)

@store.before_request
def before_request():
if app.config['AUTH_ON'] and not auth.verify_request(request):
return jsonify("Cannot authorise request. Perhaps you didn't send the x-annotator headers?", status=401)


@store.after_request
def after_request(response):
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Expose-Headers'] = 'Location'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE'
response.headers['Access-Control-Max-Age'] = '86400'
if response.status_code < 300:
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Expose-Headers'] = 'Location'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE'
response.headers['Access-Control-Max-Age'] = '86400'

return response

# INDEX
Expand All @@ -46,7 +55,7 @@ def create_annotation():

return redirect(url_for('read_annotation', id=annotation.id), 303)
else:
return jsonify('No parameters given. Annotation not created.'), 400
return jsonify('No parameters given. Annotation not created.', status=400)

# READ
@store.route('/<int:id>')
Expand All @@ -56,7 +65,7 @@ def read_annotation(id):
if annotation:
return jsonify(annotation.to_dict())
else:
return jsonify('Annotation not found.'), 404
return jsonify('Annotation not found.', status=404)

# UPDATE
@store.route('/<int:id>', methods=['PUT'])
Expand All @@ -72,7 +81,7 @@ def update_annotation(id):

return jsonify(annotation.to_dict())
else:
return jsonify('Annotation not found. No update performed.'), 404
return jsonify('Annotation not found. No update performed.', status=404)

# DELETE
@store.route('/<int:id>', methods=['DELETE'])
Expand All @@ -85,4 +94,4 @@ def delete_annotation(id):

return None, 204
else:
return jsonify('Annotation not found. No delete performed.'), 404
return jsonify('Annotation not found. No delete performed.', status=404)
23 changes: 23 additions & 0 deletions tests/annotator/test_auth.py
Expand Up @@ -10,6 +10,10 @@
}
}

class MockRequest():
def __init__(self, headers):
self.headers = headers

def iso8601(t):
t = datetime.datetime.now() if t == 'now' else t
return t.strftime("%Y-%m-%dT%H:%M:%S")
Expand All @@ -18,6 +22,14 @@ def make_token(consumerKey, userId, issueTime):
c = auth.Consumer(key=consumerKey)
return hashlib.sha256(c.secret + userId + issueTime).hexdigest()

def make_request(consumerKey, userId, issueTime):
return MockRequest({
'x-annotator-consumer-key': consumerKey,
'x-annotator-auth-token': make_token(consumerKey, userId, issueTime),
'x-annotator-auth-token-issue-time': issueTime,
'x-annotator-user-id': userId
})

def setup():
auth.consumers = fixture

Expand All @@ -37,6 +49,17 @@ def test_reject_expired_token(self):
tok = make_token('testConsumer', 'alice', issueTime)
assert not auth.verify_token(tok, 'testConsumer', 'bob', issueTime), "token had expired, should have been rejected"

def test_verify_request(self):
issueTime = iso8601('now')
request = make_request('testConsumer', 'alice', issueTime)
assert auth.verify_request(request), "request should have been verified"

def test_reject_request_missing_headers(self):
issueTime = iso8601('now')
request = make_request('testConsumer', 'alice', issueTime)
del request.headers['x-annotator-consumer-key']
assert not auth.verify_request(request), "request missing consumerKey should have been rejected"

class TestConsumer():
def test_consumer_secret(self):
c = auth.Consumer(key='testConsumer')
Expand Down
17 changes: 16 additions & 1 deletion tests/annotator/test_store.py
Expand Up @@ -10,6 +10,7 @@ def setup():

class TestStore():
def setup(self):
app.config['AUTH_ON'] = False
self.app = app.test_client()
create_all()

Expand Down Expand Up @@ -80,4 +81,18 @@ def test_cors_preflight(self):
"Did not send the right Access-Control-Allow-Origin header."

assert headers['Access-Control-Expose-Headers'] == 'Location', \
"Did not send the right Access-Control-Expose-Headers header."
"Did not send the right Access-Control-Expose-Headers header."

class TestStoreAuth():
def setup(self):
app.config['AUTH_ON'] = True
self.app = app.test_client()
create_all()

def teardown(self):
session.remove()
drop_all()

def test_reject_bare_request(self):
response = self.app.get('/store')
assert response.status_code == 401, "response should be 401 NOT AUTHORIZED"

0 comments on commit 4c816d2

Please sign in to comment.