mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-30 05:28:12 +08:00
add oauth authentication (#1681)
* add oauth authentication Add support for OAuth2. Change the interface to AuthTypeBase - better handling of config parameters - add a complete_setup function for additional setup steps Added a function wrapper to enforce authentication for the routes that require authenticaiton. * change fsspec requirement fsspec 0.8.0 breaks our tests it imports a module that is does not require.
This commit is contained in:
@@ -10,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, current_app
|
||||
from flask import session, current_app, has_request_context
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
|
||||
@@ -46,8 +46,8 @@ class Annotations(metaclass=ABCMeta):
|
||||
raise OntologyLoadFailure("Error loading OBO file") from e
|
||||
|
||||
def get_schema(self, data_adaptor):
|
||||
labels = self.read_labels(data_adaptor)
|
||||
schema = []
|
||||
labels = self.read_labels(data_adaptor)
|
||||
if labels is not None and not labels.empty:
|
||||
for col in labels.columns:
|
||||
col_schema = dict(name=col, writable=True)
|
||||
@@ -113,6 +113,10 @@ class AnnotationsLocalFile(Annotations):
|
||||
return session.get(self.CXG_ANNO_COLLECTION)
|
||||
|
||||
def read_labels(self, data_adaptor):
|
||||
if has_request_context():
|
||||
if not current_app.auth.is_user_authenticated():
|
||||
return pd.DataFrame()
|
||||
|
||||
fname = self._get_filename(data_adaptor)
|
||||
with self.label_lock:
|
||||
if fname is not None and os.path.exists(fname) and os.path.getsize(fname) > 0:
|
||||
@@ -162,7 +166,7 @@ class AnnotationsLocalFile(Annotations):
|
||||
Return a short hash that weakly identifies the user and dataset.
|
||||
Used to create safe annotations output file names.
|
||||
"""
|
||||
uid = current_app.auth.get_userid()
|
||||
uid = current_app.auth.get_user_id()
|
||||
id = (uid + data_adaptor.get_location()).encode()
|
||||
idhash = base64.b32encode(blake2b(id, digest_size=5).digest()).decode("utf-8")
|
||||
return idhash
|
||||
@@ -249,7 +253,7 @@ class AnnotationsLocalFile(Annotations):
|
||||
|
||||
elif session is not None:
|
||||
collection = self.get_collection()
|
||||
if current_app.auth.is_authenticated():
|
||||
if current_app.auth.is_user_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
|
||||
|
||||
+23
-10
@@ -272,11 +272,11 @@ class AppConfig(object):
|
||||
"diffexp_cellcount_max": server_config.limits__diffexp_cellcount_max,
|
||||
}
|
||||
|
||||
if dataset_config.app__authentication_enable and auth.is_valid():
|
||||
if dataset_config.app__authentication_enable and auth.is_valid_authentication_type():
|
||||
config["authentication"] = {
|
||||
"is_authenticated": auth.is_authenticated(),
|
||||
"is_authenticated": auth.is_user_authenticated(),
|
||||
"requires_client_login": auth.requires_client_login(),
|
||||
"username": auth.get_username(),
|
||||
"username": auth.get_user_name(),
|
||||
}
|
||||
if auth.requires_client_login():
|
||||
config["authentication"].update({
|
||||
@@ -393,7 +393,6 @@ 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"),
|
||||
]
|
||||
@@ -413,7 +412,11 @@ class ServerConfig(BaseConfig):
|
||||
self.app__csp_directives = dc["app"]["csp_directives"]
|
||||
|
||||
self.authentication__type = dc["authentication"]["type"]
|
||||
self.authentication__params = dc["authentication"]["params"]
|
||||
self.authentication__params_oauth__api_base_url = dc["authentication"]["params_oauth"]["api_base_url"]
|
||||
self.authentication__params_oauth__client_id = dc["authentication"]["params_oauth"]["client_id"]
|
||||
self.authentication__params_oauth__client_secret = dc["authentication"]["params_oauth"]["client_secret"]
|
||||
self.authentication__params_oauth__callback_base_url = \
|
||||
dc["authentication"]["params_oauth"]["callback_base_url"]
|
||||
|
||||
self.multi_dataset__dataroot = dc["multi_dataset"]["dataroot"]
|
||||
self.multi_dataset__index = dc["multi_dataset"]["index"]
|
||||
@@ -445,7 +448,7 @@ 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)
|
||||
# The authentication object
|
||||
self.auth = None
|
||||
|
||||
def complete_config(self, context):
|
||||
@@ -521,11 +524,21 @@ class ServerConfig(BaseConfig):
|
||||
|
||||
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)
|
||||
|
||||
# oauth
|
||||
ptypes = str if self.authentication__type == "oauth" else (type(None), str)
|
||||
self.check_attr("authentication__params_oauth__api_base_url", ptypes)
|
||||
self.check_attr("authentication__params_oauth__client_id", ptypes)
|
||||
self.check_attr("authentication__params_oauth__client_secret", ptypes)
|
||||
self.check_attr("authentication__params_oauth__callback_base_url", (type(None), str))
|
||||
# secret key: first, from CXG_OAUTH_CLIENT_SECRET environment variable
|
||||
# second, from config file
|
||||
self.authentication__params__oauth__client_secret = os.environ.get(
|
||||
"CXG_OAUTH_CLIENT_SECRET", self.authentication__params_oauth__client_secret)
|
||||
|
||||
self.auth = AuthTypeFactory.create(self.authentication__type, self)
|
||||
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))
|
||||
@@ -769,7 +782,7 @@ class DatasetConfig(BaseConfig):
|
||||
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():
|
||||
if not server_config.auth.is_valid_authentication_type():
|
||||
auth_type = server_config.authentication__type
|
||||
raise ConfigurationError(f"authentication method {auth_type} is not compatible with user annotations")
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ server:
|
||||
app:
|
||||
verbose: false
|
||||
debug: false
|
||||
host: "127.0.0.1"
|
||||
host: localhost
|
||||
port : null
|
||||
open_browser: false
|
||||
force_https: false
|
||||
@@ -15,13 +15,24 @@ server:
|
||||
csp_directives: null
|
||||
|
||||
authentication:
|
||||
# The authentication types may be "none" or "session"
|
||||
# The authentication types may be "none", "session", "oauth"
|
||||
# none: No authentication support, features like user_annotations must not be enabled.
|
||||
# session: A session based userid is automatically generated.
|
||||
# session: A session based userid is automatically generated. (no params needed)
|
||||
# oauth: oauth2 is used for authentication; parameters are defined in params_oauth.
|
||||
type: session
|
||||
|
||||
# a dictionary of parameters that may be required for an authentication type
|
||||
params: null
|
||||
params_oauth:
|
||||
# url to the auth server
|
||||
api_base_url: null
|
||||
# client_id of this app
|
||||
client_id: null
|
||||
# the client_secret known to the auth server and this app
|
||||
client_secret: null
|
||||
# cellxgene server location;
|
||||
# the browser will be redirected to locations relative to this location during login and logout.
|
||||
# A value of None, indicates the client and server are on the localhost. http://localhost:<port> will be used.
|
||||
callback_base_url: null
|
||||
|
||||
|
||||
multi_dataset:
|
||||
# If dataroot is set, then cellxgene may serve multiple datasets. This parameter is not
|
||||
|
||||
@@ -41,6 +41,10 @@ define_request_exception(
|
||||
)
|
||||
define_request_exception("ExceedsLimitError", "Raised when an HTTP request exceeds a limit/quota")
|
||||
define_request_exception("ColorFormatException", "Raised when color helper functions encounter an unknown color format")
|
||||
define_request_exception(
|
||||
"AuthenticationError",
|
||||
"Raised when there is an authentication error",
|
||||
default_status_code=HTTPStatus.UNAUTHORIZED)
|
||||
|
||||
define_exception("OntologyLoadFailure", "Raised when reading the ontology file fails")
|
||||
define_exception("ConfigurationError", "Raised when checking configuration errors")
|
||||
|
||||
Reference in New Issue
Block a user