From b6d6b347472fd5af3d867cf20e7db5d3f9151589 Mon Sep 17 00:00:00 2001 From: Andy Maloney <60523020+ndmlny-qs@users.noreply.github.com> Date: Mon, 21 Aug 2023 07:57:25 -0500 Subject: [PATCH] Authorization checks in server.py (#5386) --- doc/how_to/authentication/user_info.md | 26 +++++++++++++++++ panel/config.py | 6 +++- panel/io/server.py | 39 ++++++++++++++++++++------ 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/doc/how_to/authentication/user_info.md b/doc/how_to/authentication/user_info.md index 70cc5971f5..90dd711c56 100644 --- a/doc/how_to/authentication/user_info.md +++ b/doc/how_to/authentication/user_info.md @@ -38,3 +38,29 @@ The auth template must be a valid Jinja2 template and accepts a number of argume - `{{ error_type }}`: The type of error. - `{{ error }}`: A short description of the error. - `{{ error_msg }}`: A full description of the error. + +The `authorization_callback` may also contain a second parameter, which is set by the +requested application path. You can use this extra parameter to check if a user is +authenticated _and_ has access to the application at the given path. + +```python +from urllib import parse +import panel as pn + +authorized_user_paths = { + "user1": ["/app1", "/app2"], + "user2": ["/app1"], +} + +def authorize(user_info, request_path): + current_user = user_info['username'] + current_path = parse.urlparse(request_path).path + if current_user not in authorized_user_paths: + return False + current_user_paths = authorized_user_paths[current_user] + if current_path in current_user_paths: + return True + return False + +pn.config.authorize_callback = authorize +``` diff --git a/panel/config.py b/panel/config.py index f6b22e34b7..0e13c349c1 100644 --- a/panel/config.py +++ b/panel/config.py @@ -110,7 +110,11 @@ class _config(_base_config): is enabled. The callback is given the user information returned by the configured Auth provider and should return True or False depending on whether the user is authorized to access the - application.""") + application. The callback may also contain a second parameter, + which is the requested path the user is making. If the user + is authenticated and has explicit access to the path, then + the callback should return True otherwise it should return + False.""") auth_template = param.Path(default=None, doc=""" A jinja2 template rendered when the authorize_callback determines diff --git a/panel/io/server.py b/panel/io/server.py index e2747c9bbc..0eb2c999e6 100644 --- a/panel/io/server.py +++ b/panel/io/server.py @@ -8,6 +8,7 @@ import gc import html import importlib +import inspect import logging import os import pathlib @@ -503,7 +504,34 @@ async def get(self, *args, **kwargs): token = session.token logger.info(LOG_SESSION_CREATED, id(session.document)) with set_curdoc(session.document): - if config.authorize_callback and not config.authorize_callback(state.user_info): + resources = Resources.from_bokeh(self.application.resources()) + auth_cb = config.authorize_callback + if auth_cb: + auth_cb = config.authorize_callback + auth_params = inspect.signature(auth_cb).parameters + auth_args = (state.user_info,) + if len(auth_params) == 2: + auth_args += (self.request.path,) + else: + raise RuntimeError( + 'Authorization callback must accept either one or two arguments.' + ) + auth_error = f'{state.user} is not authorized to access this application.' + try: + authorized = auth_cb(*auth_args) + auth_error = None + except Exception: + auth_error = f'Authorization callback errored. Could not validate user {state.user}' + else: + authorized = True + + if authorized: + page = server_html_page_for_session( + session, resources=resources, title=session.document.title, + token=token, template=session.document.template, + template_variables=session.document.template_variables, + ) + else: if config.auth_template: with open(config.auth_template) as f: template = _env.from_string(f.read()) @@ -514,14 +542,7 @@ async def get(self, *args, **kwargs): title='Panel: Authorization Error', error_type='Authorization Error', error='User is not authorized.', - error_msg=f'{state.user} is not authorized to access this application.' - ) - else: - resources = Resources.from_bokeh(self.application.resources()) - page = server_html_page_for_session( - session, resources=resources, title=session.document.title, - token=token, template=session.document.template, - template_variables=session.document.template_variables, + error_msg=auth_error ) self.set_header("Content-Type", 'text/html') self.write(page)