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