Access control

A feature gates its screens with role_required, hides the links to them with has_role, and a feature with finer rules for another feature’s screens registers an access policy for them.

How role_required decides whether a request reaches the view Request events.admin_edit Signed in? no, to the login page Active? no, 403 A policy covers the endpoint? True lets in, False answers 403 None asks a shorter prefix, then goes on Role in the roles asked? yes, the view runs no, 403 has_role asks the same questions without answering the request, so a link is drawn only for an account the screen would let in.
A policy is asked only about a signed-in, active account, and only for the endpoints under its prefix.

Table of contents

  1. Roles
  2. Gate a screen
  3. Hide a link
  4. Access policies
  5. Special cases
  6. See also

Roles

The account’s role lives in the role column that splent_feature_auth adds to User. Auth’s vocabulary is admin, staff and user, and a product adds a role by using a new name in its gates and seeders.

The catalog features follow two conventions, the same pair the admin panel uses for its dashboard.

Constant Roles Used for
PANEL_ROLES ("admin", "staff") Content and inbox screens in the panel, such as posts, team members or contact messages
ADMIN_ROLES ("admin",) The site’s secrets and site-wide switches, such as captcha keys

Each feature declares the constant it uses next to its routes, so the gate and the sidebar link read the same tuple.

Gate a screen

from splent_framework.decorators.decorators import role_required

# Who manages courses in the panel. The admin dashboard lands the same roles.
PANEL_ROLES = ("admin", "staff")


@courses_bp.route("/admin/courses")
@role_required(*PANEL_ROLES)
def admin_courses():
    ...

role_required already asks for a login, so it replaces login_required on the view. An anonymous visitor goes to the login page, a signed-in account outside the roles gets 403.

from splent_framework.decorators.decorators import has_role


def courses_sidebar_link():
    if not has_role(*PANEL_ROLES, endpoint="courses.admin_courses"):
        return ""
    return render_template("hooks/sidebar_courses.html")

Pass the endpoint the link opens. Without it has_role reads the roles alone, and a link to a screen some policy opens to other accounts would be missing for them.

An entry of the menu registry needs none of this. role_required leaves the roles it asks for on the view, required_roles(view) reads them, and the menu asks has_role with them and the endpoint for every entry that names no roles of its own.

Access policies

A feature that brings finer rules for another feature’s screens registers a policy for their endpoints. edition_staff lets the staff of an edition into the events and editions admin, although their accounts have the plain user role, and every other panel screen keeps refusing them.

from splent_framework.decorators.decorators import register_access_policy

# The screens this feature has decided, each with its per-edition check.
STAFF_ENDPOINTS = {
    "events.admin_index": any_edition_role,
    "events.admin_edit": on_the_event("edit_event"),
    "editions.admin_edit": on_the_edition("manage_edition"),
}


def staff_of_some_edition(user, endpoint, view_args):
    if staff_service.is_admin(user):
        return None  # the roles decide, before any query
    if endpoint not in STAFF_ENDPOINTS:
        return None  # a screen this feature has not decided
    return bool(staff_service.editions_for(user))


def init_feature(app):
    register_access_policy("events.admin", staff_of_some_edition)
    register_access_policy("editions.admin", staff_of_some_edition)

Three habits keep a policy safe.

  • Pass the accounts the roles already handle, first and without a query. A policy that raises refuses, so a failing query would otherwise lock the site admin out.
  • Answer only for the screens the feature has decided. A new screen added later stays with its roles until someone decides it.
  • Say False to an account the feature knows it must keep out, so the sidebar link disappears for it instead of opening a page that answers 403.

The per-edition checks in STAFF_ENDPOINTS run in a before_request hook of the same feature, which reads the same table, so the policy and the narrowing cannot drift apart.

policy(user, endpoint, view_args) answers for every endpoint whose name starts with the prefix. The prefix starts with the blueprint’s name and a dot (events.admin, or events. for the whole blueprint), so it never covers another blueprint whose name merely starts the same way.

Answer Effect
True The account gets in, whatever the screen’s roles
False The account gets 403, whatever its role
None The policy passes. The next shorter prefix decides, or the screen’s roles when none is left

Register policies from init_feature. The registry is filled at startup and only read while serving.

Special cases

Case Behaviour
Anonymous visitor Sent to the login page. No policy is asked
Inactive account 403, even when a policy would answer True
Several prefixes match Asked from the longest to the shortest, and the first answer that is not None decides
The same prefix registered twice The later policy replaces the earlier one, with a warning in the log
A prefix without the blueprint and a dot register_access_policy raises ValueError
A policy raises The request is refused with 403 and the error is logged
A view with only login_required Policies are not asked. Gate it with role_required first
Two role_required stacked on one view Both are asked, so only the roles they share get in, and required_roles(view) returns those
Tests of a feature’s own gate Use the no_access_policies fixture, or with isolated_access_policies():. Policies other features registered at startup come back when the test ends. The registry belongs to the process, not to one app

See also


Back to top

splent. Distributed by an LGPL license v3. Contact us: drorganvidez@us.es