In the previous post, we implemented OAuth2 scopes for endpoint handler methods. This post extends that implementation to include UI elements β€” components that send HTTP requests to the server application.

🐍 Index of the Complete Series.

123-feature-image.png
Python FastAPI: OAuth2 Scopes Part 02 - UI Elements and User-Assigned Scopes

The code requires Python 3.12.4. Please refer to the following discussion on how to upgrade to Python 3.12.4.

πŸš€ Please note, complete code for this post can be downloaded from GitHub with:

git clone -b v0.11.0 https://github.com/behai-nguyen/fastapi_learning.git

❢ At the conclusion of the previous post, we stated:

Next, I would like to have an option to display or hide UI elements based on users’ scope availability. For example, for a user such as moss.shanbhogue.10045@gmail.com, who has no scope, the application should be able to decide whether or not to display the My Info and My Info as JSON buttons, since the underlying endpoint handler method rejects the requests anyway.

πŸ‘‰ In this post, we will implement this proposal. Instead of hiding or displaying UI elements, we will enable or disable them. This approach helps users visualise the entire application’s functionalities and understand what access privileges they have and do not have.

❷ Only a single integration test module has been added. The full updated structure of the project is outlined below.

– Please note, those marked with β˜… are updated, and those marked with β˜† are new.

/home/behai/fastapi_learning/
.
β”œβ”€β”€ cert
β”‚Β Β  β”œβ”€β”€ cert.pem
β”‚Β Β  └── key.pem
β”œβ”€β”€ .env β˜…
β”œβ”€β”€ logger_config.yaml
β”œβ”€β”€ main.py
β”œβ”€β”€ pyproject.toml
β”œβ”€β”€ pytest.ini β˜…
β”œβ”€β”€ README.md β˜…
β”œβ”€β”€ src
β”‚Β Β  └── fastapi_learning
β”‚Β Β      β”œβ”€β”€ businesses
β”‚Β Β      β”‚Β Β  β”œβ”€β”€ app_business.py
β”‚Β Β      β”‚Β Β  β”œβ”€β”€ base_business.py
β”‚Β Β      β”‚Β Β  β”œβ”€β”€ base_validation.py
β”‚Β Β      β”‚Β Β  β”œβ”€β”€ employees_mgr.py
β”‚Β Β      β”‚Β Β  └── employees_validation.py
β”‚Β Β      β”œβ”€β”€ common
β”‚Β Β      β”‚Β Β  β”œβ”€β”€ consts.py
β”‚Β Β      β”‚Β Β  β”œβ”€β”€ jwt_utils.py
β”‚Β Β      β”‚Β Β  β”œβ”€β”€ queue_logging.py
β”‚Β Β      β”‚Β Β  └── scope_utils.py
β”‚Β Β      β”œβ”€β”€ controllers
β”‚Β Β      β”‚Β Β  β”œβ”€β”€ admin.py β˜…
β”‚Β Β      β”‚Β Β  β”œβ”€β”€ auth.py β˜…
β”‚Β Β      β”‚Β Β  └── __init__.py β˜…
β”‚Β Β      β”œβ”€β”€ __init__.py
β”‚Β Β      β”œβ”€β”€ models
β”‚Β Β      β”‚Β Β  └── employees.py
β”‚Β Β      β”œβ”€β”€ static
β”‚Β Β      β”‚Β Β  └── styles.css
β”‚Β Β      └── templates
β”‚Β Β          β”œβ”€β”€ admin
β”‚Β Β          β”‚Β Β  └── me.html
β”‚Β Β          β”œβ”€β”€ auth
β”‚Β Β          β”‚Β Β  β”œβ”€β”€ home.html β˜…
β”‚Β Β          β”‚Β Β  └── login.html
β”‚Β Β          └── base.html
└── tests
    β”œβ”€β”€ business
    β”‚Β Β  β”œβ”€β”€ test_employees_mgr.py
    β”‚Β Β  └── test_scope_utils.py
    β”œβ”€β”€ conftest.py
    β”œβ”€β”€ __init__.py
    β”œβ”€β”€ integration
    β”‚Β Β  β”œβ”€β”€ test_admin_itgt.py
    β”‚Β Β  β”œβ”€β”€ test_api_itgt.py
    β”‚Β Β  β”œβ”€β”€ test_auth_itgt.py
    β”‚Β Β  β”œβ”€β”€ test_expired_jwt.py
    β”‚Β Β  β”œβ”€β”€ test_scope_permission_itgt.py
    β”‚Β Β  └── test_scope_ui_itgt.py β˜†
    β”œβ”€β”€ README.md
    └── unit
        └── test_employees.py

❸ In this section, we discuss the code changes, which are fairly straighforward.

β“΅ For simplicity, the option to enable or disable UI elements is set in the environment .env file:

