Open In App

Flask – Role Based Access Control

Last Updated : 26 Mar, 2024
Improve
Improve
Like Article
Like
Save
Share
Report

Flask is a  micro-framework written in Python. It is used to create web applications using Python. Role-based access control means certain users can access only certain pages. For instance, a normal visitor should not be able to access the privileges of an administrator. In this article, we will see how to implement this type of access with the help of the flask-security library where a Student can access one page, a Staff can access two, a Teacher can access three, and an Admin accesses four pages.

Note: For storing users’ details we are going to use flask-sqlalchemy and db-browser for performing database actions. You can find detailed tutorial here.

Creating the Flask Application

Step 1: Create a Python virtual environment.

To avoid any changes in the system environment, it is better to work in a virtual environment.

Step 2: Install the required libraries

pip install flask flask-security flask-wtf==1.0.1 flask_sqlalchemy email-validator

Step 3: Initialize the flask app.

Import Flask from the flask library and pass __name__ to Flask. Store this in a variable.

Python3
# import Flask from flask
from flask import Flask
# pass current module (__name__) as argument
# this will initialize the instance

app = Flask(__name__)

Step 4: Configure some settings that are required for running the app.

To do this we use app.config[‘_____‘]. Using it we can set some important things without which the app might not work.

  • SQLALCHEMY_DATABASE_URI is the path to the database. SECRET_KEY is used for securely signing the session cookie.
  • SECURITY_PASSWORD_SALT is only used if the password hash type is set to something other than plain text.
  • SECURITY_REGISTERABLE allows the application to accept new user registrations. SECURITY_SEND_REGISTER_EMAIL specifies whether the registration email is sent. 

Some of these are not used in our demo, but they are required to mention explicitly.

Python3
# path to sqlite database
# this will create the db file in instance
# if database not present already
app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///g4g.sqlite3"
# needed for session cookies
app.config['SECRET_KEY'] = 'MY_SECRET'
# hashes the password and then stores in the database
app.config['SECURITY_PASSWORD_SALT'] = "MY_SECRET"
# allows new registrations to application
app.config['SECURITY_REGISTERABLE'] = True
# to send automatic registration email to user
app.config['SECURITY_SEND_REGISTER_EMAIL'] = False

Step 5: Import SQLAlchemy for database

Because we are using SQLAlchemy for database operations, we need to import and initialize it into the app using db.init_app(app). The app_context() keeps track of the application-level data during a request

Python3
# import SQLAlchemy for database operations
# and store the instance in 'db'
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
db.init_app(app)

# runs the app instance
app.app_context().push()

Step 6: Create DB Models

For storing the user session information, the flask-security library is used. Here to store information of users UserMixin is used by importing from the library. Similarly, to store information about the roles of users, RoleMixin is used. Both are passed to the database tables’ classes.

Here we have created a user table for users containing id, email, password, and active status. The role is a table that contains the roles created with id and role name. The roles_users table contains the information about which user has what roles. It is dependent on the user and role table for user_id and role_id, therefore they are referenced from ForeignKeys.

Then we are creating all those tables using db.create_all() this will make sure that the tables are created in the database for the first time. Keeping or removing it afterward will not affect the app unless a change is made to the structure of the database code.

Python3
# import UserMixin, RoleMixin
from flask_security import UserMixin, RoleMixin

# create table in database for assigning roles
roles_users = db.Table('roles_users',
        db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
        db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))    

# create table in database for storing users
class User(db.Model, UserMixin):
    __tablename__ = 'user'
    id = db.Column(db.Integer, autoincrement=True, primary_key=True)
    email = db.Column(db.String, unique=True)
    password = db.Column(db.String(255), nullable=False, server_default='')
    active = db.Column(db.Boolean())
    # backreferences the user_id from roles_users table
    roles = db.relationship('Role', secondary=roles_users, backref='roled')

# create table in database for storing roles
class Role(db.Model, RoleMixin):
    __tablename__ = 'role'
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(80), unique=True)
    
# creates all database tables
@app.before_first_request
def create_tables():
    db.create_all()

Now, you have tables but you still don’t have roles created. You have to create those roles(Admin, Teacher, Staff, Student). Keep in mind that the id for Admin role must be 1, for Teacher: 2, Staff: 3 and Student: 4.

