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 # 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() |
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:
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:
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:
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:
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:
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:
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:
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: