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

Commit

Permalink
Disallow cross-user access.
Browse files Browse the repository at this point in the history
  • Loading branch information
bartfeenstra committed Nov 27, 2017
1 parent 22b0813 commit a8c1340
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 32 deletions.
25 changes: 19 additions & 6 deletions tk/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,31 @@ def __init__(self, secret_key, ttl):
self._algorithm = 'HS512'
self._ttl = ttl

def grant_access_token(self):
return jwt.generate_jwt({}, self._jwt_key, self._algorithm,
def grant_access_token(self, user_name):
"""
Grants an access token to the given user.
:param user_name: The name of the user to grant the access token to.
:return: str
"""
claims = {
'user': user_name,
}
return jwt.generate_jwt(claims, self._jwt_key, self._algorithm,
datetime.timedelta(seconds=self._ttl))

def verify_access_token(self, access_token):
"""
Verifies an access token.
:param access_token:
:return: The name of the authenticated user.
"""
try:
jwt.verify_jwt(access_token, self._jwt_key,
[self._algorithm])
jwt_header, jwt_claims = 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 None

return True
return jwt_claims['user']
18 changes: 12 additions & 6 deletions tk/flask/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,10 @@ 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):
user_name = self.auth.verify_access_token(access_token)
if user_name is None:
raise Forbidden()
request._tk_auth_user_name = user_name
return route_method(*route_method_args, **route_method_kwargs)

