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