Source code for bokeh.server.auth_provider

''' Provide a hook for supplying authorization mechanisms to a Bokeh server.

'''

#-----------------------------------------------------------------------------
# Boilerplate
#-----------------------------------------------------------------------------
from __future__ import annotations

import logging # isort:skip
log = logging.getLogger(__name__)

#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------

# Standard library imports
import importlib.util
from os.path import isfile
from types import ModuleType
from typing import (
    TYPE_CHECKING,
    Awaitable,
    Callable,
    List,
    NewType,
    Tuple,
    Type,
)

# External imports
from tornado.httputil import HTTPServerRequest
from tornado.web import RequestHandler

# Bokeh imports
from ..util.serialization import make_globally_unique_id

if TYPE_CHECKING:
    from ..core.types import PathLike

#-----------------------------------------------------------------------------
# Globals and constants
#-----------------------------------------------------------------------------

__all__ = (
    'AuthModule',
    'AuthProvider',
    'NullAuth'
)

#-----------------------------------------------------------------------------
# General API
#-----------------------------------------------------------------------------

User = NewType("User", object)

[docs]class AuthProvider: ''' Abstract base class for implementing authorization hooks. Subclasses must supply one of: ``get_user`` or ``get_user_async``. Subclasses must also supply one of ``login_url`` or ``get_login_url``. Optionally, if ``login_url`` provides a relative URL, then ``login_handler`` may also be supplied. The properties ``logout_url`` and ``get_logout_handler`` are analogous to the corresponding login properties, and are optional. .. autoclasstoc:: ''' def __init__(self) -> None: self._validate() @property def endpoints(self) -> List[Tuple[str, Type[RequestHandler]]]: ''' URL patterns for login/logout endpoints. ''' endpoints: List[Tuple[str, Type[RequestHandler]]] = [] if self.login_handler: assert self.login_url is not None endpoints.append((self.login_url, self.login_handler)) if self.logout_handler: assert self.logout_url is not None endpoints.append((self.logout_url, self.logout_handler)) return endpoints @property def get_login_url(self) -> Callable[[HTTPServerRequest], str] | None: ''' A function that computes a URL to redirect unathenticated users to for login. This property may return None, if a ``login_url`` is supplied instead. If a function is returned, it should accept a ``RequestHandler`` and return a login URL for unathenticated users. ''' pass @property def get_user(self) -> Callable[[HTTPServerRequest], User] | None: ''' A function to get the current authenticated user. This property may return None, if a ``get_user_async`` function is supplied instead. If a function is returned, it should accept a ``RequestHandler`` and return the current authenticated user. ''' pass @property def get_user_async(self) -> Callable[[HTTPServerRequest], Awaitable[User]] | None: ''' An async function to get the current authenticated user. This property may return None, if a ``get_user`` function is supplied instead. If a function is returned, it should accept a ``RequestHandler`` and return the current authenticated user. ''' pass @property def login_handler(self) -> Type[RequestHandler] | None: ''' A request handler class for a login page. This property may return None, if ``login_url`` is supplied instead. If a class is returned, it must be a subclass of RequestHandler, which will used for the endpoint specified by ``logout_url`` ''' pass @property def login_url(self) -> str | None: ''' A URL to redirect unauthenticated users to for login. This proprty may return None, if a ``get_login_url`` function is supplied instead. ''' pass @property def logout_handler(self) -> Type[RequestHandler] | None: ''' A request handler class for a logout page. This property may return None. If a class is returned, it must be a subclass of RequestHandler, which will used for the endpoint specified by ``logout_url`` ''' pass @property def logout_url(self) -> str | None: ''' A URL to redirect authenticated users to for logout. This proprty may return None. ''' pass def _validate(self) -> None: if self.get_user and self.get_user_async: raise ValueError("Only one of get_user or get_user_async should be supplied") if (self.get_user or self.get_user_async) and not (self.login_url or self.get_login_url): raise ValueError("When user authentication is enabled, one of login_url or get_login_url must be supplied") if self.login_url and self.get_login_url: raise ValueError("At most one of login_url or get_login_url should be supplied") if self.login_handler and self.get_login_url: raise ValueError("LoginHandler cannot be used with a get_login_url() function") if self.login_handler and not issubclass(self.login_handler, RequestHandler): raise ValueError("LoginHandler must be a Tornado RequestHandler") if self.login_url and not probably_relative_url(self.login_url): raise ValueError("LoginHandler can only be used with a relative login_url") if self.logout_handler and not issubclass(self.logout_handler, RequestHandler): raise ValueError("LogoutHandler must be a Tornado RequestHandler") if self.logout_url and not probably_relative_url(self.logout_url): raise ValueError("LogoutHandler can only be used with a relative logout_url")
[docs]class AuthModule(AuthProvider): ''' An AuthProvider configured from a Python module. The following properties return the corresponding values from the module if they exist, or None otherwise: * ``get_login_url``, * ``get_user`` * ``get_user_async`` * ``login_url`` * ``logout_url`` The ``login_handler`` property will return a ``LoginHandler`` class from the module, or None otherwise. The ``logout_handler`` property will return a ``LogoutHandler`` class from the module, or None otherwise. .. autoclasstoc:: ''' def __init__(self, module_path: PathLike) -> None: if not isfile(module_path): raise ValueError(f"no file exists at module_path: {module_path!r}") self._module = load_auth_module(module_path) super().__init__() @property def get_user(self): return getattr(self._module, 'get_user', None) @property def get_user_async(self): return getattr(self._module, 'get_user_async', None) @property def login_url(self): return getattr(self._module, 'login_url', None) @property def get_login_url(self): return getattr(self._module, 'get_login_url', None) @property def login_handler(self): return getattr(self._module, 'LoginHandler', None) @property def logout_url(self): return getattr(self._module, 'logout_url', None) @property def logout_handler(self): return getattr(self._module, 'LogoutHandler', None)
[docs]class NullAuth(AuthProvider): ''' A default no-auth AuthProvider. All of the properties of this provider return None. .. autoclasstoc:: ''' @property def get_user(self): return None @property def get_user_async(self): return None @property def login_url(self): return None @property def get_login_url(self): return None @property def login_handler(self): return None @property def logout_url(self): return None @property def logout_handler(self): return None
#----------------------------------------------------------------------------- # Dev API #----------------------------------------------------------------------------- def load_auth_module(module_path: PathLike) -> ModuleType: ''' Load a Python source file at a given path as a module. Arguments: module_path (str): path to a Python source file Returns module ''' module_name = "bokeh.auth_" + make_globally_unique_id().replace('-', '') spec = importlib.util.spec_from_file_location(module_name, module_path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def probably_relative_url(url: str) -> bool: ''' Return True if a URL is not one of the common absolute URL formats. Arguments: url (str): a URL string Returns bool ''' return not url.startswith(("http://", "https://", "//")) #----------------------------------------------------------------------------- # Private API #----------------------------------------------------------------------------- #----------------------------------------------------------------------------- # Code #-----------------------------------------------------------------------------