Please create a new file in the same folder as app.py, call it create_roles.py and add the below code. remember to execute this file post db creation.

Python
# create_roles.py
from app import Role, User, db

def create_roles():
    admin = Role(id=1, name='Admin')
    teacher = Role(id=2, name='Teacher')
    staff = Role(id=3, name='Staff')
    student = Role(id=4, name='Student')

    db.session.add(admin)
    db.session.add(teacher)
    db.session.add(staff)
    db.session.add(student)

    db.session.commit()
    print("Roles created successfully!")

# Function calling will create 4 roles as planned!
create_roles()

Step 7: Define User and Role in Database

We need to pass this database information to flask_security so as to make the connection between those. For that, we import SQLAlchemySessionUserDatastore and pass the table containing users and then the roles. This datastore is then passed to Security which binds the current instance of the app with the data. We also import LoginManager and login_manager which will maintain the information for the active session. login_user  assigns the user as a current user for the session.

Python3
# import required libraries from flask_login and flask_security
from flask_login import LoginManager, login_manager, login_user
from flask_security import Security, SQLAlchemySessionUserDatastore

# load users, roles for a session
user_datastore = SQLAlchemySessionUserDatastore(db.session, User, Role)
security = Security(app, user_datastore)

Step 8: Create a Home Route

The home page of our web app is at the ‘/’ route. So, every time the ‘/’ is routed, the code in index.html file will be rendered using a module render_template. Below the @app.route() decorator, the function needs to be defined so, that code is executed from that function. 

Python3
# import the required libraries
from flask import render_template, redirect, url_for

# ‘/’ URL is bound with index() function.
@app.route('/')
# defining function index which returns the rendered html code
# for our home page
def index():
    return render_template("index.html")

index.html

Below is the HTML code for index.html Some logic is applied using the Jinja2 templating engine which behaves similarly to python.  The current_user  variable stores the information of the currently logged-in user. So, here {% if current_user.is_authenticated  %} means if a user is logged in show that code:{{ current_user.email }} is a variable containing the email of the current user. Because the user can have many roles so roles are a list, that’s why a for loop is used. Otherwise, the code in {% else %} part is rendered, finally getting out of the if block with {% endif %}.

HTML
<!-- index.html -->

<!-- links to the pages -->
<a href="/teachers">View all Teachers</a> (Access: Admin)<br><br>
<a href="/staff">View all Staff</a> (Access: Admin, Teacher)<br><br>
<a href="/students">View all Students</a> (Access: Admin, Teacher, Staff)<br><br>
<a href="/mydetails">View My Details</a> (Access: Admin, Teacher, Staff, Student)
<br><br>
<!-- Show only if user is logged in -->
{% if current_user.is_authenticated %}
    <!-- Show current users email -->
    <b>Current user</b>: {{current_user.email}}
    <!-- Current users roles -->
    | <b>Role</b>: {% for role in current_user.roles%}
                    {{role.name}}
           {% endfor %} <br><br>
    <!-- link for logging out -->
    <a href="/logout">Logout</a>
<!-- Show if user is not logged in -->
{% else %}
    <a href="/signup">Sign up</a> | <a href="/signin">Sign in</a>
{% endif %}
<br><br>

Output:

How to implement role based access control in Flask?

 

Step 9: Create Signup Route

If the user visits the ‘/signup’ route the request is GET, so the else part will render this HTML page, and if the form is submitted the code in if the condition that is POST method is executed.

The data in the HTML form is requested using the request module. First, we check if the user already exists in the database by querying for the user using the email provided and passing the msg according to that.   

If not then we add the user and append the chosen role to the roles_users DB table, for this we query for the role using the id that we will get from options in the radio button, this will return an object containing all the column attributes of that role, in this case, the id and name of the role. And then log the user in for the user’s current session with login_user(user).

Python3
# import 'request' to request data from html
from flask import request

