From 2b6b0dc6716dbb9b7b1ca464af2f42671e613a29 Mon Sep 17 00:00:00 2001 From: Markus Scheidgen <markus.scheidgen@gmail.com> Date: Thu, 29 Nov 2018 14:24:02 +0100 Subject: [PATCH] Added x-token based authentication. --- nomad/api/app.py | 40 ++++++++++++++++++++++++++++++++++++++-- nomad/api/repository.py | 4 ++-- tests/test_api.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/nomad/api/app.py b/nomad/api/app.py index 9912a87ca1..30e85918ee 100644 --- a/nomad/api/app.py +++ b/nomad/api/app.py @@ -19,10 +19,24 @@ authentication or access tokens. Currently the authentication is validated again users and sessions in the NOMAD-coe repository postgres db. .. autodata:: base_path + +There are two authentication "schemes" to authenticate users. First we use +HTTP Basic Authentication (username, password), which also works with username=token, +password=''. Second, there is a curstom HTTP header 'X-Token' that can be used to +give a token. The first precedes the second. The used tokens are given and stored +by the NOMAD-coe repository GUI. + +Authenticated user information is available via FLASK's build in flask.g.user object. +It is set to None, if no user information is available. + +There are two decorators for FLASK API endpoints that can be used if endpoints require +authenticated user information for authorization or otherwise. + +.. autofunction:: login_if_available .. autofunction:: login_really_required """ -from flask import Flask, g +from flask import Flask, g, request from flask_restful import Api, abort from flask_cors import CORS from flask_httpauth import HTTPBasicAuth @@ -72,12 +86,34 @@ def verify_password(username_or_token, password): return True +def login_if_available(func): + """ + A decorator for API endpoint implementations that might authenticate users, but + provide limited functionality even without users. + """ + @auth.login_required + def wrapper(*args, **kwargs): + # TODO the cutom X-Token based authentication should be replaced by a real + # Authentication header based token authentication + if not g.user and 'X-Token' in request.headers: + token = request.headers['X-Token'] + g.user = User.verify_auth_token(token) + if not g.user: + abort(401, message='Provided access token is not valid or does not exist.') + + return func(*args, **kwargs) + + wrapper.__name__ = func.__name__ + wrapper.__doc__ = func.__doc__ + return wrapper + + def login_really_required(func): """ A decorator for API endpoint implementations that forces user authentication on endpoints. """ - @auth.login_required + @login_if_available def wrapper(*args, **kwargs): if g.user is None: abort(401, message='Anonymous access is forbidden, authorization required') diff --git a/nomad/api/repository.py b/nomad/api/repository.py index f4a863fcfa..d90a60da1a 100644 --- a/nomad/api/repository.py +++ b/nomad/api/repository.py @@ -23,7 +23,7 @@ from flask_restful import Resource, abort from nomad.repo import RepoCalc -from .app import api, auth, base_path +from .app import api, auth, base_path, login_if_available class RepoCalcRes(Resource): @@ -90,7 +90,7 @@ class RepoCalcRes(Resource): class RepoCalcsRes(Resource): - @auth.login_required + @login_if_available def get(self): """ Get *'all'* calculations in repository from, paginated. diff --git a/tests/test_api.py b/tests/test_api.py index b36c759425..861690acbc 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -87,6 +87,35 @@ def assert_upload(upload_json_str, id=None, **kwargs): return data +def test_xtoken_auth(client, test_user, no_warn): + rv = client.get('/uploads', headers={ + 'X-Token': test_user.email + }) + + assert rv.status_code == 200 + + +def test_xtoken_auth_denied(client, no_warn): + rv = client.get('/uploads', headers={ + 'X-Token': 'invalid' + }) + + assert rv.status_code == 401 + + +def test_basic_auth(client, test_user_auth, no_warn): + rv = client.get('/uploads', headers=test_user_auth) + assert rv.status_code == 200 + + +def test_basic_auth_denied(client, no_warn): + basic_auth_base64 = base64.b64encode('invalid'.encode('utf-8')).decode('utf-8') + rv = client.get('/uploads', headers={ + 'Authorization': 'Basic %s' % basic_auth_base64 + }) + assert rv.status_code == 401 + + def test_no_uploads(client, test_user_auth, no_warn): rv = client.get('/uploads', headers=test_user_auth) -- GitLab