mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-27 11:38:11 +08:00
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:
@@ -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
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user