1
2
3
4
# Display UI elements which send requests to the server even though
# the logged in user does not have sufficient scopes to run the 
# endpoint handler methods.
ENABLE_NO_SCOPES_UI = True

That means:

  1. When ENABLE_NO_SCOPES_UI is True, UI elements are enabled even though the logged-in user has no appropriate scopes.
  2. When ENABLE_NO_SCOPES_UI is False, UI elements are disabled when the logged-in user has no appropriate scopes.

β“Ά Next, in the controllers/__init__.py module, we add some template methods and generic helper methods.

● The two template methods are enable_no_scopes_ui() and has_required_scopes(...). They are basically one-liner methods, and they are made available to templates as, and they are made available to templates as:

69
70
templates.env.globals['enable_no_scopes_ui'] = enable_no_scopes_ui
templates.env.globals['has_required_scopes'] = has_required_scopes

● The two helper methods are credentials_exception(...) and attempt_decoding_access_token(...). They are refactored from other controllers’s modules. They are also short and should be self-explanatory.

β“· Next, the core objective of this post is to optionally disable or enable UI elements based on the logged-in users’ assigned scopes. Presently, there are only two UI elements across the entire application UI, both located on the home page, which is delivered right after a successful login. Therefore, we will discuss the controllers/auth.py module and auth/home.html together.

● There are two changes in the controllers/auth.py module:

β‘΄ In the home_page(...) method: Since we need the logged-in user scopes to decide on the state of the UI elements, we add a new parameter token: Annotated[str, Depends(oauth2_scheme)] to this method. This new parameter is then passed to __home_page(request=request, token=token).

β‘΅ In the __home_page(...) helper method: We also add a new parameter token: Annotated[str, Depends(oauth2_scheme)]. Inside the method, we decode the token to get the logged-in user scopes. The decoding can either succeed or fail; in both cases, we pass the appropriate decoding result to the home template.

● Important changes take place in the auth/home.html template:

β‘΄ Handling access token decoding errors:

7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{% if data.status_code is defined and data.detail is defined %}

<div class="row">
    <div class="col">
        <h2>It's on me... Please contact support, quoting the below message:</h2>
    </div>
</div>

<div class="row">
    <div class="col">
        <h2 class="text-danger fw-bold"></h2>
    </div>
</div>

{% else %}

If the data passed over is an HTTPException, we just display the exception detail and nothing else.

β‘΅ Preparing UI elements state:

48
49
50
51
52
53
54
55
{% set required_scopes = ['user:read'] %}

{% set disabled_str = '' %}
{% if not enable_no_scopes_ui() %}
{%     if not has_required_scopes(required_scopes, data['user_scopes']) %}
{%         set disabled_str = 'disabled' %}
{%     endif %}
{% endif %}

This page is simple β€” the only scope required is user:read. πŸ’₯ Other (future) pages might not be this simple; there could be multiple UI elements requiring different scopes. Only when ENABLE_NO_SCOPES_UI is False, we work out if UI elements need to be disabled.

β‘Ά Finally, we apply the UI elements state:

<button type="button" id="meBtn" class="btn btn-link" >My Info as JSON</button>
<button type="submit" class="btn btn-primary" >My Info</button>

β“Έ The single change in the controllers/admin.py module does not involve a logic change. In the get_current_user(...) method, we refactor some code into the two helper methods, and get_current_user(...) now calls these instead.

❹ We did not have to make any changes to the existing tests. They should all pass. The new test module we have implemented to test the new UI elements feature is test_scope_ui_itgt.py. There are only two tests in this new integration test module. Both tests use the user moss.shanbhogue.10045@gmail.com. πŸš€ Please refer to the discussion on test users in the previous post.

❺ Let’s check the application UI elements state for the user moss.shanbhogue.10045@gmail.com/password. In the screenshots below, the first shows ENABLE_NO_SCOPES_UI set to False, meaning UI elements are disabled when the logged-in user has no appropriate scopes:


The second screenshot shows that My Info and My Info as JSON are disabled as expected.

❻ The obvious disadvantage of this implementation is that we must keep the required scopes lists in sync for both the endpoint handler methods and the template UI elements that send HTTP requests to these methods. If the scope requirements change, we must hunt down and update all occurrences. Considering that, within a page, UI elements access different endpoints β€” each requiring a different scope β€” keeping them in sync could be problematic. For now, we will ignore this issue, but as the application grows, it will need to be addressed. Please keep in mind that this is only a proof of concept implementation.

❼ I would like to keep this post short. We have implemented what we set out to do. However, this new feature is not widely applied across the application since it currently lacks extensive functionalities. In the next posts, we will add more functionalities to the application and assess how well we have implemented this feature.

Thank you for reading. I hope you find the information in this post useful. Stay safe, as always.

✿✿✿

Feature image source:

🐍 Index of the Complete Series.