Different user access login to different page in dash_auth

Hi,

How to create the login access to different user which they only able to login the assigned page?

User who doesn’t have the access will not able to review or click to the page.

Currently, I am using the dash_auth to create a simple login page. However, it may need to lock some page for certain user.

Hello @beginof,

Sure thing, you can check out my auth flow here:

Leveraging this, you can add customizations based upon specific user and access levels. You’ll need to make a different dictionary of access levels, and then test to make sure the user is supposed to be able to see the page they are trying to.

If they dont, then you can return something that says they dont have permission to view the content.

This would look something like:

if path in perm[current_user]:
     return 'uh-oh, you dont have access to this content'

Where perm[current_user] is a list of paths they dont have access to.

1 Like

But, essentially, you dont need to alter the flow, just make a dictionary of users and paths they arent allowed to view. Then bump up against it via the dcc.Location upon request to make sure they can access it.

Can you share how the perm[current_user] add into the existing code and able to restrict the user?

How to call it for the validation if I have a listing which contain username, password and role stored in a dataframe (will based on the role to restrict the user)?

example, “admin” role able to view all the page and “view only” able to view the page-1.

Sure, here you go.

To determine roles, it might be easier with a dictionary, something like this:

VALID_USERNAME_PASSWORD = {"test": {"password":"test", "role":"admin"}, "hello": {"password":"world", "role":"user"}}

utils > loginhandler.py

import dash

restricted_page = {}

def require_login(page, **kwargs):
    for pg in dash.page_registry:
        if page == pg:
            restricted_page[dash.page_registry[pg]['path']] = True
            if access_level in kwargs:
                restricted_page[dash.page_registry[pg]['path']]['access_level'] = kwargs['access_level']

callback changes in app.py:

@server.route('/login', methods=['POST'])
def login_button_click():
    if request.form:
        username = request.form['username']
        password = request.form['password']
        if VALID_USERNAME_PASSWORD.get(username) is None:
            return """invalid username and/or password <a href='/login'>login here</a>"""
        if VALID_USERNAME_PASSWORD.get(username)['password'] == password:
            login_user(User(username))
            if 'url' in session:
                if session['url']:
                    url = session['url']
                    session['url'] = None
                    return redirect(url) ## redirect to target url
            return redirect('/') ## redirect to home
        return """invalid username and/or password <a href='/login'>login here</a>"""


@app.callback(
    Output("user-status-header", "children"),
    Output('url','pathname'),
    Input("url", "pathname"),
    Input({'index': ALL, 'type':'redirect'}, 'n_intervals')
)
def update_authentication_status(path, n):
    ### logout redirect
    if n:
        if not n[0]:
            return '', dash.no_update
        else:
            return '', '/login'

    ### test if user is logged in
    if current_user.is_authenticated:
        if path in restricted_page:
             if 'access_level' in restricted_page[path]:
                  if VALID_USERNAME_PASSWORD[current_user]['role'] not in restricted_page[path]['access_level']:
                      return dcc.Link('logout', href='/logout'), '/permission'
        if path == '/login':
            return dcc.Link("logout", href="/logout"), '/'
        return dcc.Link("logout", href="/logout"), dash.no_update
    else:
        ### if page is restricted, redirect to login and save path
        if path in restricted_page:
            session['url'] = path
            return dcc.Link("login", href="/login"), '/login'

    ### if path not login and logout display login link
    if current_user and path not in ['/login', '/logout']:
        return dcc.Link("login", href="/login"), dash.no_update

    ### if path login and logout hide links
    if path in ['/login', '/logout']:
        return '', dash.no_update

pages > permission.py

import dash 

dash.register_page(__name__, path='/permission')

layout = "Sorry, you do not have permission to view this content, please get with your administrator if you think this is wrong"

pages > page-1.py

import dash
from dash import html, dcc, Output, Input, callback
from utils.login_handler import require_login

dash.register_page(__name__)
require_login(__name__, access_level=['admin'])


