dash-auth-plus 0.0.1a1__tar.gz
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-0.0.1a1/LICENSE +21 -0
- dash_auth_plus-0.0.1a1/MANIFEST.in +1 -0
- dash_auth_plus-0.0.1a1/PKG-INFO +531 -0
- dash_auth_plus-0.0.1a1/README.md +492 -0
- dash_auth_plus-0.0.1a1/dash_auth_plus/__init__.py +32 -0
- dash_auth_plus-0.0.1a1/dash_auth_plus/auth.py +134 -0
- dash_auth_plus-0.0.1a1/dash_auth_plus/basic_auth.py +124 -0
- dash_auth_plus-0.0.1a1/dash_auth_plus/group_protection.py +325 -0
- dash_auth_plus-0.0.1a1/dash_auth_plus/oidc_auth.py +360 -0
- dash_auth_plus-0.0.1a1/dash_auth_plus/public_routes.py +108 -0
- dash_auth_plus-0.0.1a1/dash_auth_plus/version.py +2 -0
- dash_auth_plus-0.0.1a1/dash_auth_plus.egg-info/PKG-INFO +531 -0
- dash_auth_plus-0.0.1a1/dash_auth_plus.egg-info/SOURCES.txt +20 -0
- dash_auth_plus-0.0.1a1/dash_auth_plus.egg-info/dependency_links.txt +1 -0
- dash_auth_plus-0.0.1a1/dash_auth_plus.egg-info/requires.txt +6 -0
- dash_auth_plus-0.0.1a1/dash_auth_plus.egg-info/top_level.txt +1 -0
- dash_auth_plus-0.0.1a1/setup.cfg +4 -0
- dash_auth_plus-0.0.1a1/setup.py +55 -0
- dash_auth_plus-0.0.1a1/tests/test_basic_auth_integration.py +97 -0
- dash_auth_plus-0.0.1a1/tests/test_basic_auth_integration_auth_func.py +83 -0
- dash_auth_plus-0.0.1a1/tests/test_group_protection.py +64 -0
- dash_auth_plus-0.0.1a1/tests/test_oidc_auth.py +208 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) BSd3v
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include README.md
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: dash_auth_plus
|
|
3
|
+
Version: 0.0.1a1
|
|
4
|
+
Summary: Dash Authorization Package.
|
|
5
|
+
Home-page: https://github.com/BSd3v
|
|
6
|
+
Author: Bryan Schroeder
|
|
7
|
+
Author-email: bryan.ri.schroeder@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Framework :: Flask
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Education
|
|
14
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
15
|
+
Classifier: Intended Audience :: Healthcare Industry
|
|
16
|
+
Classifier: Intended Audience :: Manufacturing
|
|
17
|
+
Classifier: Intended Audience :: Science/Research
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Programming Language :: Python
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
26
|
+
Classifier: Topic :: Database :: Front-Ends
|
|
27
|
+
Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
|
|
28
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
29
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
30
|
+
Classifier: Topic :: Software Development :: Widget Sets
|
|
31
|
+
Requires-Python: >=3.8
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
License-File: LICENSE
|
|
34
|
+
Requires-Dist: dash>=1.1.1
|
|
35
|
+
Requires-Dist: flask
|
|
36
|
+
Requires-Dist: werkzeug
|
|
37
|
+
Provides-Extra: oidc
|
|
38
|
+
Requires-Dist: authlib; extra == "oidc"
|
|
39
|
+
|
|
40
|
+
## Dash Authorization and Login
|
|
41
|
+
|
|
42
|
+
Offshoot authentication based upon the open source `dash-auth` library from Plotly. Plotly Docs: [https://dash.plotly.com/authentication](https://dash.plotly.com/authentication)
|
|
43
|
+
|
|
44
|
+
License: MIT
|
|
45
|
+
|
|
46
|
+
For local testing, create a virtualenv, install the dev requirements, and run individual
|
|
47
|
+
tests or test classes:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
python -m venv venv
|
|
51
|
+
. venv/bin/activate
|
|
52
|
+
pip install -r dev-requirements.txt
|
|
53
|
+
python -k ba001
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Note that Python 3.8 or greater is required.
|
|
57
|
+
|
|
58
|
+
> As Plotly will not add new features to the `dash-auth` library, this was created to allow for new features to be added.
|
|
59
|
+
> However, please note that you are entirely responsible maintaining the security with using this open source package.
|
|
60
|
+
> If you are looking for a full fledged solution with little work, check out what Dash Enterprise offers. Learn more at: https://plotly.com/dash/authentication/
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
### Basic Authentication
|
|
65
|
+
|
|
66
|
+
To add basic authentication, add the following to your Dash app:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from dash import Dash
|
|
70
|
+
from dash_auth_plus import BasicAuth
|
|
71
|
+
|
|
72
|
+
app = Dash(__name__)
|
|
73
|
+
USER_PWD = {
|
|
74
|
+
"username": "password",
|
|
75
|
+
"user2": "useSomethingMoreSecurePlease",
|
|
76
|
+
}
|
|
77
|
+
BasicAuth(app, USER_PWD)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
One can also use an authorization python function instead of a dictionary/list of usernames and passwords:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from dash import Dash
|
|
84
|
+
from dash_auth_plus import BasicAuth
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def authorization_function(username, password):
|
|
88
|
+
if (username == "hello") and (password == "world"):
|
|
89
|
+
return True
|
|
90
|
+
else:
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
app = Dash(__name__)
|
|
95
|
+
BasicAuth(app, auth_func=authorization_function)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Public routes
|
|
99
|
+
|
|
100
|
+
You can whitelist routes from authentication with the `add_public_routes` utility function,
|
|
101
|
+
or by passing a `public_routes` argument to the Auth constructor.
|
|
102
|
+
The public routes should follow [Flask's route syntax](https://flask.palletsprojects.com/en/2.3.x/quickstart/#routing).
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from dash import Dash
|
|
106
|
+
from dash_auth_plus import BasicAuth, add_public_routes
|
|
107
|
+
|
|
108
|
+
app = Dash(__name__)
|
|
109
|
+
USER_PWD = {
|
|
110
|
+
"username": "password",
|
|
111
|
+
"user2": "useSomethingMoreSecurePlease",
|
|
112
|
+
}
|
|
113
|
+
BasicAuth(app, USER_PWD, public_routes=["/"])
|
|
114
|
+
|
|
115
|
+
add_public_routes(app, public_routes=["/user/<user_id>/public"])
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
NOTE: If you are using server-side callbacks on your public routes, you should also use dash_auth's new `public_callback` rather than the default Dash callback.
|
|
119
|
+
Below is an example of a public route and callbacks on a multi-page Dash app using Dash's pages API:
|
|
120
|
+
|
|
121
|
+
*app.py*
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from dash import Dash, html, dcc, page_container
|
|
125
|
+
from dash_auth_plus import BasicAuth
|
|
126
|
+
|
|
127
|
+
app = Dash(__name__, use_pages=True, suppress_callback_exceptions=True)
|
|
128
|
+
USER_PWD = {
|
|
129
|
+
"username": "password",
|
|
130
|
+
"user2": "useSomethingMoreSecurePlease",
|
|
131
|
+
}
|
|
132
|
+
BasicAuth(app, USER_PWD, public_routes=["/", "/user/<user_id>/public"])
|
|
133
|
+
|
|
134
|
+
app.layout = html.Div(
|
|
135
|
+
[
|
|
136
|
+
html.Div(
|
|
137
|
+
[
|
|
138
|
+
dcc.Link("Home", href="/"),
|
|
139
|
+
dcc.Link("John Doe", href="/user/john_doe/public"),
|
|
140
|
+
],
|
|
141
|
+
style={"display": "flex", "gap": "1rem", "background": "lightgray", "padding": "0.5rem 1rem"},
|
|
142
|
+
),
|
|
143
|
+
page_container,
|
|
144
|
+
],
|
|
145
|
+
style={"display": "flex", "flexDirection": "column"},
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
app.run(debug=True)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
*pages/home.py*
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from dash import Input, Output, html, register_page
|
|
157
|
+
from dash_auth_plus import public_callback
|
|
158
|
+
|
|
159
|
+
register_page(__name__, "/")
|
|
160
|
+
|
|
161
|
+
layout = [
|
|
162
|
+
html.H1("Home Page"),
|
|
163
|
+
html.Button("Click me", id="home-button"),
|
|
164
|
+
html.Div(id="home-contents"),
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# Note the use of public callback here rather than the default Dash callback
|
|
169
|
+
@public_callback(
|
|
170
|
+
Output("home-contents", "children"),
|
|
171
|
+
Input("home-button", "n_clicks"),
|
|
172
|
+
)
|
|
173
|
+
def home(n_clicks):
|
|
174
|
+
if not n_clicks:
|
|
175
|
+
return "You haven't clicked the button."
|
|
176
|
+
return "You clicked the button {} times".format(n_clicks)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
*pages/public_user.py*
|
|
181
|
+
```python
|
|
182
|
+
from dash import html, dcc, register_page
|
|
183
|
+
|
|
184
|
+
register_page(__name__, path_template="/user/<user_id>/public")
|
|
185
|
+
|
|
186
|
+
def layout(user_id: str):
|
|
187
|
+
return [
|
|
188
|
+
html.H1(f"User {user_id} (public)"),
|
|
189
|
+
dcc.Link("Authenticated user content", href=f"/user/{user_id}/private"),
|
|
190
|
+
]
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
*pages/private_user.py*
|
|
195
|
+
```python
|
|
196
|
+
from dash import html, register_page
|
|
197
|
+
|
|
198
|
+
register_page(__name__, path_template="/user/<user_id>/private")
|
|
199
|
+
|
|
200
|
+
def layout(user_id: str):
|
|
201
|
+
return [
|
|
202
|
+
html.H1(f"User {user_id} (authenticated only)"),
|
|
203
|
+
html.Div("Members-only information"),
|
|
204
|
+
]
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### OIDC Authentication
|
|
208
|
+
|
|
209
|
+
To add authentication with OpenID Connect, you will first need to set up an OpenID Connect provider (IDP).
|
|
210
|
+
This typically requires creating
|
|
211
|
+
* An application in your IDP
|
|
212
|
+
* Defining the redirect URI for your application, for testing locally you can use http://localhost:8050/oidc/callback
|
|
213
|
+
* A client ID and secret for the application
|
|
214
|
+
|
|
215
|
+
Once you have set up your IDP, you can add it to your Dash app as follows:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
from dash import Dash
|
|
219
|
+
from dash_auth_plus import OIDCAuth
|
|
220
|
+
|
|
221
|
+
app = Dash(__name__)
|
|
222
|
+
|
|
223
|
+
auth = OIDCAuth(app, secret_key="aStaticSecretKey!")
|
|
224
|
+
auth.register_provider(
|
|
225
|
+
"idp",
|
|
226
|
+
token_endpoint_auth_method="client_secret_post",
|
|
227
|
+
# Replace the below values with your own
|
|
228
|
+
# NOTE: Do not hardcode your client secret!
|
|
229
|
+
client_id="<my-client-id>",
|
|
230
|
+
client_secret="<my-client-secret>",
|
|
231
|
+
server_metadata_url="<my-idp-.well-known-configuration>",
|
|
232
|
+
)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Once this is done, connecting to your app will automatically redirect to the IDP login page.
|
|
236
|
+
|
|
237
|
+
#### Multiple OIDC Providers
|
|
238
|
+
|
|
239
|
+
For multiple OIDC providers, you can use `register_provider` to add new ones after the OIDCAuth has been instantiated.
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
from dash import Dash, html
|
|
243
|
+
from dash_auth_plus import OIDCAuth
|
|
244
|
+
from flask import request, redirect, url_for
|
|
245
|
+
|
|
246
|
+
app = Dash(__name__)
|
|
247
|
+
|
|
248
|
+
app.layout = html.Div([
|
|
249
|
+
html.Div("Hello world!"),
|
|
250
|
+
html.A("Logout", href="/oidc/logout"),
|
|
251
|
+
])
|
|
252
|
+
|
|
253
|
+
auth = OIDCAuth(
|
|
254
|
+
app,
|
|
255
|
+
secret_key="aStaticSecretKey!", # be sure to replace this key and make it strong as this is how cookies are generated in the application
|
|
256
|
+
# Set the route at which the user will select the IDP they wish to login with
|
|
257
|
+
idp_selection_route="/login",
|
|
258
|
+
)
|
|
259
|
+
auth.register_provider(
|
|
260
|
+
"IDP 1",
|
|
261
|
+
token_endpoint_auth_method="client_secret_post",
|
|
262
|
+
client_id="<my-client-id>",
|
|
263
|
+
client_secret="<my-client-secret>",
|
|
264
|
+
server_metadata_url="<my-idp-.well-known-configuration>",
|
|
265
|
+
)
|
|
266
|
+
auth.register_provider(
|
|
267
|
+
"IDP 2",
|
|
268
|
+
token_endpoint_auth_method="client_secret_post",
|
|
269
|
+
client_id="<my-client-id2>",
|
|
270
|
+
client_secret="<my-client-secret2>",
|
|
271
|
+
server_metadata_url="<my-idp2-.well-known-configuration>",
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@app.server.route("/login", methods=["GET", "POST"])
|
|
276
|
+
def login_handler():
|
|
277
|
+
if request.method == "POST":
|
|
278
|
+
idp = request.form.get("idp")
|
|
279
|
+
else:
|
|
280
|
+
idp = request.args.get("idp")
|
|
281
|
+
|
|
282
|
+
if idp is not None:
|
|
283
|
+
return redirect(url_for("oidc_login", idp=idp))
|
|
284
|
+
|
|
285
|
+
return """<div>
|
|
286
|
+
<form>
|
|
287
|
+
<div>How do you wish to sign in:</div>
|
|
288
|
+
<select name="idp">
|
|
289
|
+
<option value="IDP 1">IDP 1</option>
|
|
290
|
+
<option value="IDP 2">IDP 2</option>
|
|
291
|
+
</select>
|
|
292
|
+
<input type="submit" value="Login">
|
|
293
|
+
</form>
|
|
294
|
+
</div>"""
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
if __name__ == "__main__":
|
|
298
|
+
app.run(debug=True)
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
#### Mixed Logins
|
|
302
|
+
|
|
303
|
+
To utilize OIDC and legacy logins, you need to provide a `idp_selection_route`, here is an example flow
|
|
304
|
+
using `Flask-Login`.
|
|
305
|
+
The `login_user_callback` is also utilized so that you can configure the session cookies to
|
|
306
|
+
be a similar format, or log the OIDC user into the `Flask-Login`
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
from dash import Dash, html
|
|
310
|
+
from dash_auth import OIDCAuth
|
|
311
|
+
from flask import request, redirect, url_for, session
|
|
312
|
+
from flask_login import current_user, LoginManager, login_user, UserMixin
|
|
313
|
+
|
|
314
|
+
app = Dash(__name__)
|
|
315
|
+
|
|
316
|
+
login_manager = LoginManager()
|
|
317
|
+
login_manager.init_app(app.server)
|
|
318
|
+
class User(UserMixin):
|
|
319
|
+
pass
|
|
320
|
+
|
|
321
|
+
@login_manager.user_loader
|
|
322
|
+
def user_loader(username):
|
|
323
|
+
user = User()
|
|
324
|
+
user.id = username
|
|
325
|
+
return user
|
|
326
|
+
|
|
327
|
+
def all_login_method(user_info, idp=None):
|
|
328
|
+
if idp:
|
|
329
|
+
session["user"] = user_info
|
|
330
|
+
session["idp"] = idp
|
|
331
|
+
session['user']['groups'] = ['this', 'is', 'a', 'testing']
|
|
332
|
+
user = User()
|
|
333
|
+
user.id = user_info['email']
|
|
334
|
+
login_user(user)
|
|
335
|
+
else:
|
|
336
|
+
user = User()
|
|
337
|
+
user.id = user_info.get('user')
|
|
338
|
+
login_user(user)
|
|
339
|
+
session['user'] = {}
|
|
340
|
+
session['user']['groups'] = ['nah']
|
|
341
|
+
session['user']['email'] = user_info.get('user')
|
|
342
|
+
return redirect(app.config.get("url_base_pathname") or "/")
|
|
343
|
+
|
|
344
|
+
def layout():
|
|
345
|
+
if request:
|
|
346
|
+
if current_user:
|
|
347
|
+
try:
|
|
348
|
+
return html.Div([
|
|
349
|
+
html.Div(f"Hello {current_user.id}!"),
|
|
350
|
+
html.Button(id='change_users', children='change restrictions'),
|
|
351
|
+
html.Button(id='test', children='you cant use me'),
|
|
352
|
+
html.A("Logout", href="/oidc/logout"),
|
|
353
|
+
])
|
|
354
|
+
except:
|
|
355
|
+
pass
|
|
356
|
+
if 'user' in session:
|
|
357
|
+
return html.Div([
|
|
358
|
+
html.Div(f"""Hello {session['user'].get('email')}!
|
|
359
|
+
You have access to these groups: {session['user'].get('groups')}"""),
|
|
360
|
+
html.Button(id='change_users', children='change restrictions'),
|
|
361
|
+
html.Button(id='test', children='you cant use me'),
|
|
362
|
+
html.A("Logout", href="/oidc/logout"),
|
|
363
|
+
])
|
|
364
|
+
return html.Div([
|
|
365
|
+
html.Div("Hello world!"),
|
|
366
|
+
html.Button(id='change_users', children='change restrictions'),
|
|
367
|
+
html.Button(id='test', children='you cant use me'),
|
|
368
|
+
html.A("Logout", href="/oidc/logout"),
|
|
369
|
+
])
|
|
370
|
+
|
|
371
|
+
app.layout = layout
|
|
372
|
+
|
|
373
|
+
auth = OIDCAuth(
|
|
374
|
+
app,
|
|
375
|
+
secret_key="aStaticSecretKey!",
|
|
376
|
+
# Set the route at which the user will select the IDP they wish to login with
|
|
377
|
+
idp_selection_route="/login",
|
|
378
|
+
login_user_callback=all_login_method
|
|
379
|
+
)
|
|
380
|
+
auth.register_provider(
|
|
381
|
+
"IDP 1",
|
|
382
|
+
token_endpoint_auth_method="client_secret_post",
|
|
383
|
+
client_id="<my-client-id>",
|
|
384
|
+
client_secret="<my-client-secret>",
|
|
385
|
+
server_metadata_url="<my-idp-.well-known-configuration>",
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
@app.server.route("/login", methods=["GET", "POST"])
|
|
389
|
+
def login_handler():
|
|
390
|
+
if request.method == 'POST':
|
|
391
|
+
form_data = request.form
|
|
392
|
+
else:
|
|
393
|
+
form_data = request.args
|
|
394
|
+
|
|
395
|
+
if form_data.get('user') and form_data.get('password'):
|
|
396
|
+
return all_login_method(form_data)
|
|
397
|
+
|
|
398
|
+
if form_data.get('IDP 1'):
|
|
399
|
+
return redirect(url_for("oidc_login", idp='IDP 1'))
|
|
400
|
+
|
|
401
|
+
return """<div>
|
|
402
|
+
<form method="POST">
|
|
403
|
+
<div>How do you wish to sign in:</div>
|
|
404
|
+
<button type="submit" name="IDP 1" value="true">Microsoft</button>
|
|
405
|
+
<div><input name="user"/>
|
|
406
|
+
<input name="password"/></div>
|
|
407
|
+
<input type="submit" value="Login">
|
|
408
|
+
</form>
|
|
409
|
+
</div>"""
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
if __name__ == "__main__":
|
|
413
|
+
app.run_server(debug=True)
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### User-group-based permissions
|
|
417
|
+
|
|
418
|
+
`dash_auth` provides a convenient way to secure parts of your app based on user groups.
|
|
419
|
+
|
|
420
|
+
The following utilities are defined:
|
|
421
|
+
* `list_groups`: Returns the groups of the current user, or None if the user is not authenticated.
|
|
422
|
+
* `check_groups`: Checks the current user groups against the provided list of groups.
|
|
423
|
+
Available group checks are `one_of`, `all_of` and `none_of`.
|
|
424
|
+
The function returns None if the user is not authenticated.
|
|
425
|
+
* `protected`: A function decorator that modifies the output if the user is unauthenticated
|
|
426
|
+
or missing group permission.
|
|
427
|
+
* `protected_callback`: A callback that only runs if the user is authenticated
|
|
428
|
+
and with the right group permissions.
|
|
429
|
+
* `protect_layouts`: A function that will iterate through all pages and called `protected` on the `layout`,
|
|
430
|
+
* passes `kwargs` to `protected` if not already defined in the `layout`
|
|
431
|
+
* eg `protect_layouts(missing_permissions_output=html.Div("I'm sorry, Dave, I'm afraid I can't do that"))`
|
|
432
|
+
|
|
433
|
+
NOTE: user info is stored in the session so make sure you define a secret_key on the Flask server
|
|
434
|
+
to use this feature.
|
|
435
|
+
|
|
436
|
+
If you wish to use this feature with BasicAuth, you will need to define the groups for individual
|
|
437
|
+
basicauth users:
|
|
438
|
+
|
|
439
|
+
```python
|
|
440
|
+
from dash_auth_plus import BasicAuth
|
|
441
|
+
|
|
442
|
+
app = Dash(__name__)
|
|
443
|
+
USER_PWD = {
|
|
444
|
+
"username": "password",
|
|
445
|
+
"user2": "useSomethingMoreSecurePlease",
|
|
446
|
+
}
|
|
447
|
+
BasicAuth(
|
|
448
|
+
app,
|
|
449
|
+
USER_PWD,
|
|
450
|
+
user_groups={"user1": ["group1", "group2"], "user2": ["group2"]},
|
|
451
|
+
secret_key="Test!",
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
# You can also use a function to get user groups
|
|
456
|
+
def check_user(username, password):
|
|
457
|
+
if username == "user1" and password == "password":
|
|
458
|
+
return True
|
|
459
|
+
if username == "user2" and password == "useSomethingMoreSecurePlease":
|
|
460
|
+
return True
|
|
461
|
+
return False
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def get_user_groups(user):
|
|
465
|
+
if user == "user1":
|
|
466
|
+
return ["group1", "group2"]
|
|
467
|
+
elif user == "user2":
|
|
468
|
+
return ["group2"]
|
|
469
|
+
return []
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
BasicAuth(
|
|
473
|
+
app,
|
|
474
|
+
auth_func=check_user,
|
|
475
|
+
user_groups=get_user_groups,
|
|
476
|
+
secret_key="Test!",
|
|
477
|
+
)
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### User-based restrictions
|
|
481
|
+
|
|
482
|
+
`dash_auth` also allows for certain users to be restricted from content and callbacks,
|
|
483
|
+
even when they are assigned to a group which grants them access.
|
|
484
|
+
This allows for more granular control. This is done by passing a list of users to `restricted_users`.
|
|
485
|
+
To check if a user is in the list, it needs the key from the `session["user"]` to compare,
|
|
486
|
+
this is defaulted as `"email"`.
|
|
487
|
+
|
|
488
|
+
eg
|
|
489
|
+
```python
|
|
490
|
+
"""
|
|
491
|
+
where session['user'] = {'email': 'me@email.com'}
|
|
492
|
+
the below callback will not work
|
|
493
|
+
"""
|
|
494
|
+
|
|
495
|
+
@protected_callback(
|
|
496
|
+
Output('test', 'children'),
|
|
497
|
+
Input('test', 'n_clicks'),
|
|
498
|
+
prevent_initial_call=True,
|
|
499
|
+
restricted_users=['me@email.com']
|
|
500
|
+
)
|
|
501
|
+
def testing(n):
|
|
502
|
+
return 'I was clicked'
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Additional flexibility
|
|
506
|
+
|
|
507
|
+
`dash_auth` has functions enabled for `groups` and `restricted_users`, this allows for dynamic
|
|
508
|
+
control after application spinup.
|
|
509
|
+
|
|
510
|
+
When using the functions, the following dictionaries will be passed respectively as `kwargs` to
|
|
511
|
+
the function you provide:
|
|
512
|
+
- `group_lookup`: `{'path': '/test'}` => `pull_groups(path)`
|
|
513
|
+
- `restricted_users_lookup`: `{'path': '/test'}` => `pull_users(path)`
|
|
514
|
+
|
|
515
|
+
### Restricting layouts
|
|
516
|
+
|
|
517
|
+
`dash_auth` by default will cater your page layouts that are in your public routes or where the user is authenticated.
|
|
518
|
+
However, it is possible to lock down layouts by passing these additional arguments to `OIDCAuth` or `BasicAuth` methods:
|
|
519
|
+
|
|
520
|
+
```python
|
|
521
|
+
auth_protect_layouts=True,
|
|
522
|
+
auth_protect_layouts_kwargs=dict(missing_permissions_output=html.Div('you cant get me')),
|
|
523
|
+
page_container='_pages_content'
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
Passing `auth_protect_layouts` tells the app to invoke the `protected` with the `public_routes` passed to
|
|
527
|
+
not protect the layouts of public routes.
|
|
528
|
+
Passing `auth_protect_layouts_kwargs` is the same are the additional `kwargs` passed to the function
|
|
529
|
+
By default, the app will check any non-public callback that has the `pathname` as an input,
|
|
530
|
+
when you pass `page_container` as the `id` of your container element for a page container,
|
|
531
|
+
it will only check the route if it is an output.
|