Skip to content
This repository has been archived by the owner on Feb 9, 2024. It is now read-only.

Commit

Permalink
Merge pull request #5 from bartfeenstra/silver
Browse files Browse the repository at this point in the history
Silver
  • Loading branch information
bartfeenstra committed Nov 19, 2017
2 parents 6f3b475 + dd66e14 commit ca026e4
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 24 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ python:
cache: pip

install:
- cp tk/default_config.py config.py
- cp travis-ci/config.py config.py
- ./bin/build-dev

script:
Expand Down
7 changes: 7 additions & 0 deletions requirements-dev-frozen.txt
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
asn1crypto==0.23.0
autopep8==1.3.3
certifi==2017.11.5
cffi==1.11.2
chardet==3.0.4
click==6.7
coverage==4.4.2
coveralls==1.2.0
cryptography==2.1.3
docopt==0.6.2
flake8==3.5.0
Flask==0.12.2
Flask-HTTPAuth==3.2.3
idna==2.6
itsdangerous==0.24
Jinja2==2.10
jwcrypto==0.4.2
MarkupSafe==1.0
mccabe==0.6.1
nose2==0.7.2
pycodestyle==2.3.1
pycparser==2.18
pyflakes==1.6.0
python-jwt==3.0.0
requests==2.18.4
requests-futures==0.9.7
requests-mock==1.3.0
Expand Down
8 changes: 8 additions & 0 deletions requirements-frozen.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
asn1crypto==0.23.0
certifi==2017.11.5
cffi==1.11.2
chardet==3.0.4
click==6.7
cryptography==2.1.3
Flask==0.12.2
Flask-HTTPAuth==3.2.3
idna==2.6
itsdangerous==0.24
Jinja2==2.10
jwcrypto==0.4.2
MarkupSafe==1.0
pycparser==2.18
python-jwt==3.0.0
requests==2.18.4
requests-futures==0.9.7
six==1.11.0
urllib3==1.22
Werkzeug==0.12.2
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
flask ~= 0.12.2
flask-httpauth ~= 3.2.3
python_jwt ~= 3.0.0
requests ~= 2.18.4
requests-futures ~= 0.9.7
34 changes: 34 additions & 0 deletions tk/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import datetime

import jwcrypto.jwk as jwk
import python_jwt as jwt


class Auth:
def __init__(self, secret_key, ttl):
assert secret_key is not None
jwt_key = jwk.JWK.generate(kty='oct', k=secret_key)
# A little assurance that we will always enforce a secure signature. If
# the key is None, the JWT signature algorithm will be None, leading to
# unsigned and therefore unauthenticated tokens. For more details, see
# https://www.chosenplaintext.ca/2015/03/31/jwt-algorithm-confusion.html.
assert jwt_key is not None
self._jwt_key = jwt_key
self._algorithm = 'HS512'
self._ttl = ttl

def grant_access_token(self):
return jwt.generate_jwt({}, self._jwt_key, self._algorithm,
datetime.timedelta(seconds=self._ttl))

def verify_access_token(self, access_token):
try:
jwt.verify_jwt(access_token, self._jwt_key,
[self._algorithm])
# This is an overly broad exception clause, because:
# 1) the JWT library does not raise exceptions of a single type.
# 2) calling code will convert this to an appropriate HTTP response.
except Exception as e:
return False

return True
6 changes: 6 additions & 0 deletions tk/default_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@
SOURCEBOX_ACCOUNT_NAME = None
SOURCEBOX_USER_NAME = None
SOURCEBOX_PASSWORD = None
# A list of 2-tuples (username, plain-text password).
USERS = []
# A secret (private) key for symmetric encryption.
SECRET_KEY = None
# The access token lifetime in seconds.
ACCESS_TOKEN_TTL = 600
61 changes: 53 additions & 8 deletions tk/flask/app.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from functools import wraps

