Add basic authentication in the server (#1670)

* Add basic authentication in the server

A pattern for creating authentication methods is introduced, with three
authentication types defined:
  none - no authentication
  session - like the current session based auth used for user annotations
  test - used to test the login/logout process end to end

The config endpoint now returns informations about the authentication, like if
the user is authenticated and their username.  The redirect uri's for login and
logout are also returned if the authentication type requires login

This is the first a several PRs for authentication.

*. Update server tests to avoid hardcoded ports

test_api and test_nan_rest now use a common function for starting a test server,
than will initially choose a random port.
This commit is contained in:
bmccandless
2020-07-28 13:28:30 -07:00
committed by GitHub
parent bbef27b8c9
commit 5285556415
14 changed files with 498 additions and 132 deletions
+6
View File
@@ -0,0 +1,6 @@
# import the built in auth types so they can be registered
import server.auth.auth_none # noqa: F401
import server.auth.auth_test # noqa: F401
import server.auth.auth_session # noqa: F401
+80
View File
@@ -0,0 +1,80 @@
from abc import ABC, abstractmethod
class AuthTypeBase(ABC):
"""Base type for all authentication types."""
def __init__(self):
super().__init__()
@abstractmethod
def set_params(self, params):
"""Set the parameters from app config. raise ConfigurationError if any params are invalid"""
pass
@abstractmethod
def is_valid(self):
"""Return True if the auth type can return user info (AuthTypeNone is the only one that cannot)"""
pass
def requires_client_login(self):
"""Return True if the user needs to login from the client (e.g. Login button is shown)"""
return False
@abstractmethod
def is_authenticated(self):
"""Return True if the user is authenticated"""
pass
@abstractmethod
def get_userid(self):
"""Return the id for this user (string)"""
pass
@abstractmethod
def get_username(self):
"""Return the name of the user (string)"""
pass
class AuthTypeClientBase(AuthTypeBase):
"""Base type for all authentication types that require the client to login"""
def __init__(self):
super().__init__()
def requires_client_login(self):
return True
@abstractmethod
def add_url_rules(self, selfapp):
"""Add url rules to the app (like /login, /logout, etc)"""
pass
@abstractmethod
def get_login_url(self, data_adaptor):
"""Return the url for the login route"""
pass
@abstractmethod
def get_logout_url(self, data_adaptor):
"""Return the url for the logout route"""
pass
class AuthTypeFactory:
"""Factory class to create an authentication type"""
auth_types = {}
@staticmethod
def register(name, auth_type):
assert(issubclass(auth_type, AuthTypeBase))
AuthTypeFactory.auth_types[name] = auth_type
@staticmethod
def create(name):
auth_type = AuthTypeFactory.auth_types.get(name)
if auth_type is None:
return None
return auth_type()
+27
View File
@@ -0,0 +1,27 @@
from server.auth.auth import AuthTypeBase, AuthTypeFactory
from server.common.errors import ConfigurationError
class AuthTypeNone(AuthTypeBase):
def __init__(self):
super().__init__()
def is_valid(self):
return False
def set_params(self, params):
if params:
raise ConfigurationError("not expecting authentication parameters")
def is_authenticated(self):
return True
def get_userid(self):
return None
def get_username(self):
return None
AuthTypeFactory.register(None, AuthTypeNone)
+36
View File
@@ -0,0 +1,36 @@
from server.auth.auth import AuthTypeBase, AuthTypeFactory
from flask import session
from uuid import uuid4
class AuthTypeSession(AuthTypeBase):
"""Session based authentication. The user is always logged. The user id is a random number
associated with the session. This is a good choice for desktop servers."""
# key in the session token for userid
CXGUID = "cxguid"
def __init__(self):
super().__init__()
def is_valid(self):
return True
def set_params(self, params):
return
def is_authenticated(self):
# always authenticated
return True
def get_userid(self):
if self.CXGUID not in session:
session[self.CXGUID] = uuid4().hex
session.permanent = True
return session[self.CXGUID]
def get_username(self):
return "anonymous"
AuthTypeFactory.register("session", AuthTypeSession)
+69
View File
@@ -0,0 +1,69 @@
from server.auth.auth import AuthTypeClientBase, AuthTypeFactory
from flask import session, request, redirect, current_app
class AuthTypeTest(AuthTypeClientBase):
"""An authentication type for testing client based logins. When the login route is accessed
the user is automatically logged in with a default or configured username"""
# key in session token with userid and username
CXGUID = "cxguid_test"
CXGUNAME = "cxguname_test"
def __init__(self):
super().__init__()
self.username = "test_account"
self.userid = "id0001"
def is_valid(self):
return True
def requires_client_login(self):
return True
def add_url_rules(self, app):
app.add_url_rule("/login", "login", self.login, methods=["GET"])
app.add_url_rule("/logout", "logout", self.logout, methods=["GET"])
def set_params(self, params):
if params:
self.username = params.get("username", self.username)
self.userid = params.get("userid", self.userid)
def is_authenticated(self):
return self.CXGUID in session
def get_userid(self):
return session.get(self.CXGUID)
def get_username(self):
return session.get(self.CXGUNAME)
def login(self):
args = request.args
return_to = args.get("dataset", "/")
session[self.CXGUID] = args.get("userid", self.userid)
session[self.CXGUNAME] = args.get("username", self.username)
return redirect(return_to)
def logout(self):
session.clear()
return_to = request.args.get("dataset", "/")
return redirect(return_to)
def get_login_url(self, data_adaptor):
"""Return the url for the login route"""
if current_app.app_config.is_multi_dataset():
return f"/login?dataset={data_adaptor.uri_path}"
else:
return "/login"
def get_logout_url(self, data_adaptor):
"""Return the url for the logout route"""
if current_app.app_config.is_multi_dataset():
return f"/logout?dataset={data_adaptor.uri_path}"
else:
return "/logout"
AuthTypeFactory.register("test", AuthTypeTest)