1''' Provide a hook for supplying authorization mechanisms to a Bokeh server. 2 3''' 4 5#----------------------------------------------------------------------------- 6# Boilerplate 7#----------------------------------------------------------------------------- 8import logging # isort:skip 9log = logging.getLogger(__name__) 10 11#----------------------------------------------------------------------------- 12# Imports 13#----------------------------------------------------------------------------- 14 15# Standard library imports 16import importlib.util 17from os.path import isfile 18 19# External imports 20from tornado.web import RequestHandler 21 22# Bokeh imports 23from ..util.serialization import make_globally_unique_id 24 25#----------------------------------------------------------------------------- 26# Globals and constants 27#----------------------------------------------------------------------------- 28 29__all__ = ( 30 'AuthModule', 31 'AuthProvider', 32 'NullAuth' 33) 34 35#----------------------------------------------------------------------------- 36# General API 37#----------------------------------------------------------------------------- 38 39class AuthProvider: 40 ''' Abstract base class for implementing authorization hooks. 41 42 Subclasses must supply one of: ``get_user`` or ``get_user_async``. 43 44 Subclasses must also supply one of ``login_url`` or ``get_login_url``. 45 46 Optionally, if ``login_url`` provides a relative URL, then ``login_handler`` 47 may also be supplied. 48 49 The properties ``logout_url`` and ``get_logout_handler`` are analogous to 50 the corresponding login properties, and are optional. 51 52 ''' 53 54 def __init__(self): 55 self._validate() 56 57 @property 58 def endpoints(self): 59 ''' URL patterns for login/logout endpoints. 60 61 ''' 62 endpoints = [] 63 if self.login_handler: 64 endpoints.append((self.login_url, self.login_handler)) 65 if self.logout_handler: 66 endpoints.append((self.logout_url, self.logout_handler)) 67 return endpoints 68 69 @property 70 def get_login_url(self): 71 ''' A function that computes a URL to redirect unathenticated users 72 to for login. 73 74 This property may return None, if a ``login_url`` is supplied 75 instead. 76 77 If a function is returned, it should accept a ``RequestHandler`` 78 and return a login URL for unathenticated users. 79 80 ''' 81 pass 82 83 @property 84 def get_user(self): 85 ''' A function to get the current authenticated user. 86 87 This property may return None, if a ``get_user_async`` function is 88 supplied instead. 89 90 If a function is returned, it should accept a ``RequestHandler`` 91 and return the current authenticated user. 92 93 ''' 94 pass 95 96 @property 97 def get_user_async(self): 98 ''' An async function to get the current authenticated user. 99 100 This property may return None, if a ``get_user`` function is supplied 101 instead. 102 103 If a function is returned, it should accept a ``RequestHandler`` 104 and return the current authenticated user. 105 106 ''' 107 pass 108 109 @property 110 def login_handler(self): 111 ''' A request handler class for a login page. 112 113 This property may return None, if ``login_url`` is supplied 114 instead. 115 116 If a class is returned, it must be a subclass of RequestHandler, 117 which will used for the endpoint specified by ``logout_url`` 118 119 ''' 120 pass 121 122 @property 123 def login_url(self): 124 ''' A URL to redirect unauthenticated users to for login. 125 126 This proprty may return None, if a ``get_login_url`` function is 127 supplied instead. 128 129 ''' 130 pass 131 132 @property 133 def logout_handler(self): 134 ''' A request handler class for a logout page. 135 136 This property may return None. 137 138 If a class is returned, it must be a subclass of RequestHandler, 139 which will used for the endpoint specified by ``logout_url`` 140 141 ''' 142 pass 143 144 @property 145 def logout_url(self): 146 ''' A URL to redirect unathenticated users to for logout. 147 148 This proprty may return None. 149 150 ''' 151 pass 152 153 def _validate(self): 154 if self.get_user and self.get_user_async: 155 raise ValueError("Only one of get_user or get_user_async should be supplied") 156 157 if (self.get_user or self.get_user_async) and not (self.login_url or self.get_login_url): 158 raise ValueError("When user authentication is enabled, one of login_url or get_login_url must be supplied") 159 160 if self.login_url and self.get_login_url: 161 raise ValueError("At most one of login_url or get_login_url should be supplied") 162 if self.login_handler and self.get_login_url: 163 raise ValueError("LoginHandler cannot be used with a get_login_url() function") 164 if self.login_handler and not issubclass(self.login_handler, RequestHandler): 165 raise ValueError("LoginHandler must be a Tornado RequestHandler") 166 if self.login_url and not probably_relative_url(self.login_url): 167 raise ValueError("LoginHandler can only be used with a relative login_url") 168 169 if self.logout_handler and not issubclass(self.logout_handler, RequestHandler): 170 raise ValueError("LogoutHandler must be a Tornado RequestHandler") 171 if self.logout_url and not probably_relative_url(self.logout_url): 172 raise ValueError("LogoutHandler can only be used with a relative login_url") 173 174class AuthModule(AuthProvider): 175 ''' An AuthProvider configured from a Python module. 176 177 The following properties return the corresponding values from the module if 178 they exist, or None otherwise: 179 180 * ``get_login_url``, 181 * ``get_user`` 182 * ``get_user_async`` 183 * ``login_url`` 184 * ``logout_url`` 185 186 The ``login_handler`` property will return a ``LoginHandler`` class from the 187 module, or None otherwise. 188 189 The ``logout_handler`` property will return a ``LogoutHandler`` class from 190 the module, or None otherwise. 191 192 ''' 193 194 def __init__(self, module_path): 195 if not isfile(module_path): 196 raise ValueError("no file exists at module_path: %r" % module_path) 197 198 self._module = load_auth_module(module_path) 199 200 super().__init__() 201 202 @property 203 def get_user(self): 204 return getattr(self._module, 'get_user', None) 205 206 @property 207 def get_user_async(self): 208 return getattr(self._module, 'get_user_async', None) 209 210 @property 211 def login_url(self): 212 return getattr(self._module, 'login_url', None) 213 214 @property 215 def get_login_url(self): 216 return getattr(self._module, 'get_login_url', None) 217 218 @property 219 def login_handler(self): 220 return getattr(self._module, 'LoginHandler', None) 221 222 @property 223 def logout_url(self): 224 return getattr(self._module, 'logout_url', None) 225 226 @property 227 def logout_handler(self): 228 return getattr(self._module, 'LogoutHandler', None) 229 230class NullAuth(AuthProvider): 231 ''' A default no-auth AuthProvider. 232 233 All of the properties of this provider return None. 234 235 ''' 236 @property 237 def get_user(self): 238 return None 239 240 @property 241 def get_user_async(self): 242 return None 243 244 @property 245 def login_url(self): 246 return None 247 248 @property 249 def get_login_url(self): 250 return None 251 252 @property 253 def login_handler(self): 254 return None 255 256 @property 257 def logout_url(self): 258 return None 259 260 @property 261 def logout_handler(self): 262 return None 263 264#----------------------------------------------------------------------------- 265# Dev API 266#----------------------------------------------------------------------------- 267 268def load_auth_module(module_path): 269 ''' Load a Python source file at a given path as a module. 270 271 Arguments: 272 module_path (str): path to a Python source file 273 274 Returns 275 module 276 277 ''' 278 module_name ="bokeh.auth_" + make_globally_unique_id().replace('-', '') 279 spec = importlib.util.spec_from_file_location(module_name, module_path) 280 module = importlib.util.module_from_spec(spec) 281 spec.loader.exec_module(module) 282 return module 283 284def probably_relative_url(url): 285 ''' Return True if a URL is not one of the common absolute URL formats. 286 287 Arguments: 288 url (str): a URL string 289 290 Returns 291 bool 292 293 ''' 294 return not url.startswith(("http://", "https://", "//")) 295 296#----------------------------------------------------------------------------- 297# Private API 298#----------------------------------------------------------------------------- 299 300#----------------------------------------------------------------------------- 301# Code 302#----------------------------------------------------------------------------- 303