Lesson 4C

A Social Blogging Application — Part 2: User Roles

Share Tweet

Contents

  1. Resources
  2. Part 2: User Roles
    1. Updating the Role table
      1. Adding new attributes to Role table
      2. Defining application permissions
    2. Assigning Roles to Users
    3. Implementing the Mechanism to Verify Users' Permissions
    4. Injecting Permissions Class in HTML Templates
  3. Exercises
  4. Further Reading

Resources


Part 2: User Roles

In most applications, users need to be assigned to the predefined roles/permissions such as normal user, moderator, administrator when using the application. In case of an application that has only two roles such as normal user and administrator, the application can create a boolean column in the User table. A more complex application may need additional roles than normal and administrator roles. In this case, we need to clarify the Roles and Permissions terminologies. According to the Oxford Dictionary, role is the function or position that somebody has or is expected to have in an organization, for example, moderator, normal user, administrator. Each role will be defined to perform a list of permissions such as READ, WRITE, DELETE. In this lesson, we will discuss the implementation of User Roles in our demo application. Our lesson will be organized into 4 sections:

  • Updating the Role table.
  • Assigning roles to users.
  • Implementing the mechanism to verify users' permissions.
  • Injecting Permissions Class in HTML Templates.

We note that we will make some changes in the accompanied source code presented in the textbook. We thus suggest that you should follow this lesson to update the changes.


1/ Updating the Role table

1.1/ Adding new attributes to Role table

The Role table from the previous lesson will be updated with two new attributes (columns/fields), i.e., default_role and permissions. If the application has many roles, there will be a role set as default, for instance, normal user role for a new registered user.

The permissions attribute defines a list of permissions via an integer number. We should note that SQLAlchemy will set an integer attribute (declared by db.Integer) to None by default, we need to implement a class constructor to set this attribute to 0 if there is no initial value provided. In Fig. 1, we present the updated version of class Role(db.Model) in app/models.py.

Fig 1. app/models.py: Update Role class.
Fig 1. app/models.py: Update Role class.
1.2/ Defining application permissions

Fig. 2 presents the application permissions that will be assigned to users. You may notice that the permission values are represented as the power of two — 2k. The benefit of using this sort of representation is that we can sum up the permission values to represent a combination of permissions. For example, if we want to combine the permissions of FOLLOW, COMMENT, and WRITE, the permission value will be 1 + 2 + 4 = 7. Besides that, this representation allows us to employ the bitwise & operator in Python to implement the permissions validator - the has_permission(self, perm) function, as shown in Fig. 4. Let's take an example to understand the use of bitwise & operator in our case; given a permission value is 7, then the binary value of it will be 01112 (01112 = (0 × 23) + (1 × 22) + (1 × 21) + (1 × 20)). If we want to validate whether a user has the WRITE permission (permission value is 4), we first need to convert 4 from decimal value to binary value, and the result will be 01002. When we perform 01112 & 01002, the result will be 01002, equivalent to 4 in decimal. And then we just compare with the perm argument to know whether a user has been given the permission.

Fig 2. Permission names and values.
Fig 2. Permission names and values.

Fig. 3 presents an additional implementation of app/models.py to include a new class - Permission. This class just contains all predefined permission values, as shown in Fig. 2.

Fig 3. app/models.py: Implement Permission class.
Fig 3. app/models.py: Implement Permission class.

With the predefined permission values in Fig. 3, we will implement some new methods in the Role model to manipulate the permissions. Fig. 4 presents the implementation of four new methods, i.e., add_permission, remove_permission, reset_permission, and has_permission. We note that the has_permission method employs the bitwise & operator as we have explained above.

Fig 4. app/models.py: Implement permission validation methods in the Role class.
Fig 4. app/models.py: Implement permission validation methods in the Role class.

Fig. 5 presents the user roles along with their corresponding permissions, for example, an Administrator role includes all the permissions.

Fig 5. User roles along with their permissions.
Fig 5. User roles along with their permissions.

At this point, we have already defined the user roles and their permissions, as shown in Fig. 2 and 5. Now, we are going to discuss the procedure to insert these user roles into the database. We can use the flask shell to insert them manually. However, this approach is time consuming and error prone, we will implement a method in Role class to insert these user roles into the database instead. Technically, we will employ the @staticmethod decorator (annotation) to implement the insert_roles() method so that this method can be directly invoked via classname, for example, Role.insert_roles(). As mentioned in the previous lesson, a method with @staticmethod decorator does not have a self argument like instance methods. In Fig. 6, we show the implementation of insert_roles() method; this method first declares a list of user roles together with their permissions, and then inserts these roles into the Role table. We should note that this method will be invoked for one time only. In order to change or add new roles, we just need to update the roles dictionary.

Fig 6. app/models.py: Implement insert_role() method in Role class.
Fig 6. app/models.py: Implement insert_role() method in Role class.

2/ Assigning roles to users

When users register an account, they are often assigned with a default role, for instance, User role in our case. If an email address of a user is defined in the FLASKY_ADMIN configuration variable, the application will automatically assign an administrative role for this user. Otherwise, the application will assign the User role, as shown in Fig. 7.

Fig 7. app/models.py: Implement the user role assignment method.
Fig 7. app/models.py: Implement the user role assignment method.

Note In Python OOP, like Java OOP, the constructor - def __init__(self, **kwargs) - will be invoked first when creating an instance of a class.


3/ Implementing the Mechanism to Verify Users' Permissions

As we have implemented the methods to verify user's permissions in the Role class, as shown in Fig. 4, these methods can be invoked by instances of this class only. Therefore, in order to allow instances of the User class to verify the roles of the current users, we need to implement additional methods in the User class, as shown in Fig. 8. The can() and is_administrator() methods are implemented to verify either a given permissions or the ADMIN role for the current user.

