This package enables simple, granular user access management within Dash, with built-in event logging and integration with Flask-Login.
Add the AccessUserMixin
to your flask_login
User
class:
from flask_login import UserMixin
from dash_access import AccessUserMixin, PostgresAccessStore
from flask_sqlalchemy import SQLAlchemy
class User(UserMixin, db.Model, AccessUserMixin):
__tablename__ = "users"
__access_store__ = PostgresAccessStore(POSTGRES_URI)
...
Control user access within your layout with dash_access.Controlled
:
from dash_access import Controlled
app.layout = html.Div([
Controlled(
name="level1",
alt="div",
component = html.H1("Some controlled value"),
),
])
This checks the current_user
's permissions; if the user has access to the given permission name,
the component is returned as planned; otherwise, the alt
option is returned. alt
has three options:
- blank: return
""
(most common) "div"
: return anhtml.Div
that says"Access Denied"
"bad"
: return a link that sends the user to your /bad URL (customizable - useful for full-page control)"custom"
: return a custom value defined in thecustom_value
parameter
dash-access comes with an administration API to create and manage access relationships. Custom database connectors are provided for PostgresQL, SQLite3, MySQL, DynamoDB, and SQLAlchemy.
Included in /examples
are apps that demonstrate full functionality and potential administration workflows.
Authentication is the first step: making sure each user has logged in before showing them any content.
Authorization is next: the user must be authorized to perform some action or to see some content. "Authentication" and "authorization" sound too similar, so dash-access uses the term access control.
dash-access has six main concepts:
- principal: an entity (user, group)
- permission: allows some principal to access some controlled asset
- group: a named collection of permissions
- grant: the action of giving a principal access to a permission
- relationship: the stored link between the principal and the granted permission
- inherits: a group can be granted another group, thus inheriting its permissions
Group permission inheritance allows groups to fullfill the policy, role, and group functions of traditional RBAC. Most access control problems in Dash are simple and most applications are relatively small so adding the additional levels of policy and role make reasoning more complex rather than more useful.
A common pattern is granting permissions to a group and granting that group to another group to create tiers of access among users.
In dash-access, a permission is blind: it doesn't exist outside of being granted to a group or user. A permission does not need to be registered or saved anywhere - it is just a string name.
In the example, level1
and level2
are arbitrary permission names.
level1
has been granted to the user test
, but level2
has
not been granted, so anything requiring level2
will not be shown to user test
.
Relationships are the secret sauce of dash_access
: each permission is defined
as a relationship where a principal is granted a permission of some kind. A user-permission
relationship allows a user to access some asset that requires that permission. A user-group
relationship gives a user access to the groups's permissions. A group-group relationship extends
one group's permissions to encompass another group's permissions.
You can access this system in two ways: by adding an access store and access user mixin to your
User
class model a la flask_login
, or directly by calling the dash_access
internal functions. The first option covers most use cases with Dash, but the latter is more flexible.
Control user access within your layout with dash_access.Controlled
. This looks like
Controlled(
name="level1",
alt="div",
component = html.H1("Some controlled value"),
)
This checks the current user's permissions; if the user has access to the given permission name,
the component is returned as planned; otherwise, the alt
option is returned. alt
has three options:
- blank: return
""
"div"
: return anhtml.Div
that says"Access Denied"
"bad"
: return a link that changes the page to your/bad
page link (custom)
dash-access internals are exposed through dash_access.access
. Each function gets passed an access database and some parameters to create some access control operation. The following is not an exhaustive list:
The relationship API is how you access direct relationships.
import dash_access as da
store = da.Sqlite3AccessStore("local.sqlite3")
# basic API
from dash_access import relationship as rship
rship.create(store, rship.Args(principal="me",principal_type="user",granted="powers",granted_type="permission"))
rship.exists(store,rship.Args("me","user","powers","permission"))
rship.delete(store,rship.Args("me","user","powers","permission"))
rship.copy(store,from_principal="me",from_principal_type="user",to_principal="them",to_principal_type"user")
rship.get_all(store,rship.Args("me","user",granted_type="group"))
Custom database connectors are provided for PostgreSQL, SQLite3, MySQL, DynamoDB, and SQLAlchemy.
Why pass around a custom store
everywhere?
At first it seems strange to pass a store
object to each function.
However, this is an intentional decision with practical benefits:
- access management is not connected the Flask app context
- you can use the API in a Jupyter notebook without an app running
- you can use a different
store
type in different environments - for example, SQLite3 for local development and PostgreSQL for staging and production envs - custom
where
statements for flexible queries, handled in a different way by each client type
Two event types are logged by default: admin events and access events
Any operation that creates, updates, or deletes an access relationship or group logs an event to the admin_events
table.
Whenever a user attempts to access a permission, the attempt is logged in the access_events
table.
These two event logs make it easy to see who tried to access a permission, when they tried to access it, and what the outcome was.
First, install dash_access:
pip install dash-access
Then set up the app and create the databases it will use for authentication and access control:
# server.py
from dash_access import Sqlite3AccessStore, AccessUserMixin
from flask_login import UserMixin, LoginManager
app = dash.Dash(__name__)
db = SQLAlchemy() # in-memory sqlite
login_manager = LoginManager(app)
# create the User model
class User(UserMixin, db.Model, AccessUserMixin):
__tablename__ = "users"
__access_store__ = Sqlite3AccessStore("local.db") # on-disk sqlite
id = db.Column(db.String(40), primary_key=True)
password = db.Column(db.String(200), nullable=False, unique=False)
def set_password(self, password):
"""Create hashed password."""
self.password = generate_password_hash(password)
def check_password(self, password):
"""Check hashed password."""
return check_password_hash(self.password, password)
@classmethod
def get(self, user_id: str):
return User.query.filter_by(id=user_id).first()
@login_manager.user_loader
def load_user(user_id):
return User.get(user_id)
# run once before starting the app
if __name__=="__main__":
with app.app_context():
db.create_all()
test = User(id='test',name='test',)
test.set_password("test")
test.add_permission("level1")
db.session.add(test)
db.session.commit()
Create the initial database tables once with:
python server.py
Finally, create the access-controlled app that uses the previously-created database:
# app.py
import dash
from dash import html
from dash_access import Controlled
from server import app
def login_layout():
return [
dbc.Input(id='username',placeholder='username'),
dbc.Input(id='password',placeholder='password'),
dbc.Button("login",id='login'),
]
app.layout = html.Div([
dcc.Location(id="url",refresh=False),
html.Div(id='content'),
])
def home():
return html.Div([
html.H1("Accessible to all"),
Controlled("level1","div",html.H1("Need level1 access")),
Controlled("level2","div",html.H1("Need level2 access")),
])
@app.callback(Output("url","pathname"),[Input("login","n_clicks")],[State("username","value"),State("password","value")])
def login(n_clicks, email, pw):
existing_user = User.get(email)
if existing_user:
if existing_user.check_password(pw):
login_user(existing_user)
return "/home"
return dash.no_update
@app.callback(Output("content","children"),[Input("url","pathname")])
def router(url):
if url=="/home" or url=='/':
if current_user.is_authenticated:
return home()
if url=='/login':
if current_user.is_authenticated:
return home()
return login()
if __name__ == "__main__":
app.run_server(debug=True,port=8050)
and run it with
python app.py