Skip to content

Commit

Permalink
Add ability to use basic auth with programmatic server (#4926)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr authored May 24, 2023
1 parent 94690bb commit 80da5b4
Show file tree
Hide file tree
Showing 6 changed files with 35 additions and 9 deletions.
4 changes: 4 additions & 0 deletions doc/how_to/authentication/basic.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ panel serve app.py --basic-auth credentials.json --cookie-secret my_super_safe_c

The basic auth provider will now check the provided credentials against the credentials declared in this file.

:::note
When serving an application dynamically using `pn.serve` you can also provide a dictionary of usernames and passwords via the `basic_auth` keyword argument.
:::

## Custom templates

```{admonition} Prerequisites
Expand Down
17 changes: 13 additions & 4 deletions panel/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -834,16 +834,24 @@ def get(self):
errormessage = self.get_argument("error")
except Exception:
errormessage = ""
self.write(self._basic_login_template.render(errormessage=errormessage, PANEL_CDN=CDN_DIST))
html = self._basic_login_template.render(
errormessage=errormessage, PANEL_CDN=CDN_DIST
)
self.write(html)

def _validate(self, username, password):
if os.path.isfile(config.basic_auth):
with open(config.basic_auth, encoding='utf-8') as auth_file:
if 'basic_auth' in state._server_config.get(self.application, {}):
auth_info = state._server_config[self.application]['basic_auth']
else:
auth_info = config.basic_auth
if isinstance(auth_info, str) and os.path.isfile(auth_info):
with open(auth_info, encoding='utf-8') as auth_file:
auth_info = json.loads(auth_file.read())
if isinstance(auth_info, dict):
if username not in auth_info:
return False
return password == auth_info[username]
elif password == config.basic_auth:
elif password == auth_info:
return True
return False

Expand Down Expand Up @@ -871,6 +879,7 @@ def set_current_user(self, user):


class BasicProvider(OAuthProvider):

def __init__(self, basic_login_template=None):
if basic_login_template is None:
self._basic_login_template = BASIC_LOGIN_TEMPLATE
Expand Down
4 changes: 3 additions & 1 deletion panel/command/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,9 @@ def customize_kwargs(self, args, server_kwargs):
)
else:
basic_login_template = None
kwargs['auth_provider'] = BasicProvider(basic_login_template=basic_login_template)
kwargs['auth_provider'] = BasicProvider(
basic_login_template=basic_login_template
)

if args.cookie_secret and config.cookie_secret:
raise ValueError(
Expand Down
7 changes: 4 additions & 3 deletions panel/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,10 @@ class _config(_base_config):
Whenever an event arrives from the frontend it will be
dispatched to the thread pool to be processed.""")

_basic_auth = param.ObjectSelector(
default=None, allow_None=True, objects=[], doc="""
Use Basic authentication.""")
_basic_auth = param.ClassSelector(default=None, class_=(dict, str), allow_None=True, doc="""
Password, dictionary with a mapping from username to password
or filepath containing JSON to use with the basic auth
provider.""")

_oauth_provider = param.ObjectSelector(
default=None, allow_None=True, objects=[], doc="""
Expand Down
11 changes: 10 additions & 1 deletion panel/io/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,7 @@ def get_server(
location: bool | Location = True,
admin: bool = False,
static_dirs: Mapping[str, str] = {},
basic_auth: str = None,
oauth_provider: Optional[str] = None,
oauth_key: Optional[str] = None,
oauth_secret: Optional[str] = None,
Expand Down Expand Up @@ -975,6 +976,8 @@ def get_server(
static_dirs: dict (optional, default={})
A dictionary of routes and local paths to serve as static file
directories on those routes.
basic_auth: str (optional, default=None)
Password or filepath to use with basic auth provider.
oauth_provider: str
One of the available OAuth providers
oauth_key: str (optional, default=None)
Expand Down Expand Up @@ -1093,7 +1096,12 @@ def get_server(

# Configure OAuth
from ..config import config
if oauth_provider:
server_config = {}
if basic_auth:
from ..auth import BasicProvider
server_config['basic_auth'] = basic_auth
opts['auth_provider'] = BasicProvider()
elif oauth_provider:
from ..auth import OAuthProvider
config.oauth_provider = oauth_provider # type: ignore
opts['auth_provider'] = OAuthProvider()
Expand All @@ -1116,6 +1124,7 @@ def get_server(
print(f"Launching server at {url}")

state._servers[server_id] = (server, panel, [])
state._server_config[server._tornado] = server_config

if show:
def show_callback():
Expand Down
1 change: 1 addition & 0 deletions panel/io/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ class _state(param.Parameterized):
# An index of all currently active servers
_servers: ClassVar[Dict[str, Tuple[Server, Viewable | BaseTemplate, List[Document]]]] = {}
_threads: ClassVar[Dict[str, StoppableThread]] = {}
_server_config: ClassVar[WeakKeyDictionary[Any, Dict[str, Any]]] = WeakKeyDictionary()

# Jupyter display handles
_handles: ClassVar[Dict[str, [DisplayHandle, List[str]]]] = {}
Expand Down

0 comments on commit 80da5b4

Please sign in to comment.