from flask import Flask, request, Response
from flask_httpauth import HTTPBasicAuth
from requests_futures.sessions import FuturesSession
from werkzeug.exceptions import NotAcceptable, UnsupportedMediaType, NotFound, \
BadRequest
BadRequest, Forbidden, Unauthorized

from tk.auth import Auth
from tk.process import Process


Expand Down Expand Up @@ -52,29 +54,72 @@ def __init__(self, *args, **kwargs):
super().__init__('tk', static_folder=None, *args, **kwargs)
self.config.from_object('tk.default_config')
self.config.from_envvar('TK_CONFIG_FILE')
self._http_basic_auth = HTTPBasicAuth()
self.auth = Auth(self.config['SECRET_KEY'],
self.config['ACCESS_TOKEN_TTL'])
self._register_routes()
self._session = FuturesSession()
self._process = Process(self._session, self.config['SOURCEBOX_URL'],
self.config['SOURCEBOX_ACCOUNT_NAME'],
self.config['SOURCEBOX_USER_NAME'],
self.config['SOURCEBOX_PASSWORD'])
self.process = Process(self._session, self.config['SOURCEBOX_URL'],
self.config['SOURCEBOX_ACCOUNT_NAME'],
self.config['SOURCEBOX_USER_NAME'],
self.config['SOURCEBOX_PASSWORD'])

def add_user(self, name, password):
self.config['USERS'].append((name, password))

def request_access_token(self, route_method):
"""
Check the request contains a valid access token.
:return:
"""
@wraps(route_method)
def checker(*route_method_args, **route_method_kwargs):
if 'access_token' not in request.args:
raise Unauthorized()
access_token = request.args.get('access_token')
if not self.auth.verify_access_token(access_token):
raise Forbidden()
return route_method(*route_method_args, **route_method_kwargs)

return checker

def _register_routes(self):
@self._http_basic_auth.get_password
def _get_user_password(actual_name):
"""
Retrieves a user's password for HTTP Basic Auth.
:param actual_name:
:return: The password or None if the user does not exist.
"""
for name, password in self.config['USERS']:
if name == actual_name:
return password
return None

@self.route('/accesstoken')
@self._http_basic_auth.login_required
@request_content_type('')
@response_content_type('text/plain')
def access_token():
return self.auth.grant_access_token()

@self.route('/submit', methods=['POST'])
@self.request_access_token
@request_content_type('application/octet-stream')
@response_content_type('text/plain')
def submit():
document = request.get_data()
if not document:
raise BadRequest()
process_id = self._process.submit(document)
process_id = self.process.submit(document)
return Response(process_id, 200, mimetype='text/plain')

@self.route('/retrieve/<process_id>')
@self.request_access_token
@request_content_type('')
@response_content_type('text/xml')
def retrieve(process_id):
result = self._process.retrieve(process_id)
result = self.process.retrieve(process_id)
if result is None:
raise NotFound()
return Response(result, 200, mimetype='text/plain')
return Response(result, 200, mimetype='text/xml')
27 changes: 27 additions & 0 deletions tk/tests/test_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from time import sleep
from unittest import TestCase

from tk.auth import Auth


class AuthTest(TestCase):
def testGrantShouldProduceAccessToken(self):
auth = Auth('foo', 9)
self.assertRegex(auth.grant_access_token(),
'^[^.]+\.[^.]+\.[^.]+$')

def testVerification(self):
auth = Auth('foo', 9)
self.assertTrue(auth.verify_access_token(auth.grant_access_token()))

def testVerifyExpiredToken(self):
auth = Auth('foo', 1)
token = auth.grant_access_token()
sleep(2)
self.assertFalse(auth.verify_access_token(token))

def testVerifyInvalidSignature(self):
auth_a = Auth('foo', 9)
auth_b = Auth('bar', 9)
self.assertFalse(auth_b.verify_access_token(
auth_a.grant_access_token()))
Loading

0 comments on commit ca026e4

Please sign in to comment.