layout = html.Div(
    [
        html.H1("Page 1"),
        dcc.Dropdown(
            id="page-1-dropdown",
            options=[{"label": i, "value": i} for i in ["LA", "NYC", "MTL"]],
            value="LA",
        ),
        html.Div(id="page-1-content"),
        html.Br(),
        dcc.Link("Go to Page 2", href="/page-2"),
        html.Br(),
        dcc.Link("Go back to home", href="/"),
    ]
)


@callback(Output("page-1-content", "children"), Input("page-1-dropdown", "value"))
def page_1_dropdown(value):
    return f'You have selected "{value}"'

the access_level in the login_handler is invalid, is it I miss one page py?

Sorry, ‘access_level’

Amended to 'access_level'.

Show another error :joy:

restricted_page[dash.page_registry[pg][‘path’]][‘access_level’] = kwargs[‘access_level’]
TypeError: ‘bool’ object does not support item assignment

1 Like

Ok, had to make some more changes:

app.py

import os
from flask import Flask, request, redirect, session
from flask_login import login_user, LoginManager, UserMixin, logout_user, current_user

import dash
from dash import dcc, html, Input, Output, State, ALL
from dash.exceptions import PreventUpdate
from utils.login_handler import restricted_page


import plotly.express as px
from plotly.offline import plot

# from dash import dcc, html, callback
# from dash.dependencies import Output,Input, State
import dash_bootstrap_components as dbc
# from dash import dash_table
# from dash_table import DataTable, FormatTemplate
import dash_mantine_components as dmc
from dash_iconify import DashIconify

# import pandas as pd
# import pandas_datareader.data as web
#
# from datetime import date, datetime
#
#
# import pyodbc
# import os
# import psycopg2
#
# from plotly.io import write_image
# import flask
# import base64

# Exposing the Flask Server to enable configuring it for logging in
server = Flask(__name__)


@server.route('/login', methods=['POST'])
def login_button_click():
    if request.form:
        username = request.form['username']
        password = request.form['password']
        if VALID_USERNAME_PASSWORD.get(username) is None:
            return """invalid username and/or password <a href='/login'>login here</a>"""
        if VALID_USERNAME_PASSWORD.get(username)['password'] == password:
            login_user(User(username))
            if 'url' in session:
                if session['url']:
                    url = session['url']
                    session['url'] = None
                    return redirect(url) ## redirect to target url
            return redirect('/') ## redirect to home
        return """invalid username and/or password <a href='/login'>login here</a>"""


app = dash.Dash(
    __name__, server=server, use_pages=True, suppress_callback_exceptions=True
    ,external_stylesheets=[dbc.themes.BOOTSTRAP, dbc.icons.BOOTSTRAP],
                meta_tags=[{'name': 'viewport',
                            'content': 'width=device-width, initial-scale=1.0'}]
)

# Keep this out of source code repository - save in a file or a database
#  passwords should be encrypted
VALID_USERNAME_PASSWORD = {"test": {"password":"test", "role":"admin"}, "hello": {"password":"world", "role":"user"}}


# Updating the Flask Server configuration with Secret Key to encrypt the user session cookie
server.config.update(SECRET_KEY='291a47103f3cd8fc26d05ffc7b31e33f73ca3d459d6259bd')

# Login manager object will be used to login / logout users
login_manager = LoginManager()
login_manager.init_app(server)
login_manager.login_view = "/login"


class User(UserMixin):
    # User data model. It has to have at least self.id as a minimum
    def __init__(self, username):
        self.id = username


@login_manager.user_loader
def load_user(username):
    """This function loads the user by user id. Typically this looks up the user from a user database.
    We won't be registering or looking up users in this example, since we'll just login using LDAP server.
    So we'll simply return a User object with the passed in username.
    """
    return User(username)


SIDEBAR_STYLE = {
    "position": "fixed",
    "width": "20rem",
    "padding": "2rem 1rem",
    "background-color": "#cbd3dd",
    "z-index": 1050,
    "transition": "width 0.1s ease-in-out",
}

# padding for the page content
CONTENT_STYLE = {
    "margin-left": "10rem",
    "margin-right": "2rem",
    "padding": "2rem 1rem",

}