# signup page
@app.route('/signup', methods=['GET', 'POST'])
def signup():
    msg=""
    # if the form is submitted
    if request.method == 'POST':
    # check if user already exists
        user = User.query.filter_by(email=request.form['email']).first()
        msg=""
        # if user already exists render the msg
        if user:
            msg="User already exist"
            # render signup.html if user exists
            return render_template('signup.html', msg=msg)
        
        # if user doesn't exist
        
        # store the user to database
        user = User(email=request.form['email'], active=1, password=request.form['password'])
        # store the role
        role = Role.query.filter_by(id=request.form['options']).first()
        user.roles.append(role)
        
        # commit the changes to database
        db.session.add(user)
        db.session.commit()
        
        # login the user to the app
        # this user is current user
        login_user(user)
        # redirect to index page
        return redirect(url_for('index'))
        
    # case other than submitting form, like loading the page itself
    else:
        return render_template("signup.html", msg=msg)

signup.html page:

Here in the form, the action is ‘#’ which means after submitting the form the current page itself is loaded. The method in the form is POST because we are creating a new entry in the database. There are fields for email, password, and choosing a role with a radio button. In the radio button, the value should be different because that’s the main differentiator of the chosen role.

Also, an if condition is applied using Jinja2, {% if %} which checks that if a user is logged in, and if not  {%else %}  then only shows the form otherwise just shows the already logged-in message.

HTML
<!-- signup.html -->

<h2>Sign up</h2>

<!-- Show only if user is logged in -->
{% if current_user.is_authenticated %}
    You are already logged in.

<!-- Show if user is NOT logged in -->    
{% else %}
{{ msg }}<br>

<!-- Form for signup -->
<form action="#" method="POST" id="signup-form">
            <label>Email Address </label>
            <input type="text" name="email" required /><br><br>
            
            <label>Password </label>
            <input type="password" name="password" required/><br><br>
            
            <!-- Options to choose role -->
              <!-- Give the role ids in the value -->
            <input type="radio" name="options" id="option1" value=1 required> Admin </input> 
        <input type="radio" name="options" id="option2" value=2> Teacher </input>
        <input type="radio" name="options" id="option3" value=3> Staff </input>
            <input type="radio" name="options" id="option3" value=4> Student </input><br>
        <br>
        
            <button type="submit">Submit</button><br><br>
            
            <!-- Link for signin -->
            <span>Already have an account?</span>
            <a href="/signin">Sign in</a>
</form>
<!-- End the if block -->
{% endif %}

Output:

How to implement role based access control in Flask?

 

Step 10: Create Signin Route

As you might have noticed we are using two methods GET, and POST. That is because we want to know if the user has just loaded the page (GET) or submitted the form (POST). Then we check if the user exists by querying the database. If the user exists then we see if the password matches. If both are validated the user is logged in using login_user(user). Otherwise, the msg is passed to HTML accordingly i.e., if the password is wrong msg is set to “Wrong password” and if the user doesn’t exist then the msg is set to “User doesn’t exist”.

Python3
# signin page
@app.route('/signin', methods=['GET', 'POST'])
def signin():
    msg=""
    if request.method == 'POST':
        # search user in database
        user = User.query.filter_by(email=request.form['email']).first()
        # if exist check password
        if user:
            if  user.password == request.form['password']:
                # if password matches, login the user
                login_user(user)
                return redirect(url_for('index'))
            # if password doesn't match
            else:
                msg="Wrong password"
        
        # if user does not exist
        else:
            msg="User doesn't exist"
        return render_template('signin.html', msg=msg)
        
    else:
        return render_template("signin.html", msg=msg)

signin.html

Similar to the signup page, check if a user is already logged in, if not then show the form asking for email and password. The form method should be POST. Ask in the form for, email and password. We can also show links for sign-up optionally.

HTML
<!-- signin.html -->

<h2>Sign in</h2>

<!-- Show only if user is logged in -->
{% if current_user.is_authenticated %}
    You are already logged in.
    
<!-- Show if user is NOT logged in -->    
{% else %}
<!-- msg that was passed while rendering template -->
{{ msg }}<br>

<form action="#" method="POST" id="signin-form">
            <label>Email Address </label>
            <input type="text" name="email" required /><br><br>
            
            <label>Password </label>
            <input type="password" name="password" required/><br><br>
            
            <input class="btn btn-primary" type="submit" value="Submit"><br><br>
    
            <span>Don't have an account?</span>
            <a href="/signup">Sign up</a>
</form>
{% endif %}

Output:

How to implement role based access control in Flask?

 

Step 11: Create a Teacher Route