return checker
Expand All @@ -93,6 +95,7 @@ def _get_user_password(actual_name):
"""
for name, password in self.config['USERS']:
if name == actual_name:
request._tk_auth_user_name = name
return password
return None

Expand All @@ -101,7 +104,7 @@ def _get_user_password(actual_name):
@request_content_type('')
@response_content_type('text/plain')
def access_token():
return self.auth.grant_access_token()
return self.auth.grant_access_token(request._tk_auth_user_name)

@self.route('/submit', methods=['POST'])
@self.request_access_token
Expand All @@ -111,15 +114,18 @@ def submit():
document = request.get_data()
if not document:
raise BadRequest()
process_id = self.process.submit(document)
process_id = self.process.submit(
request._tk_auth_user_name, 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)
if result is None:
process = self.process.retrieve(process_id)
if process is None:
raise NotFound()
return Response(result, 200, mimetype='text/xml')
if request._tk_auth_user_name != process[0]:
raise Forbidden()
return Response(process[1], 200, mimetype='text/xml')
8 changes: 5 additions & 3 deletions tk/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

class Process:
def __init__(self, session, sourcebox_url, sourcebox_account_name, sourcebox_user_name, sourcebox_password):
# Values are 2-tuples (user_name: str, result: str).
self._processes = {}
self._process_queue = Queue()
self._session = session
Expand All @@ -21,12 +22,13 @@ def __init__(self, session, sourcebox_url, sourcebox_account_name, sourcebox_use
def _process_queue_worker(self, queue):
while True:
process_id, profile = queue.get()
self._processes[process_id] = profile
self._processes[process_id] = (
self._processes[process_id][0], profile)
queue.task_done()

def submit(self, document):
def submit(self, user_name, document):
process_id = str(uuid.uuid4())
self._processes[process_id] = 'PROGRESS'
self._processes[process_id] = (user_name, 'PROGRESS')
self._session.post(self._sourcebox_url, data={
'account': self._sourcebox_account_name,
'username': self._sourcebox_user_name,
Expand Down
17 changes: 11 additions & 6 deletions tk/tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,26 @@
class AuthTest(TestCase):
def testGrantShouldProduceAccessToken(self):
auth = Auth('foo', 9)
self.assertRegex(auth.grant_access_token(),
user_name = 'User Foo'
self.assertRegex(auth.grant_access_token(user_name),
'^[^.]+\.[^.]+\.[^.]+$')

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

def testVerifyExpiredToken(self):
auth = Auth('foo', 1)
token = auth.grant_access_token()
user_name = 'User Foo'
token = auth.grant_access_token(user_name)
sleep(2)
self.assertFalse(auth.verify_access_token(token))
self.assertIsNone(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()))
user_name = 'User Foo'
self.assertIsNone(auth_b.verify_access_token(
auth_a.grant_access_token(user_name)))
41 changes: 30 additions & 11 deletions tk/tests/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,15 +136,15 @@ def testWithDisallowedMethodShould405(self, method):

def testWithUnsupportedMediaTypeShould415(self):
response = self._flask_app_client.post('/submit', query_string={
'access_token': self._flask_app.auth.grant_access_token(),
'access_token': self._flask_app.auth.grant_access_token('User Foo'),
})
self.assertEquals(415, response.status_code)

def testWithNotAcceptableShould406(self):
response = self._flask_app_client.post('/submit', headers={
'Content-Type': 'application/octet-stream'
}, query_string={
'access_token': self._flask_app.auth.grant_access_token(),
'access_token': self._flask_app.auth.grant_access_token('User Foo'),
})
self.assertEquals(406, response.status_code)

Expand All @@ -153,7 +153,7 @@ def testWithMissingDocumentShould400(self):
'Accept': 'text/plain',
'Content-Type': 'application/octet-stream'
}, query_string={
'access_token': self._flask_app.auth.grant_access_token(),
'access_token': self._flask_app.auth.grant_access_token('User Foo'),
})
self.assertEquals(400, response.status_code)

Expand All @@ -180,7 +180,7 @@ def testSuccess(self, m):
'Accept': 'text/plain',
'Content-Type': 'application/octet-stream'
}, data=b'I am an excellent CV, mind you.', query_string={
'access_token': self._flask_app.auth.grant_access_token(),
'access_token': self._flask_app.auth.grant_access_token('User Foo'),
})
self.assertEquals(200, response.status_code)
# Assert the response contains a plain-text process UUID.
Expand All @@ -199,21 +199,21 @@ def testWithUnsupportedMediaTypeShould415(self):
response = self._flask_app_client.get('/retrieve/foo', headers={
'Content-Type': 'text/plain',
}, query_string={
'access_token': self._flask_app.auth.grant_access_token(),
'access_token': self._flask_app.auth.grant_access_token('User Foo'),
})
self.assertEquals(response.status_code, 415)

def testWithNotAcceptableShould406(self):
response = self._flask_app_client.get('/retrieve/foo', query_string={
'access_token': self._flask_app.auth.grant_access_token(),
'access_token': self._flask_app.auth.grant_access_token('User Foo'),
})
self.assertEquals(response.status_code, 406)

def testWithUnknownProcessIdShould404(self):
response = self._flask_app_client.get('/retrieve/foo', headers={
'Accept': 'text/xml',
}, query_string={
'access_token': self._flask_app.auth.grant_access_token(),
'access_token': self._flask_app.auth.grant_access_token('User Foo'),
})
self.assertEquals(response.status_code, 404)

Expand All @@ -236,21 +236,25 @@ def testWithForbiddenShould403(self):

@requests_mock.mock()
def testSuccessWithUnprocessedDocument(self, m):
user_name = 'User Foo'
m.post(self._flask_app.config['SOURCEBOX_URL'], text=delayed_profile)
process_id = self._flask_app.process.submit(b'I am an excellent CV, mind you.')
process_id = self._flask_app.process.submit(
user_name, b'I am an excellent CV, mind you.')
response = self._flask_app_client.get('/retrieve/%s' % process_id,
headers={
'Accept': 'text/xml',
}, query_string={
'access_token': self._flask_app.auth.grant_access_token(),
'access_token': self._flask_app.auth.grant_access_token(user_name),
})
self.assertEquals(response.status_code, 200)
self.assertEquals(response.get_data(as_text=True), 'PROGRESS')

@requests_mock.mock()
def testSuccessWithProcessedDocument(self, m):
user_name = 'User Foo'
m.post(self._flask_app.config['SOURCEBOX_URL'], text=PROFILE)
process_id = self._flask_app.process.submit(b'I am an excellent CV too, mind you.')
process_id = self._flask_app.process.submit(
user_name, b'I am an excellent CV too, mind you.')
# Sleep to allow asynchronous processing of the document. This is not
# ideal as it could lead to random test failures, but it is
# unavoidable without additional tools.
Expand All @@ -259,7 +263,22 @@ def testSuccessWithProcessedDocument(self, m):
headers={
'Accept': 'text/xml',
}, query_string={
'access_token': self._flask_app.auth.grant_access_token(),
'access_token': self._flask_app.auth.grant_access_token(user_name),
})
self.assertEquals(response.status_code, 200)
self.assertEquals(response.get_data(as_text=True), PROFILE)

@requests_mock.mock()
def testSuccessWithSomeoneElsesDocument(self, m):
my_user_name = 'User Foo'
someone_elses_user_name = 'User Bar'
m.post(self._flask_app.config['SOURCEBOX_URL'], text=delayed_profile)
process_id = self._flask_app.process.submit(
someone_elses_user_name, b'I am an excellent CV, mind you.')
response = self._flask_app_client.get('/retrieve/%s' % process_id,
headers={
'Accept': 'text/xml',
}, query_string={
'access_token': self._flask_app.auth.grant_access_token(my_user_name),
})
self.assertEquals(response.status_code, 403)

0 comments on commit a8c1340

Please sign in to comment.