Free Ebook cover Flask Essentials: Practical Backend Patterns for Small Services

Flask Essentials: Practical Backend Patterns for Small Services

New course

14 pages

Flask Essentials: Authorization and Permission Checks in Route Design

Capítulo 11

Estimated reading time: 9 minutes

+ Exercise

What authorization is (and what it is not)

Authentication answers who the caller is; authorization answers what they are allowed to do. In Flask services, authorization should be an explicit layer that can be applied consistently across routes and services. A practical authorization layer typically combines:

  • Role-based checks (e.g., admin, support, member)
  • Resource ownership checks (e.g., user can edit their own profile, but not others)
  • Policy functions that encode business rules (e.g., “can publish only if document is approved”)

The goal is to keep permission logic out of route handlers so routes remain thin: parse input, call a service, return a response.

Designing a clear authorization layer

1) Define permissions as stable identifiers

Prefer explicit permission strings over scattered role checks. Roles can map to permissions, but your code should usually ask “does the actor have permission X?” rather than “is the actor role Y?”.

# permissions.py (example module name; place where it fits your project layout)  PERM_PROJECT_READ = "project:read"  PERM_PROJECT_WRITE = "project:write"  PERM_PROJECT_DELETE = "project:delete"  PERM_USER_ADMIN = "user:admin"

These identifiers become a shared language across routes, services, tests, and documentation.

2) Represent the actor (current user) and their grants

Assume authentication already provides a current actor (e.g., via g.current_user or a request context object). Authorization should not depend on request parsing; it should depend on an actor object and domain objects.

Continue in our app.

You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.

Or continue reading below...
Download App

Download the app

from dataclasses import dataclass  @dataclass(frozen=True) class Actor:     id: int     roles: set[str]     permissions: set[str]  def actor_has(actor: Actor, perm: str) -> bool:     return perm in actor.permissions

If your system uses roles only, you can compute permissions from roles once per request (or at login) and keep checks uniform.

3) Add ownership checks as first-class helpers

Ownership is not a role; it is a relationship between the actor and a specific resource.

def is_owner(actor: Actor, resource_owner_id: int) -> bool:     return actor.id == resource_owner_id

Keep ownership checks simple and composable; do not embed database queries inside them unless you are intentionally building a policy that loads related data.

4) Encode business rules in policy functions

Policies are pure(ish) functions that answer “may actor do action on resource?”. They can combine permissions, ownership, and resource state.

from permissions import PERM_PROJECT_READ, PERM_PROJECT_WRITE  def can_view_project(actor: Actor, project) -> bool:     # Example rule: public projects are readable by anyone authenticated;     # private projects require explicit read permission or ownership.     if project.is_public:         return True     return actor_has(actor, PERM_PROJECT_READ) or is_owner(actor, project.owner_id)  def can_edit_project(actor: Actor, project) -> bool:     # Example rule: editing requires write permission AND project not archived,     # or ownership with a special constraint.     if project.is_archived:         return False     if actor_has(actor, PERM_PROJECT_WRITE):         return True     return is_owner(actor, project.owner_id) and project.allow_owner_edits

Policies should be easy to unit test: pass an actor and a resource, assert True/False.

Keeping permission logic out of route handlers

Approach A: Decorators for route-level permission gates

Use decorators for checks that do not require loading a specific resource (e.g., “must have admin permission” or “must have project:write to create”). This keeps routes clean and consistent.

from functools import wraps  class Forbidden(Exception):     pass  def require_permission(perm: str):     def decorator(fn):         @wraps(fn)         def wrapper(*args, **kwargs):             actor = getattr(g, "current_actor", None)             if actor is None or perm not in actor.permissions:                 raise Forbidden()             return fn(*args, **kwargs)         return wrapper     return decorator

Example usage for a write operation that is not resource-specific:

@bp.post("/projects") @require_permission(PERM_PROJECT_WRITE) def create_project():     payload = request.get_json()     project = project_service.create_project(actor=g.current_actor, data=payload)     return jsonify(project.to_dict()), 201

The route does not contain authorization logic; it declares a requirement.

Approach B: Service-level guards for resource-specific checks

For checks that depend on the resource (ownership, state, membership), place the guard in the service method that loads and mutates the resource. This prevents accidental bypass when the same service is called from multiple routes or background jobs.

class NotFound(Exception):     pass  class Forbidden(Exception):     pass  class ProjectService:     def __init__(self, session):         self.session = session      def get_project_for_view(self, actor: Actor, project_id: int):         project = self.session.get(Project, project_id)         if project is None:             raise NotFound()         # Anti-leak pattern: if actor cannot view, respond as not found.         if not can_view_project(actor, project):             raise NotFound()         return project      def update_project(self, actor: Actor, project_id: int, data: dict):         project = self.session.get(Project, project_id)         if project is None:             raise NotFound()         # For write operations, you may choose Forbidden (see next section).         if not can_edit_project(actor, project):             raise Forbidden()         project.name = data.get("name", project.name)         self.session.add(project)         return project

Routes become orchestration only:

@bp.get("/projects/<int:project_id>") def get_project(project_id: int):     project = project_service.get_project_for_view(g.current_actor, project_id)     return jsonify(project.to_dict())  @bp.patch("/projects/<int:project_id>") def patch_project(project_id: int):     payload = request.get_json()     project = project_service.update_project(g.current_actor, project_id, payload)     return jsonify(project.to_dict())