We are passing the users with the role of Teacher to the HTML template. On the home page if we click any link then it will load the same page if the user is not signed in. If the user is signed in we want to give Role Based Access so that the user with the role:

  • Students can access View My Details page.
  • Staff can access View My Details and View all Students pages.
  • The teacher can access View My Details, View all Students, and View all Staff pages.
  • Admin can access View My Details, View all Students, View all Staff, and View all Teachers pages.

We need to import, roles_accepted: this will check the database for the role of the user and if it matches the specified roles then only the user is given access to that page. The teacher’s page can be accessed by Admin only using @roles_accepted(‘Admin’). 

Python3
# to implement role based access
# import roles_accepted from flask_security
from flask_security import roles_accepted

# for teachers page
@app.route('/teachers')
# only Admin can access the page
@roles_accepted('Admin')
def teachers():
    teachers = []
    # query for role Teacher that is role_id=2
    role_teachers = db.session.query(roles_users).filter_by(role_id=2)
    # query for the users' details using user_id
    for teacher in role_teachers:
        user = User.query.filter_by(id=teacher.user_id).first()
        teachers.append(user)
    # return the teachers list
    return render_template("teachers.html", teachers=teachers)

teachers.html

The teachers passed in the render_template is a list of objects, containing all the columns of the user table, so we’re using Python for loop in jinja2 to show the elements in the list in HTML ordered list tag.

HTML
<!-- teachers.html -->

<h3>Teachers</h3>

<!-- list that shows all teachers' email -->
<ol>
{% for teacher in teachers %}
<li>
{{teacher.email}}
</li>
{% endfor %}
</ol>

Output:

How to implement role based access control in Flask?

 

Step 12: Create staff, student, and mydetail Routes

Similarly, routes for other pages are created by adding the roles to the decorator @roles_accepted().

Python3
# for staff page
@app.route('/staff')
# only Admin and Teacher can access the page
@roles_accepted('Admin', 'Teacher')
def staff():
    staff = []
    role_staff = db.session.query(roles_users).filter_by(role_id=3)
    for staf in role_staff:
        user = User.query.filter_by(id=staf.user_id).first()
        staff.append(user)
    return render_template("staff.html", staff=staff)
    
# for student page
@app.route('/students')
# only Admin, Teacher and Staff can access the page
@roles_accepted('Admin', 'Teacher', 'Staff')
def students():
    students = []
    role_students = db.session.query(roles_users).filter_by(role_id=4)
    for student in role_students:
        user = User.query.filter_by(id=student.user_id).first()
        students.append(user)
    return render_template("students.html", students=students)
    
# for details page
@app.route('/mydetails')
# Admin, Teacher, Staff and Student can access the page
@roles_accepted('Admin', 'Teacher', 'Staff', 'Student')
def mydetails():
    return render_template("mydetails.html")

staff.html

Here, we are iterating all the staff and extracting their email IDs.

HTML
<! staff.html -->

<h3>Staff</h3>
<ol>
{% for staf in staff %}
<li>
{{staf.email}}
</li>
{% endfor %}
</ol>

Output:

How to implement role based access control in Flask?

 

student.html

Here, we are iterating all the students and extracting their email IDs.

HTML
<!-- students.html -->

<h3>Students</h3>
<ol>
{% for student in students %}
<li>
{{student.email}}
</li>
{% endfor %}
</ol>

Output:

How to implement role based access control in Flask?

 

mydetails.html

Similar to the index page, to show the role use a for loop from Jinja2, because a user can more than one role i.e., current_user.roles is a list of roles that were queried from the database.

HTML
<!-- mydetails.html -->

<h3>My Details</h3><br>
<b>My email</b>: {{current_user.email}}
| <b>Role</b>: {% for role in current_user.roles%}
                    {{role.name}}
       {% endfor %} <br><br>

Output:

How to implement role based access control in Flask?

 

Step 13: Finally, Add code Initializer.

Here, debug is set to True. When in a development environment. It can be set to False when the app is ready for production.

Python3
#for running the app
if __name__ == "__main__":  
    app.run(debug = True)

Now test your app by running the below command in the terminal.

python app.py

Go to:

http://127.0.0.1:5000

Output:

How to implement role based access control in Flask?

Like Article
Suggest improvement
Previous
Next
Share your thoughts in the comments

Similar Reads