mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-26 12:48: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:
@@ -1,6 +1,5 @@
|
||||
from datetime import datetime
|
||||
import re
|
||||
from uuid import uuid4
|
||||
import os
|
||||
import pandas as pd
|
||||
from hashlib import blake2b
|
||||
@@ -11,7 +10,7 @@ from server.common.errors import AnnotationsError, OntologyLoadFailure
|
||||
from server.common.utils import series_to_schema
|
||||
import fsspec
|
||||
import fastobo
|
||||
from flask import session
|
||||
from flask import session, current_app
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
|
||||
@@ -80,7 +79,6 @@ class Annotations(metaclass=ABCMeta):
|
||||
|
||||
class AnnotationsLocalFile(Annotations):
|
||||
|
||||
CXGUID = "cxguid"
|
||||
CXG_ANNO_COLLECTION = "cxg_anno_collection"
|
||||
|
||||
def __init__(self, output_dir, output_file):
|
||||
@@ -159,18 +157,12 @@ class AnnotationsLocalFile(Annotations):
|
||||
self.last_fname = fname
|
||||
self.last_labels = df
|
||||
|
||||
def _get_userid(self):
|
||||
if self.CXGUID not in session:
|
||||
session[self.CXGUID] = uuid4().hex
|
||||
session.permanent = True
|
||||
return session[self.CXGUID]
|
||||
|
||||
def _get_userdata_idhash(self, data_adaptor):
|
||||
"""
|
||||
Return a short hash that weakly identifies the user and dataset.
|
||||
Used to create safe annotations output file names.
|
||||
"""
|
||||
uid = self._get_userid()
|
||||
uid = current_app.auth.get_userid()
|
||||
id = (uid + data_adaptor.get_location()).encode()
|
||||
idhash = base64.b32encode(blake2b(id, digest_size=5).digest()).decode("utf-8")
|
||||
return idhash
|
||||
@@ -257,8 +249,9 @@ class AnnotationsLocalFile(Annotations):
|
||||
|
||||
elif session is not None:
|
||||
collection = self.get_collection()
|
||||
params["annotations-user-data-idhash"] = self._get_userdata_idhash(data_adaptor)
|
||||
params["annotations-data-collection-is-read-only"] = False
|
||||
params["annotations-data-collection-name"] = collection
|
||||
if current_app.auth.is_authenticated():
|
||||
params["annotations-user-data-idhash"] = self._get_userdata_idhash(data_adaptor)
|
||||
params["annotations-data-collection-is-read-only"] = False
|
||||
params["annotations-data-collection-name"] = collection
|
||||
|
||||
parameters.update(params)
|
||||
|
||||
@@ -16,6 +16,7 @@ from server.common.annotations import AnnotationsLocalFile
|
||||
from server.common.utils import custom_format_warning
|
||||
import server.compute.diffexp_cxg as diffexp_tiledb
|
||||
from server.common.data_locator import discover_s3_region_name
|
||||
from server.auth.auth import AuthTypeFactory
|
||||
|
||||
DEFAULT_SERVER_PORT = 5005
|
||||
# anything bigger than this will generate a special message
|
||||
@@ -194,6 +195,7 @@ class AppConfig(object):
|
||||
server_config = self.server_config
|
||||
dataset_config = data_adaptor.dataset_config
|
||||
annotation = dataset_config.user_annotations
|
||||
auth = server_config.auth
|
||||
|
||||
# FIXME The current set of config is not consistently presented:
|
||||
# we have camalCase, hyphen-text, and underscore_text
|
||||
@@ -257,6 +259,18 @@ class AppConfig(object):
|
||||
"diffexp_cellcount_max": server_config.limits__diffexp_cellcount_max,
|
||||
}
|
||||
|
||||
if dataset_config.app__authentication_enable and auth.is_valid():
|
||||
config["authentication"] = {
|
||||
"is_authenticated": auth.is_authenticated(),
|
||||
"requires_client_login": auth.requires_client_login(),
|
||||
"username": auth.get_username(),
|
||||
}
|
||||
if auth.requires_client_login():
|
||||
config["authentication"].update({
|
||||
"login": auth.get_login_url(data_adaptor),
|
||||
"logout" : auth.get_logout_url(data_adaptor),
|
||||
})
|
||||
|
||||
return c
|
||||
|
||||
|
||||
@@ -366,6 +380,7 @@ class ServerConfig(BaseConfig):
|
||||
def __init__(self, app_config, default_config):
|
||||
dictval_cases = [
|
||||
("app", "csp_directives"),
|
||||
("authentication", "params"),
|
||||
("adaptor", "cxg_adaptor", "tiledb_ctx"),
|
||||
("multi_dataset", "dataroot"),
|
||||
]
|
||||
@@ -384,6 +399,9 @@ class ServerConfig(BaseConfig):
|
||||
self.app__server_timing_headers = dc["app"]["server_timing_headers"]
|
||||
self.app__csp_directives = dc["app"]["csp_directives"]
|
||||
|
||||
self.authentication__type = dc["authentication"]["type"]
|
||||
self.authentication__params = dc["authentication"]["params"]
|
||||
|
||||
self.multi_dataset__dataroot = dc["multi_dataset"]["dataroot"]
|
||||
self.multi_dataset__index = dc["multi_dataset"]["index"]
|
||||
self.multi_dataset__allowed_matrix_types = dc["multi_dataset"]["allowed_matrix_types"]
|
||||
@@ -414,8 +432,12 @@ class ServerConfig(BaseConfig):
|
||||
# The matrix data cache manager is created during the complete_config and stored here.
|
||||
self.matrix_data_cache_manager = None
|
||||
|
||||
# The authentication object (BCM -- better name)
|
||||
self.auth = None
|
||||
|
||||
def complete_config(self, context):
|
||||
self.handle_app(context)
|
||||
self.handle_authentication(context)
|
||||
self.handle_data_locator(context)
|
||||
self.handle_adaptor(context) # may depend on data_locator
|
||||
self.handle_single_dataset(context) # may depend on adaptor
|
||||
@@ -484,6 +506,14 @@ class ServerConfig(BaseConfig):
|
||||
elif not isinstance(v, str):
|
||||
raise ConfigurationError("CSP directive value must be a string or list of strings.")
|
||||
|
||||
def handle_authentication(self, context):
|
||||
self.check_attr("authentication__type", (type(None), str))
|
||||
self.check_attr("authentication__params", (type(None), dict))
|
||||
self.auth = AuthTypeFactory.create(self.authentication__type)
|
||||
if self.auth is None:
|
||||
raise ConfigurationError(f"Unknown authentication type: {self.authentication__type}")
|
||||
self.auth.set_params(self.authentication__params)
|
||||
|
||||
def handle_data_locator(self, context):
|
||||
self.check_attr("data_locator__s3__region_name", (type(None), bool, str))
|
||||
if self.data_locator__s3__region_name is True:
|
||||
@@ -660,6 +690,7 @@ class DatasetConfig(BaseConfig):
|
||||
self.app__inline_scripts = dc["app"]["inline_scripts"]
|
||||
self.app__about_legal_tos = dc["app"]["about_legal_tos"]
|
||||
self.app__about_legal_privacy = dc["app"]["about_legal_privacy"]
|
||||
self.app__authentication_enable = dc["app"]["authentication_enable"]
|
||||
|
||||
self.presentation__max_categories = dc["presentation"]["max_categories"]
|
||||
self.presentation__custom_colors = dc["presentation"]["custom_colors"]
|
||||
@@ -696,6 +727,7 @@ class DatasetConfig(BaseConfig):
|
||||
self.check_attr("app__inline_scripts", list)
|
||||
self.check_attr("app__about_legal_tos", (type(None), str))
|
||||
self.check_attr("app__about_legal_privacy", (type(None), str))
|
||||
self.check_attr("app__authentication_enable", bool)
|
||||
|
||||
# scripts can be string (filename) or dict (attributes). Convert string to dict.
|
||||
scripts = []
|
||||
@@ -721,6 +753,13 @@ class DatasetConfig(BaseConfig):
|
||||
self.check_attr("user_annotations__ontology__obo_location", (type(None), str))
|
||||
|
||||
if self.user_annotations__enable:
|
||||
server_config = self.app_config.server_config
|
||||
if not self.app__authentication_enable:
|
||||
raise ConfigurationError("user annotations requires authentication to be enabled")
|
||||
if not server_config.auth.is_valid():
|
||||
auth_type = server_config.authentication__type
|
||||
raise ConfigurationError(f"authentication method {auth_type} is not compatible with user annotations")
|
||||
|
||||
# TODO, replace this with a factory pattern once we have more than one way
|
||||
# to do annotations. currently only local_file_csv
|
||||
if self.user_annotations__type != "local_file_csv":
|
||||
|
||||
@@ -14,6 +14,15 @@ server:
|
||||
server_timing_headers: false
|
||||
csp_directives: null
|
||||
|
||||
authentication:
|
||||
# The authentication types may be "none" or "session"
|
||||
# none: No authentication support, features like user_annotations must not be enabled.
|
||||
# session: A session based userid is automatically generated.
|
||||
type: session
|
||||
|
||||
# a dictionary of parameters that may be required for an authentication type
|
||||
params: null
|
||||
|
||||
multi_dataset:
|
||||
# If dataroot is set, then cellxgene may serve multiple datasets. This parameter is not
|
||||
# compatible with single_dataset/datapath.
|
||||
@@ -132,6 +141,9 @@ dataset:
|
||||
about_legal_tos: null
|
||||
about_legal_privacy: null
|
||||
|
||||
# allow authentication support
|
||||
authentication_enable: true
|
||||
|
||||
presentation:
|
||||
max_categories: 1000
|
||||
custom_colors: true
|
||||
|
||||
Reference in New Issue
Block a user