This pattern centralizes authorization where the resource is loaded and modified.

Protecting read vs. write operations

Read operations: consider “not found” to avoid information leaks

For sensitive resources, returning 404 Not Found when the actor lacks access prevents confirming that the resource exists. This is especially useful for endpoints like GET /projects/<id> where IDs may be guessable.

Common rule of thumb:

  • Read: if unauthorized, often return 404 (hide existence) for private resources.
  • Write: if the actor can locate the resource but cannot modify it, return 403 Forbidden (clear denial), unless existence itself is sensitive.

Be consistent per resource type and document the behavior so clients can handle it.

Write operations: explicit forbidden vs. not found

For updates/deletes, you have two defensible patterns:

  • Strict anti-leak: return 404 for both “missing” and “no access”. This hides existence but can complicate client UX and auditing.
  • Explicit authorization: return 403 when the resource exists but the actor lacks permission. This is clearer for clients and logs, but reveals existence.

Choose based on threat model. For multi-tenant B2B data, anti-leak is often preferred for reads; for internal/admin tools, explicit 403 is usually acceptable.

Step-by-step: implementing a maintainable policy-based authorization

Step 1: Create a policy module per domain

Group policies by domain object (e.g., project_policy.py, invoice_policy.py) so they are discoverable and testable.

# project_policy.py  from permissions import PERM_PROJECT_READ, PERM_PROJECT_WRITE, PERM_PROJECT_DELETE  def can_delete_project(actor: Actor, project) -> bool:     # Example: only explicit delete permission; owners cannot delete if locked     if project.is_locked:         return False     return PERM_PROJECT_DELETE in actor.permissions

Step 2: Add a small “authorization service” (optional) for consistency

If you want a single entry point, wrap policy calls in an authorizer that raises consistent exceptions.

class Authorizer:     def require(self, allowed: bool, *, hide: bool = False):         if allowed:             return         if hide:             raise NotFound()         raise Forbidden()      def require_view_project(self, actor: Actor, project):         self.require(can_view_project(actor, project), hide=True)      def require_edit_project(self, actor: Actor, project):         self.require(can_edit_project(actor, project), hide=False)

Then in services:

def update_project(self, actor: Actor, project_id: int, data: dict):     project = self.session.get(Project, project_id)     if project is None:         raise NotFound()     self.authorizer.require_edit_project(actor, project)     ...

Step 3: Use decorators only for coarse-grained gates

Decorators work best for global permissions that do not depend on resource state. Keep them simple and avoid database access inside decorators to prevent hidden performance costs.

@bp.delete("/projects/<int:project_id>") @require_permission(PERM_PROJECT_DELETE) def delete_project(project_id: int):     project_service.delete_project(g.current_actor, project_id)     return "", 204

Even here, the service should still enforce resource-state rules (e.g., locked projects cannot be deleted), because a decorator cannot know that.

Documenting required permissions (and keeping docs in sync)

Authorization becomes hard to maintain when requirements live only in developers’ heads. Treat permission requirements as an artifact that is easy to review during code changes.

Endpoint-permission matrix

EndpointMethodOperationRequired permission(s)Additional policyUnauthorized response
/projectsGETList projectsproject:read (optional if listing only public)Filter by visibility/membership200 with filtered results
/projects/<id>GETRead projectproject:read OR ownerPublic projects readable404 (hide existence)
/projectsPOSTCreate projectproject:writeMay enforce quotas403
/projects/<id>PATCHUpdate projectproject:write OR owner (if allowed)Cannot edit archived403 (or 404 if strict anti-leak)
/projects/<id>DELETEDelete projectproject:deleteCannot delete locked403 (or 404 if strict anti-leak)

Keep this matrix near the code (e.g., in a docs/authorization.md file or as a module docstring) and update it in the same pull request as endpoint changes.

Inline documentation via decorators and docstrings

Even without a full OpenAPI workflow, you can make permission requirements visible:

  • Add a short docstring on the route: """Requires project:write"""
  • Name decorators clearly: @require_permission(PERM_PROJECT_WRITE)
  • For resource policies, reference the policy function name in service code (e.g., require_edit_project)

Common pitfalls and how to avoid them

Scattering role checks across routes

If routes contain logic like if "admin" in actor.roles in multiple places, you will eventually create inconsistent behavior. Prefer permission identifiers and policy functions.

Checking permissions after performing the action

Authorization should happen before mutating state. In services, load resource, authorize, then update. This also reduces the chance of partial updates when an exception occurs.

Leaking existence through error differences

If GET /resource/123 returns 403 for unauthorized users and 404 for missing resources, an attacker can enumerate valid IDs. For sensitive resources, standardize on 404 when access is denied for reads.

Forgetting authorization on non-HTTP entry points

If you have CLI commands, background jobs, or internal service calls that reuse the same service methods, service-level guards ensure authorization rules remain enforced consistently (or are explicitly bypassed with a clearly named internal method).

Now answer the exercise about the content:

In a Flask service, where should authorization checks that depend on a specific resource (like ownership or archived state) be enforced to avoid accidental bypass across multiple entry points?

You are right! Congratulations, now go to the next page

You missed! Try again.

Resource-specific authorization (ownership, state, membership) should be checked where the resource is loaded and modified. Putting guards in service methods prevents bypass when the same service is called from different routes, jobs, or internal calls.

Next chapter

Flask Essentials: Input Validation and Serialization for Robust APIs

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.