content = html.Div(id="page-content", children=[], style=CONTENT_STYLE)

home = [' Home', ' login', ' logout']

# page 1 data
# def layout():
sidebar = dbc.Nav([

    dbc.NavLink([
        html.Div([
            html.I(className='bi bi-house', style={'position': 'absolute', 'left': '42px'}),
            html.Span(' Home', style={'padding-left': '20px'}),
        ], className="ms-2"),
    ],
        href='/',
        active="exact",
    ),

    dmc.Accordion(
        dmc.AccordionItem([
            dbc.NavLink([
                html.Div([
                    #html.I(className=page["icon"]),
                    page["name"]
                ], className="ms-2"),
            ],
                href=page["path"],
                active="exact",
            )
            for page in dash.page_registry.values() if page['name'] not in home

        ], label='Profile',
            icon=[
                DashIconify(
                    icon="carbon:search-locate",
                    color=dmc.theme.DEFAULT_COLORS["blue"][6],
                    width=20,
                )
            ],

        ),

        multiple=True,
        disableIconRotation=True,
    )
],
    vertical=True,
    pills=True,
    # className="bg-light",

)

app.layout = dbc.Container([
    dcc.Location(id="url"),
    html.Div([
        dbc.Button(html.I(className="bi bi-list"),  # "Menu",
                   id="open-offcanvas", n_clicks=0),
        dbc.Offcanvas([

            sidebar,
            content
        ],

            id="offcanvas",
            title="Welcome to .../",
            is_open=False,
            scrollable=True,
            backdrop="static",
            style=SIDEBAR_STYLE,
        ),
    ]),
    html.Div(id="user-status-header"),

    dbc.Row([
            # dbc.Col(
            #     [html.Div([
            #         sidebar
            #         ]),
            #     ], xs=4, sm=4, md=2, lg=2, xl=2, xxl=2),

            # dbc.Col([
                    dash.page_container
                # ], xs=8, sm=8, md=10, lg=10, xl=10, xxl=10)
        ]
    )


    ], fluid=True)
    # return layout



@app.callback(
    Output("offcanvas", "is_open"),
    Input("open-offcanvas", "n_clicks"),
    [State("offcanvas", "is_open")],
)
def toggle_offcanvas(n1, is_open):
    if n1:
        return not is_open
    return is_open



@app.callback(
    Output("user-status-header", "children"),
    Output('url','pathname'),
    Input("url", "pathname"),
    Input({'index': ALL, 'type':'redirect'}, 'n_intervals')
)
def update_authentication_status(path, n):
    ### logout redirect
    if n:
        if not n[0]:
            return '', dash.no_update
        else:
            return '', '/login'

    ### test if user is logged in
    if current_user.is_authenticated:
        if path in restricted_page:
             if 'access_level' in restricted_page[path]:
                  if VALID_USERNAME_PASSWORD[current_user.id]['role'] not in restricted_page[path]['access_level']:
                      return dcc.Link('logout', href='/logout'), '/permission'
        if path == '/login':
            return dcc.Link("logout", href="/logout"), '/'
        return dcc.Link("logout", href="/logout"), dash.no_update
    else:
        ### if page is restricted, redirect to login and save path
        if path in restricted_page:
            session['url'] = path
            return dcc.Link("login", href="/login"), '/login'

    ### if path not login and logout display login link
    if current_user and path not in ['/login', '/logout']:
        return dcc.Link("login", href="/login"), dash.no_update

    ### if path login and logout hide links
    if path in ['/login', '/logout']:
        return '', dash.no_update

app.run(debug=True, port='12345')

login_handler.py

import dash

restricted_page = {}

def require_login(page, **kwargs):
    for pg in dash.page_registry:
        if page == pg:
            restricted_page[dash.page_registry[pg]['path']] = {}
            restricted_page[dash.page_registry[pg]['path']]['restricted'] = True
            if 'access_level' in kwargs:
                restricted_page[dash.page_registry[pg]['path']]['access_level'] = kwargs['access_level']