Fig 8. app/models.py: Implement the permission validation methods in User class; and implement AnonymousUser class.
Fig 8. app/models.py: Implement the permission validation methods in User class; and implement AnonymousUser class.

In Fig. 8, we also implement the AnonymousUser class extending from AnonymousUserMixin of Flask-Login. According to the Flask-Login's documentation, when a user is not actually logged in, current_user is set to an AnonymousUserMixin object by default. If you have custom requirements for anonymous users (for example, they need to have a permissions field), you can provide a callable (either a class or factory function) that creates anonymous users to the LoginManager with login_manager.anonymous_user = AnonymousUser declaration.

Let's recall that views which require users to be logged in can be decorated with the login_required decorator. In this lesson, with the definition of user's roles, the issue here is that do we have any decorators for manipulating user's permissions; and the answer is yes-and-no. The Flask-Login does not supply any decorators to handle the user's permissions, but we can define our own decorators. By using the functools package, we can define a custom decorator, as shown in the following figure.

Fig 9. app/decorators.py: Implement custom decorators to manipulate user's permissions.
Fig 9. app/decorators.py: Implement custom decorators to manipulate user's permissions.

We already have created the custom error handlers in app/main/errors.py to handle the 404 and 500 error codes. You need to create a new error handler for the 403 Forbidden to response status code indicating that the server understands the request but refuses to authorize it. You should note that an error handler has two elements; the first one is the implementation in app/main/errors.py and the second one is its corresponding HTML template.


4/ Injecting Permissions Class in HTML Templates

We have used the syntax such as {{ user }}, {{ user.email }} in HTML templates to render dynamic components. The question here is how to inject (or render/embed) the Permissions class (or information/attributes) into HTML templates. Unlike the current_user variable which is represented for a specific user, the permission values are defined as constant values, so we should make them publicly available to HTML templates. We can employ the app_context_processor provided by Flask to implement the inject_permissions() function, as shown in Fig. 10, and then we can freely access all the fields of the Permission class, as shown in Fig. 3.

Fig 10. app/main/__init__.py: Implement inject_permissions() function.
Fig 10. app/main/__init__.py: Implement inject_permissions() function.

To demonstrate the benefits of app_context_processor decorator, we also implement one more method, i.e., inject_now(). This method will make the now variable available to all HTML templates, so you can just call (or inject) it to the template as {{ now }}. This method is useful when you want to put the year information ({{ now.year }}) in the footer area of all HTML templates. Since it is not a good approach to hard-code the year value to your web pages, and manually change it when we turn to a new year.

At this point, we have finished the definition of the user's roles for the application along with the methods to handle them. Before discussing the next section, I would like to summarize the contents of this lesson as follows:

  • In our application, we have defined four user's roles and each of them has a corresponding list of permissions. First, we updated the Role table to include additional fields such as default_role and permissions. Permissions are represented as the powers of two (2k) in order to take into account two benefits. The first one is to allow the combination of permissions by summing up the permission's values; and the second one is to employ the bitwise & operator for the has_permission() method in the Role class.
  • Second, we implemented the insert_roles() method with @staticmethod decorator to make this method be called via class name. This method should be run for one time only to construct the values of permissions column of the Role table.
  • Third, we defined the procedure to assign roles for users in the class constructor of the User class.
  • Fourth, we defined two new custom decorators to handle @permission_required and @admin_required in app/decorators.py, so that we can use these new defined decorators together with @login_required provided by Flask-Login.

In the following program, as shown in Fig. 11, we demonstrate how to use the @permission_required and @admin_required in app/main/views.py. The results when we visit the http://localhost:5000/admin and http://localhost:5000/user are presented in Fig. 12.

Fig 11. app/main/views.py: Demonstrate the use of @permission_required and @admin_required decorators.
Fig 11. app/main/views.py: Demonstrate the use of @permission_required and @admin_required decorators.
Fig 12. Results when visiting the /admin and /user in a web browser.
Fig 12. Results when visiting the /admin and /user in a web browser.

Before ending this lesson, we have one more thing to do, we need to reconstruct the database since we have updated its structure. A simple way to do this is to delete the current data-dev.sqlite and /migrations directory, and then execute the flask db init and flask db upgrade commands to generate a new database, as mentioned in Lesson 4A - Running your application. Another option is to execute the flask db stamp [--sql] [--tag TAG] <revision> to set the revision in the database to the one given as an argument, without performing any migrations.


Exercises

  1. We will discuss the User Profile page in the future lesson, but at this time, you can implement a basic version of it. In particular, the User Profile page will render all basic information about the current user together with its role.
  2. Create a new route in the main blueprint, for example, @main.route('/admin/users-list'), to print the list of all users in the database. This route is accessible only by an administrator.
  3. Let's recall that we have implemented the inject_now() function, as shown in Fig. 10. You need to update the base template (app/templates/base.html) to include the current year value at the footer so that all extended templates will now have a footer with up-to-date year information.

Further Reading

  1. Bootstrap-Flask
  2. Bootstrap Footer Templates
  3. Primer on Python Decorators
  4. Python Bitwise Operators

Next Lesson

In the next lesson, we will present how to construct the User Profile page for each user which shows the basic user's information such as name, email, account created date, location. And then, we will discuss the most important functionality of our application which is Blog Posts creatation and management.

Lesson 5: A Social Blogging Application — User Profiles & Blog Posts


J4F 😊

Fig 13. The power of power
Fig 12. The power of power (Source: Internet)

Relaxing 🧘