diff --git a/.circleci/config.yml b/.circleci/config.yml index 9aafe74..187590f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,7 @@ version: 2.1 orbs: - browser-tools: circleci/browser-tools@1.4.1 + browser-tools: circleci/browser-tools@1.4.3 jobs: python-36: &test-template diff --git a/CHANGELOG.md b/CHANGELOG.md index f76d48c..a052bbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Changed +- Uses flask `before_request` to protect all endpoints rather than protecting routes present at instantiation time + ## [2.0.0] - 2023-03-10 ### Removed Removed obsolete `PlotlyAuth`. `dash-auth` is now just responsible for `BasicAuth`. diff --git a/README.md b/README.md index 33a3b89..619868a 100644 --- a/README.md +++ b/README.md @@ -23,3 +23,21 @@ python -k ba001 ``` Note that Python 3.6 or greater is required. + +## Usage + +### Basic Authentication + +To add basic authentication, add the following to your Dash app: + +```python +from dash import Dash +from dash_auth import BasicAuth + +app = Dash(__name__) +USER_PWD = { + "username": "password", + "user2": "useSomethingMoreSecurePlease", +} +BasicAuth(app, USER_PWD) +``` diff --git a/dash_auth/__init__.py b/dash_auth/__init__.py index 4a793cc..6ca5cec 100644 --- a/dash_auth/__init__.py +++ b/dash_auth/__init__.py @@ -1,2 +1,5 @@ -from .basic_auth import BasicAuth # noqa: F401 -from .version import __version__ # noqa: F401 +from .basic_auth import BasicAuth +from .version import __version__ + + +__all__ = ["BasicAuth", "__version__"] diff --git a/dash_auth/auth.py b/dash_auth/auth.py index 29c209d..f5fdcb3 100644 --- a/dash_auth/auth.py +++ b/dash_auth/auth.py @@ -1,29 +1,43 @@ from __future__ import absolute_import from abc import ABC, abstractmethod +from dash import Dash + class Auth(ABC): - def __init__(self, app, authorization_hook=None, _overwrite_index=True): + def __init__(self, app: Dash, **obsolete): + """Auth base class for authentication in Dash. + + :param app: Dash app + """ + + # Deprecated arguments + if obsolete: + raise TypeError( + f"Auth got unexpected keyword arguments: {list(obsolete)}" + ) + self.app = app - self._index_view_name = app.config['routes_pathname_prefix'] - if _overwrite_index: - self._overwrite_index() - self._protect_views() - self._index_view_name = app.config['routes_pathname_prefix'] - self._auth_hooks = [authorization_hook] if authorization_hook else [] - - def _overwrite_index(self): - original_index = self.app.server.view_functions[self._index_view_name] - - self.app.server.view_functions[self._index_view_name] = \ - self.index_auth_wrapper(original_index) - - def _protect_views(self): - # TODO - allow users to white list in case they add their own views - for view_name, view_method in self.app.server.view_functions.items(): - if view_name != self._index_view_name: - self.app.server.view_functions[view_name] = \ - self.auth_wrapper(view_method) + self._protect() + + def _protect(self): + """Add a before_request authentication check on all routes. + + The authentication check will pass if the request + is authorised by `Auth.is_authorised` + """ + + server = self.app.server + + @server.before_request + def before_request_auth(): + + # Check whether the request is authorised + if self.is_authorized(): + return None + + # Otherwise, ask the user to log in + return self.login_request() def is_authorized_hook(self, func): self._auth_hooks.append(func) diff --git a/dash_auth/basic_auth.py b/dash_auth/basic_auth.py index e8d56f4..b175b7b 100644 --- a/dash_auth/basic_auth.py +++ b/dash_auth/basic_auth.py @@ -1,10 +1,23 @@ -from .auth import Auth import base64 +from typing import Union import flask +from dash import Dash + +from .auth import Auth class BasicAuth(Auth): - def __init__(self, app, username_password_list): + def __init__( + self, + app: Dash, + username_password_list: Union[list, dict], + ): + """Add basic authentication to Dash. + + :param app: Dash app + :param username_password_list: username:password list, either as a + list of tuples or a dict + """ Auth.__init__(self, app) self._users = ( username_password_list diff --git a/dev-requirements.txt b/dev-requirements.txt index 5e4a42a..d09bb92 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,3 +1,4 @@ dash[testing]>=2 requests[security] flake8 +flask diff --git a/setup.py b/setup.py index febcf9b..98f26e0 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,8 @@ description='Dash Authorization Package.', long_description=long_description, install_requires=[ - 'dash>=1.1.1' + 'dash>=1.1.1', + "flask", ], python_requires=">=3.6", include_package_data=True, diff --git a/tests/test_basic_auth_integration.py b/tests/test_basic_auth_integration.py index 8f7e791..3dc049c 100644 --- a/tests/test_basic_auth_integration.py +++ b/tests/test_basic_auth_integration.py @@ -33,7 +33,7 @@ def update_output(new_value): def test_failed_views(url): assert requests.get(url).status_code == 401 - assert requests.get(url.strip("/") + "/_dash-layout").status_code == 403 + assert requests.get(url.strip("/") + "/_dash-layout").status_code == 401 test_failed_views(base_url)