dash-auth-plus 0.0.1a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dash_auth_plus/__init__.py +32 -0
- dash_auth_plus/auth.py +134 -0
- dash_auth_plus/basic_auth.py +124 -0
- dash_auth_plus/group_protection.py +325 -0
- dash_auth_plus/oidc_auth.py +360 -0
- dash_auth_plus/public_routes.py +108 -0
- dash_auth_plus/version.py +2 -0
- dash_auth_plus-0.0.1a1.dist-info/LICENSE +21 -0
- dash_auth_plus-0.0.1a1.dist-info/METADATA +531 -0
- dash_auth_plus-0.0.1a1.dist-info/RECORD +12 -0
- dash_auth_plus-0.0.1a1.dist-info/WHEEL +5 -0
- dash_auth_plus-0.0.1a1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from .public_routes import add_public_routes, public_callback
|
|
2
|
+
from .basic_auth import BasicAuth
|
|
3
|
+
from .group_protection import (
|
|
4
|
+
list_groups,
|
|
5
|
+
check_groups,
|
|
6
|
+
protected,
|
|
7
|
+
protected_callback,
|
|
8
|
+
protect_layouts,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
# oidc auth requires authlib, install with `pip install dash-auth-plus[oidc]`
|
|
12
|
+
try:
|
|
13
|
+
from .oidc_auth import OIDCAuth, get_oauth
|
|
14
|
+
except ModuleNotFoundError:
|
|
15
|
+
pass
|
|
16
|
+
from .version import __version__, __plotly_dash_auth_version__
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"add_public_routes",
|
|
21
|
+
"check_groups",
|
|
22
|
+
"list_groups",
|
|
23
|
+
"get_oauth",
|
|
24
|
+
"protect_layouts",
|
|
25
|
+
"protected",
|
|
26
|
+
"protected_callback",
|
|
27
|
+
"public_callback",
|
|
28
|
+
"BasicAuth",
|
|
29
|
+
"OIDCAuth",
|
|
30
|
+
"__version__",
|
|
31
|
+
"__plotly_dash_auth_version__",
|
|
32
|
+
]
|
dash_auth_plus/auth.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import absolute_import
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import Optional, Union
|
|
4
|
+
|
|
5
|
+
from dash import Dash
|
|
6
|
+
from flask import request
|
|
7
|
+
|
|
8
|
+
from .public_routes import (
|
|
9
|
+
add_public_routes,
|
|
10
|
+
get_public_callbacks,
|
|
11
|
+
get_public_routes,
|
|
12
|
+
)
|
|
13
|
+
from .group_protection import protect_layouts
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Auth(ABC):
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
app: Dash,
|
|
20
|
+
public_routes: Optional[list] = None,
|
|
21
|
+
auth_protect_layouts: Optional[Union[dict, bool]] = False,
|
|
22
|
+
auth_protect_layouts_kwargs: Optional[dict] = None,
|
|
23
|
+
page_container: Optional[str] = None,
|
|
24
|
+
**obsolete,
|
|
25
|
+
):
|
|
26
|
+
"""Auth base class for authentication in Dash.
|
|
27
|
+
|
|
28
|
+
:param app: Dash app
|
|
29
|
+
:param public_routes: list of public routes, routes should follow the
|
|
30
|
+
Flask route syntax
|
|
31
|
+
:param auth_protect_layouts: bool, defaults to False.
|
|
32
|
+
If true, runs protect_layout()
|
|
33
|
+
:param auth_protect_layouts_kwargs: dict, if provided is passed to the
|
|
34
|
+
protect_layout as kwargs
|
|
35
|
+
:param page_container: string, id of the page container in the app.
|
|
36
|
+
If not provided, this will set the page_container_test to True,
|
|
37
|
+
meaning all pathname callbacks will be judged.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# Deprecated arguments
|
|
41
|
+
if obsolete:
|
|
42
|
+
raise TypeError(f"Auth got unexpected keyword arguments: {list(obsolete)}")
|
|
43
|
+
|
|
44
|
+
self.app = app
|
|
45
|
+
self._protect()
|
|
46
|
+
self.auth_protect_layouts = auth_protect_layouts
|
|
47
|
+
self.page_container = page_container
|
|
48
|
+
if public_routes is not None:
|
|
49
|
+
add_public_routes(app, public_routes)
|
|
50
|
+
if self.auth_protect_layouts:
|
|
51
|
+
protect_layouts(
|
|
52
|
+
public_routes=get_public_routes(self.app),
|
|
53
|
+
**(auth_protect_layouts_kwargs or {}),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def _protect(self):
|
|
57
|
+
"""Add a before_request authentication check on all routes.
|
|
58
|
+
|
|
59
|
+
The authentication check will pass if either
|
|
60
|
+
* The endpoint is marked as public via `add_public_routes`
|
|
61
|
+
* The request is authorised by `Auth.is_authorised`
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
server = self.app.server
|
|
65
|
+
|
|
66
|
+
@server.before_request
|
|
67
|
+
def before_request_auth():
|
|
68
|
+
|
|
69
|
+
public_routes = get_public_routes(self.app)
|
|
70
|
+
public_callbacks = get_public_callbacks(self.app)
|
|
71
|
+
|
|
72
|
+
# Handle Dash's callback route:
|
|
73
|
+
# * Check whether the callback is marked as public
|
|
74
|
+
# * Check whether the callback is performed on route change in
|
|
75
|
+
# which case the path should be checked against the public routes
|
|
76
|
+
if request.path == "/_dash-update-component":
|
|
77
|
+
body = request.get_json()
|
|
78
|
+
|
|
79
|
+
# Check whether the callback is marked as public
|
|
80
|
+
if body["output"] in public_callbacks:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
pathname = next(
|
|
84
|
+
(
|
|
85
|
+
inp.get("value")
|
|
86
|
+
for inp in body["inputs"]
|
|
87
|
+
if isinstance(inp, dict) and inp.get("property") == "pathname"
|
|
88
|
+
),
|
|
89
|
+
None,
|
|
90
|
+
)
|
|
91
|
+
if self.page_container:
|
|
92
|
+
page_container_test = next(
|
|
93
|
+
(
|
|
94
|
+
out
|
|
95
|
+
for out in body["outputs"]
|
|
96
|
+
if isinstance(out, dict)
|
|
97
|
+
and out.get("id") == self.page_container
|
|
98
|
+
and out.get("property") == "children"
|
|
99
|
+
),
|
|
100
|
+
None,
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
page_container_test = True
|
|
104
|
+
|
|
105
|
+
# Check whether the callback has an input using the pathname,
|
|
106
|
+
# such a callback will be a routing callback and the pathname
|
|
107
|
+
# should be checked against the public routes
|
|
108
|
+
if not self.auth_protect_layouts:
|
|
109
|
+
if (
|
|
110
|
+
pathname
|
|
111
|
+
and page_container_test
|
|
112
|
+
and public_routes.test(pathname)
|
|
113
|
+
):
|
|
114
|
+
return None
|
|
115
|
+
else:
|
|
116
|
+
# protected by layout
|
|
117
|
+
if pathname and page_container_test:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
# If the route is not a callback route, check whether the path
|
|
121
|
+
# matches a public route, or whether the request is authorised
|
|
122
|
+
if public_routes.test(request.path) or self.is_authorized():
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
# Otherwise, ask the user to log in
|
|
126
|
+
return self.login_request()
|
|
127
|
+
|
|
128
|
+
@abstractmethod
|
|
129
|
+
def is_authorized(self):
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
@abstractmethod
|
|
133
|
+
def login_request(self):
|
|
134
|
+
pass
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Dict, List, Optional, Union, Callable
|
|
4
|
+
import flask
|
|
5
|
+
from dash import Dash
|
|
6
|
+
|
|
7
|
+
from .auth import Auth
|
|
8
|
+
|
|
9
|
+
UserGroups = Dict[str, List[str]]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BasicAuth(Auth):
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
app: Dash,
|
|
16
|
+
username_password_list: Union[list, dict] = None,
|
|
17
|
+
auth_func: Callable = None,
|
|
18
|
+
public_routes: Optional[list] = None,
|
|
19
|
+
user_groups: Optional[Union[UserGroups, Callable[[str], UserGroups]]] = None,
|
|
20
|
+
secret_key: str = None,
|
|
21
|
+
auth_protect_layouts: Optional[bool] = False,
|
|
22
|
+
auth_protect_layouts_kwargs: Optional[dict] = None,
|
|
23
|
+
page_container: Optional[str] = None,
|
|
24
|
+
):
|
|
25
|
+
"""Add basic authentication to Dash.
|
|
26
|
+
|
|
27
|
+
:param app: Dash app
|
|
28
|
+
:param username_password_list: username:password list, either as a
|
|
29
|
+
list of tuples or a dict
|
|
30
|
+
:param auth_func: python function accepting two string
|
|
31
|
+
arguments (username, password) and returning a
|
|
32
|
+
boolean (True if the user has access otherwise False).
|
|
33
|
+
:param public_routes: list of public routes, routes should follow the
|
|
34
|
+
Flask route syntax
|
|
35
|
+
:param user_groups: a dict or a function returning a dict
|
|
36
|
+
Optional group for each user, allowing to protect routes and
|
|
37
|
+
callbacks depending on user groups
|
|
38
|
+
:param secret_key: Flask secret key
|
|
39
|
+
A string to protect the Flask session, by default None.
|
|
40
|
+
It is required if you need to store the current user
|
|
41
|
+
in the session.
|
|
42
|
+
Generate a secret key in your Python session
|
|
43
|
+
with the following commands:
|
|
44
|
+
>>> import os
|
|
45
|
+
>>> import base64
|
|
46
|
+
>>> base64.b64encode(os.urandom(30)).decode('utf-8')
|
|
47
|
+
Note that you should not do this dynamically:
|
|
48
|
+
you should create a key and then assign the value of
|
|
49
|
+
that key in your code.
|
|
50
|
+
:param auth_protect_layouts: bool, defaults to False.
|
|
51
|
+
If true, runs protect_layout()
|
|
52
|
+
:param auth_protect_layouts_kwargs: dict, if provided is passed to the
|
|
53
|
+
protect_layout as kwargs
|
|
54
|
+
:param page_container: string, id of the page container in the app.
|
|
55
|
+
If not provided, this will set the page_container_test to True,
|
|
56
|
+
meaning all pathname callbacks will be judged.
|
|
57
|
+
"""
|
|
58
|
+
super().__init__(
|
|
59
|
+
app,
|
|
60
|
+
public_routes=public_routes,
|
|
61
|
+
auth_protect_layouts=auth_protect_layouts,
|
|
62
|
+
auth_protect_layouts_kwargs=auth_protect_layouts_kwargs,
|
|
63
|
+
page_container=page_container,
|
|
64
|
+
)
|
|
65
|
+
self._auth_func = auth_func
|
|
66
|
+
self._user_groups = user_groups
|
|
67
|
+
if secret_key is not None:
|
|
68
|
+
app.server.secret_key = secret_key
|
|
69
|
+
|
|
70
|
+
if self._auth_func is not None:
|
|
71
|
+
if username_password_list is not None:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
"BasicAuth can only use authorization function "
|
|
74
|
+
"(auth_func kwarg) or username_password_list, "
|
|
75
|
+
"it cannot use both."
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
if username_password_list is None:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
"BasicAuth requires username/password map "
|
|
81
|
+
"or user-defined authorization function."
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
self._users = (
|
|
85
|
+
username_password_list
|
|
86
|
+
if isinstance(username_password_list, dict)
|
|
87
|
+
else {k: v for k, v in username_password_list}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def is_authorized(self):
|
|
91
|
+
header = flask.request.headers.get("Authorization", None)
|
|
92
|
+
if not header:
|
|
93
|
+
return False
|
|
94
|
+
username_password = base64.b64decode(header.split("Basic ")[1])
|
|
95
|
+
username_password_utf8 = username_password.decode("utf-8")
|
|
96
|
+
username, password = username_password_utf8.split(":", 1)
|
|
97
|
+
authorized = False
|
|
98
|
+
if self._auth_func is not None:
|
|
99
|
+
try:
|
|
100
|
+
authorized = self._auth_func(username, password)
|
|
101
|
+
except Exception:
|
|
102
|
+
logging.exception("Error in authorization function.")
|
|
103
|
+
return False
|
|
104
|
+
else:
|
|
105
|
+
authorized = self._users.get(username) == password
|
|
106
|
+
if authorized:
|
|
107
|
+
try:
|
|
108
|
+
flask.session["user"] = {"email": username, "groups": []}
|
|
109
|
+
if callable(self._user_groups):
|
|
110
|
+
flask.session["user"]["groups"] = self._user_groups(username)
|
|
111
|
+
elif self._user_groups:
|
|
112
|
+
flask.session["user"]["groups"] = self._user_groups.get(
|
|
113
|
+
username, []
|
|
114
|
+
)
|
|
115
|
+
except RuntimeError:
|
|
116
|
+
logging.warning("Session is not available. Have you set a secret key?")
|
|
117
|
+
return authorized
|
|
118
|
+
|
|
119
|
+
def login_request(self):
|
|
120
|
+
return flask.Response(
|
|
121
|
+
"Login Required",
|
|
122
|
+
headers={"WWW-Authenticate": 'Basic realm="User Visible Realm"'},
|
|
123
|
+
status=401,
|
|
124
|
+
)
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
from typing import Any, Callable, List, Literal, Optional, Union
|
|
4
|
+
from werkzeug.routing import MapAdapter
|
|
5
|
+
|
|
6
|
+
import dash
|
|
7
|
+
from dash.exceptions import PreventUpdate
|
|
8
|
+
from flask import session, has_request_context
|
|
9
|
+
from dash import html
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
OutputVal = Union[Callable[[], Any], Any]
|
|
13
|
+
CheckType = Literal["one_of", "all_of", "none_of"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def list_groups(
|
|
17
|
+
*,
|
|
18
|
+
groups_key: str = "groups",
|
|
19
|
+
groups_str_split: str = None,
|
|
20
|
+
) -> Optional[List[str]]:
|
|
21
|
+
"""List all the groups the user belongs to.
|
|
22
|
+
|
|
23
|
+
:param groups_key: Groups key in the user data saved in the Flask session
|
|
24
|
+
e.g. session["user"] == {"email": "a.b@mail.com", "groups": ["admin"]}
|
|
25
|
+
:param groups_str_split: Used to split groups if provided as a string
|
|
26
|
+
:return: None or list[str]:
|
|
27
|
+
* None if the user is not authenticated
|
|
28
|
+
* list[str] otherwise
|
|
29
|
+
"""
|
|
30
|
+
if not has_request_context() or "user" not in session:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
user_groups = session.get("user", {}).get(groups_key, [])
|
|
34
|
+
# Handle cases where groups are ,- or ;-separated string,
|
|
35
|
+
# may depend on OIDC provider
|
|
36
|
+
if isinstance(user_groups, str) and groups_str_split is not None:
|
|
37
|
+
user_groups = re.split(groups_str_split, user_groups)
|
|
38
|
+
return user_groups
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def check_groups(
|
|
42
|
+
groups: Optional[Union[Callable, List[str]]] = None,
|
|
43
|
+
*,
|
|
44
|
+
groups_key: str = "groups",
|
|
45
|
+
groups_str_split: str = None,
|
|
46
|
+
check_type: CheckType = "one_of",
|
|
47
|
+
group_lookup: dict = None,
|
|
48
|
+
restricted_users: Optional[Union[Callable, List[str]]] = None,
|
|
49
|
+
restricted_users_lookup: dict = None,
|
|
50
|
+
user_session_key: str = "email",
|
|
51
|
+
) -> Optional[bool]:
|
|
52
|
+
"""Check whether the current user is authenticated
|
|
53
|
+
and has the specified groups.
|
|
54
|
+
|
|
55
|
+
:param groups: List of groups or a python function
|
|
56
|
+
to return a list of groups.
|
|
57
|
+
If this is a function, will be called with group_lookup dict as kwargs.
|
|
58
|
+
The result is used to check for with check_type
|
|
59
|
+
:param groups_key: Groups key in the user data saved in the Flask session
|
|
60
|
+
e.g. session["user"] == {"email": "a.b@mail.com", "groups": ["admin"]}
|
|
61
|
+
:param groups_str_split: Used to split groups if provided as a string
|
|
62
|
+
:param check_type: Type of check to perform.
|
|
63
|
+
Either "one_of", "all_of" or "none_of"
|
|
64
|
+
:param group_lookup: A dictionary of kwargs to be passed
|
|
65
|
+
if groups is a function.
|
|
66
|
+
e.g. {"path": "/test"} will work with this as a
|
|
67
|
+
groups function: check_path(path)
|
|
68
|
+
:param restricted_users: List of restricted users or a python function
|
|
69
|
+
to return a list of users.
|
|
70
|
+
If this is a function, will be called with
|
|
71
|
+
restricted_users_lookup dict as kwargs.
|
|
72
|
+
:param restricted_users_lookup: A dictionary of kwargs to be passed
|
|
73
|
+
if restricted_users is a function.
|
|
74
|
+
e.g. {"path": "/test"} will work with this as a
|
|
75
|
+
restricted_users function: check_users_path(path)
|
|
76
|
+
:param user_session_key: String of a key in the session["user"] cookie
|
|
77
|
+
where the user will be used to determine whether they are
|
|
78
|
+
in the list of restricted_users. Defaults to "email".
|
|
79
|
+
:return: None or boolean:
|
|
80
|
+
* None if the user is not authenticated
|
|
81
|
+
* True if the user is authenticated and has the right permissions
|
|
82
|
+
* False if the user is authenticated but does not have
|
|
83
|
+
the right permissions
|
|
84
|
+
"""
|
|
85
|
+
user_groups = list_groups(
|
|
86
|
+
groups_key=groups_key,
|
|
87
|
+
groups_str_split=groups_str_split,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if user_groups is None:
|
|
91
|
+
# User is not authenticated
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
if restricted_users:
|
|
95
|
+
if callable(restricted_users):
|
|
96
|
+
restricted_users = restricted_users(**(restricted_users_lookup or {}))
|
|
97
|
+
if session["user"][user_session_key] in restricted_users:
|
|
98
|
+
# User is restricted
|
|
99
|
+
return False
|
|
100
|
+
if callable(groups):
|
|
101
|
+
groups = groups(**(group_lookup or {}))
|
|
102
|
+
if groups is None:
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
if check_type == "one_of":
|
|
106
|
+
return bool(set(user_groups).intersection(groups))
|
|
107
|
+
if check_type == "all_of":
|
|
108
|
+
return all(group in user_groups for group in groups)
|
|
109
|
+
if check_type == "none_of":
|
|
110
|
+
return not any(group in user_groups for group in groups)
|
|
111
|
+
|
|
112
|
+
raise ValueError(f"Invalid check_type: {check_type}")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def protected(
|
|
116
|
+
unauthenticated_output: OutputVal,
|
|
117
|
+
*,
|
|
118
|
+
missing_permissions_output: Optional[OutputVal] = None,
|
|
119
|
+
groups: Optional[Union[Callable, List[str]]] = None,
|
|
120
|
+
groups_key: str = "groups",
|
|
121
|
+
groups_str_split: str = None,
|
|
122
|
+
check_type: CheckType = "one_of",
|
|
123
|
+
group_lookup: dict = None,
|
|
124
|
+
restricted_users: Optional[Union[Callable, List[str]]] = None,
|
|
125
|
+
restricted_users_lookup: dict = None,
|
|
126
|
+
user_session_key: str = "email",
|
|
127
|
+
**_kwargs,
|
|
128
|
+
) -> Callable:
|
|
129
|
+
"""Decorate a function or output to alter it depending on the state
|
|
130
|
+
of authentication and permissions.
|
|
131
|
+
|
|
132
|
+
:param unauthenticated_output: Output when the user is not authenticated.
|
|
133
|
+
Note: needs to be a function with no argument or static outputs.
|
|
134
|
+
:param missing_permissions_output: Output when the user is authenticated
|
|
135
|
+
but does not have the right permissions.
|
|
136
|
+
It defaults to unauthenticated_output when not set.
|
|
137
|
+
Note: needs to be a function with no argument or static outputs.
|
|
138
|
+
:param groups: List of authorized user groups
|
|
139
|
+
or a python function to return a list of groups.
|
|
140
|
+
If this is a function, will be called with group_lookup dict as kwargs.
|
|
141
|
+
If no groups are passed,
|
|
142
|
+
the decorator will only check whether the user is authenticated.
|
|
143
|
+
:param groups_key: Groups key in the user data saved in the Flask session
|
|
144
|
+
e.g. session["user"] == {"email": "a.b@mail.com", "groups": ["admin"]}
|
|
145
|
+
:param groups_str_split: Used to split groups if provided as a string
|
|
146
|
+
:param check_type: Type of check to perform.
|
|
147
|
+
Either "one_of", "all_of" or "none_of"
|
|
148
|
+
:param group_lookup: A dictionary of kwargs to be passed
|
|
149
|
+
if groups is a function.
|
|
150
|
+
e.g. {"path": "/test"} will work with this as a
|
|
151
|
+
groups function: check_path(path)
|
|
152
|
+
:param restricted_users: List of restricted users or a python function
|
|
153
|
+
to return a list of users.
|
|
154
|
+
If this is a function, will be called with
|
|
155
|
+
restricted_users_lookup dict as kwargs.
|
|
156
|
+
:param restricted_users_lookup: A dictionary of kwargs to be passed
|
|
157
|
+
if restricted_users is a function.
|
|
158
|
+
e.g. {"path": "/test"} will work with this as a
|
|
159
|
+
restricted_users function: check_users_path(path)
|
|
160
|
+
:param user_session_key: String of a key in the session["user"] cookie
|
|
161
|
+
where the user will be used to determine whether they are
|
|
162
|
+
in the list of restricted_users. Defaults to "email".
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
if missing_permissions_output is None:
|
|
166
|
+
missing_permissions_output = unauthenticated_output
|
|
167
|
+
|
|
168
|
+
def decorator(output: OutputVal):
|
|
169
|
+
def wrap(*args, **kwargs):
|
|
170
|
+
def process_output(output, *args, **kwargs):
|
|
171
|
+
if isinstance(output, Callable):
|
|
172
|
+
return output(*args, **kwargs)
|
|
173
|
+
return output
|
|
174
|
+
|
|
175
|
+
authorized = check_groups(
|
|
176
|
+
groups=groups,
|
|
177
|
+
groups_key=groups_key,
|
|
178
|
+
groups_str_split=groups_str_split,
|
|
179
|
+
check_type=check_type,
|
|
180
|
+
group_lookup=group_lookup,
|
|
181
|
+
restricted_users=restricted_users,
|
|
182
|
+
restricted_users_lookup=restricted_users_lookup,
|
|
183
|
+
user_session_key=user_session_key,
|
|
184
|
+
)
|
|
185
|
+
if authorized is None:
|
|
186
|
+
return process_output(unauthenticated_output)
|
|
187
|
+
if authorized:
|
|
188
|
+
return process_output(output, *args, **kwargs)
|
|
189
|
+
return process_output(missing_permissions_output)
|
|
190
|
+
|
|
191
|
+
if isinstance(output, Callable):
|
|
192
|
+
return wrap
|
|
193
|
+
return wrap()
|
|
194
|
+
|
|
195
|
+
return decorator
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def protected_callback(
|
|
199
|
+
*callback_args,
|
|
200
|
+
unauthenticated_output: Optional[OutputVal] = None,
|
|
201
|
+
missing_permissions_output: Optional[OutputVal] = None,
|
|
202
|
+
groups: Optional[Union[Callable, List[str]]] = None,
|
|
203
|
+
groups_key: str = "groups",
|
|
204
|
+
groups_str_split: str = None,
|
|
205
|
+
check_type: CheckType = "one_of",
|
|
206
|
+
group_lookup: dict = None,
|
|
207
|
+
restricted_users: Optional[Union[Callable, List[str]]] = None,
|
|
208
|
+
restricted_users_lookup: dict = None,
|
|
209
|
+
user_session_key: str = "email",
|
|
210
|
+
**callback_kwargs,
|
|
211
|
+
) -> Callable:
|
|
212
|
+
"""Protected Dash callback.
|
|
213
|
+
|
|
214
|
+
:param **: all args and kwargs passed to a Dash callback
|
|
215
|
+
:param unauthenticated_output: Output when the user is not authenticated.
|
|
216
|
+
**Note**: Needs to be a function with no argument or static outputs.
|
|
217
|
+
You can access the Dash callback context within the function call if
|
|
218
|
+
you need to use some of the inputs/states of the callback.
|
|
219
|
+
If left as None, it will simply raise PreventUpdate, stopping the
|
|
220
|
+
callback from processing.
|
|
221
|
+
:param missing_permissions_output: Output when the user is authenticated
|
|
222
|
+
but does not have the right permissions.
|
|
223
|
+
It defaults to unauthenticated_output when not set.
|
|
224
|
+
**Note**: Needs to be a function with no argument or static outputs.
|
|
225
|
+
You can access the Dash callback context within the function call if
|
|
226
|
+
you need to use some of the inputs/states of the callback.
|
|
227
|
+
If left as None, it will simply raise PreventUpdate, stopping the
|
|
228
|
+
callback from processing.
|
|
229
|
+
:param groups: List of authorized user groups
|
|
230
|
+
or a python function to return a list of groups.
|
|
231
|
+
If this is a function, will be called with group_lookup dict as kwargs.
|
|
232
|
+
:param groups_key: Groups key in the user data saved in the Flask session
|
|
233
|
+
e.g. session["user"] == {"email": "a.b@mail.com", "groups": ["admin"]}
|
|
234
|
+
:param groups_str_split: Used to split groups if provided as a string
|
|
235
|
+
:param check_type: Type of check to perform.
|
|
236
|
+
Either "one_of", "all_of" or "none_of"
|
|
237
|
+
:param group_lookup: A dictionary of kwargs to be passed
|
|
238
|
+
if groups is a function.
|
|
239
|
+
e.g. {"path": "/test"} will work with this as a
|
|
240
|
+
groups function: check_path(path)
|
|
241
|
+
:param restricted_users: List of restricted users or a python function
|
|
242
|
+
to return a list of users.
|
|
243
|
+
If this is a function, will be called with
|
|
244
|
+
restricted_users_lookup dict as kwargs.
|
|
245
|
+
:param restricted_users_lookup: A dictionary of kwargs to be passed
|
|
246
|
+
if restricted_users is a function.
|
|
247
|
+
e.g. {"path": "/test"} will work with this as a
|
|
248
|
+
restricted_users function: check_users_path(path)
|
|
249
|
+
:param user_session_key: String of a key in the session["user"] cookie
|
|
250
|
+
where the user will be used to determine whether they are
|
|
251
|
+
in the list of restricted_users. Defaults to "email".
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
def decorator(func):
|
|
255
|
+
def prevent_unauthenticated():
|
|
256
|
+
logging.info(
|
|
257
|
+
"A user tried to run %s without being authenticated.",
|
|
258
|
+
func.__name__,
|
|
259
|
+
)
|
|
260
|
+
raise PreventUpdate
|
|
261
|
+
|
|
262
|
+
def prevent_unauthorised():
|
|
263
|
+
logging.info(
|
|
264
|
+
"%s tried to run %s but did not have the right permissions.",
|
|
265
|
+
session["user"]["email"],
|
|
266
|
+
func.__name__,
|
|
267
|
+
)
|
|
268
|
+
raise PreventUpdate
|
|
269
|
+
|
|
270
|
+
wrapped_func = dash.callback(*callback_args, **callback_kwargs)(
|
|
271
|
+
protected(
|
|
272
|
+
unauthenticated_output=(
|
|
273
|
+
unauthenticated_output
|
|
274
|
+
if unauthenticated_output is not None
|
|
275
|
+
else prevent_unauthenticated
|
|
276
|
+
),
|
|
277
|
+
missing_permissions_output=(
|
|
278
|
+
missing_permissions_output
|
|
279
|
+
if missing_permissions_output is not None
|
|
280
|
+
else prevent_unauthorised
|
|
281
|
+
),
|
|
282
|
+
groups=groups,
|
|
283
|
+
groups_key=groups_key,
|
|
284
|
+
groups_str_split=groups_str_split,
|
|
285
|
+
check_type=check_type,
|
|
286
|
+
group_lookup=group_lookup,
|
|
287
|
+
restricted_users=restricted_users,
|
|
288
|
+
restricted_users_lookup=restricted_users_lookup,
|
|
289
|
+
user_session_key=user_session_key,
|
|
290
|
+
)(func)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def wrap(*args, **kwargs):
|
|
294
|
+
return wrapped_func(*args, **kwargs)
|
|
295
|
+
|
|
296
|
+
return wrap
|
|
297
|
+
|
|
298
|
+
return decorator
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def protect_layouts(
|
|
302
|
+
public_routes: Union[List[str], MapAdapter] = None, **kwargs
|
|
303
|
+
) -> str:
|
|
304
|
+
if "pages_folder" in dash.get_app().config:
|
|
305
|
+
for pg in dash.page_registry.values():
|
|
306
|
+
new_kwargs = {**kwargs, **pg}
|
|
307
|
+
if new_kwargs.get("unauthenticated_output") is None:
|
|
308
|
+
new_kwargs["unauthenticated_output"] = html.Div(
|
|
309
|
+
"You do not have access to this content."
|
|
310
|
+
)
|
|
311
|
+
if public_routes:
|
|
312
|
+
if isinstance(public_routes, list):
|
|
313
|
+
if not (
|
|
314
|
+
pg["path"] in public_routes
|
|
315
|
+
or pg.get("path_template") in public_routes
|
|
316
|
+
):
|
|
317
|
+
pg["layout"] = protected(**new_kwargs)(pg["layout"])
|
|
318
|
+
elif not (
|
|
319
|
+
public_routes.test(pg.get("path_template"))
|
|
320
|
+
or public_routes.test(pg["path"])
|
|
321
|
+
):
|
|
322
|
+
pg["layout"] = protected(**new_kwargs)(pg["layout"])
|
|
323
|
+
else:
|
|
324
|
+
pg["layout"] = protected(**new_kwargs)(pg["layout"])
|
|
325
|
+
return "your layouts are now protected"
|