Roles and permissions
Roles and permissions
You’ve reached the last lesson of the module. By now, your Books app can register users, log them in, hash their passwords, enforce login on private routes, scope queries per user, and even integrate with third-party providers via OAuth if you choose to.
There’s one piece left, and it’s something we’ve actually been doing all along without naming it properly. Every time you wrote a check like:
if book.user_id != session["user_id"]:
flash("You can't access that book.")
return redirect(url_for("index"))
…you were doing authorization.
That’s distinct from authentication. Authentication is who are you. Authorization is what can you do. Until now we’ve only handled the simplest kind of authorization — “this resource belongs to you, nobody else.” Real apps need more. An admin needs to see all users. A moderator needs to delete reviews but not change settings. A read-only viewer needs to see data without changing it.
This lesson is the conceptual close to the module. We’re not going to build a complete role system; we’re going to understand the shape of the problem so you can recognise it (and choose the right tool) the moment your app needs it.
Authentication vs authorization
A clean way to remember the distinction:
Authentication answers “who are you?”
Authorization answers “what are you allowed to do?”
They’re often confused because they happen in sequence and both end in -ation. But they solve different problems with different tools.
Concrete examples from your Books app:
@login_requiredis authentication. It only checks that somebody is signed in.- The ownership check (
book.user_id != session["user_id"]) is authorization. It checks whether this signed-in user is allowed to do this specific thing.
You can have authentication without authorization (every signed-in user has identical permissions — the auth-playground’s /secret route worked that way). You can have authorization without it being interesting (your Books app already does ownership checks but everyone is “just a user”). The richness comes when you have multiple kinds of users with different powers.
That’s the world of roles.
The simplest possible role system
Imagine the Books app needs an admin. The admin’s job: see all users, deactivate troublesome accounts, browse any user’s books to investigate reports.
The simplest possible implementation: a column on the User model.
class User(db.Model):
# ...existing columns...
role = db.Column(db.String(20), nullable=False, default="user")
Two valid values: "user" and "admin". New accounts default to "user". Promote someone to admin manually (a one-line SQL update, or a script).
Then in your views, gate the admin routes:
def admin_required(view_func):
@wraps(view_func)
def wrapper(*args, **kwargs):
if "user_id" not in session:
return redirect(url_for("login"))
user = User.query.get(session["user_id"])
if user.role != "admin":
flash("Admins only.")
return redirect(url_for("index"))
return view_func(*args, **kwargs)
return wrapper
@app.route("/admin/users")
@admin_required
def admin_users():
users = User.query.all()
return render_template("admin/users.html", users=users)
That’s it. One column, one decorator, a few admin routes. For an app with two kinds of users, this is genuinely sufficient. Don’t reach for something more sophisticated until you need it.
When one role isn’t enough
The “string column” approach starts to creak when:
- You have more than three or four roles (user, moderator, editor, admin, super-admin, billing-admin…).
- A user might belong to more than one role simultaneously (an editor who’s also a billing manager).
- You want to assign permissions independently of roles — e.g. “this specific user can delete reviews, even though they’re not a moderator.”
When that happens, you’ve outgrown roles-as-a-column and need to model them as their own thing.
The conventional design is RBAC — Role-Based Access Control. The shape:
- A User has one or more Roles.
- A Role has one or more Permissions.
- A Permission is a specific verb:
"reviews.delete","users.deactivate","books.edit_any".
Now checking access becomes: “Does the current user have any role that grants the permission reviews.delete?”
The schema for this is straightforward — a roles table, a permissions table, and two join tables (user_roles, role_permissions). The checking logic is also straightforward, but writing it from scratch is the kind of work that’s easy to get subtly wrong. Production apps usually reach for a library.
A spectrum of complexity
You can think of permission systems as a spectrum, with cost growing as you move right:
| Level | Pattern | When to use |
|---|---|---|
| 0 | Just @login_required — everyone signed in is equal |
Personal apps, MVPs, single-class user base |
| 1 | Ownership checks — every record belongs to one user | Per-user data (the Books app, today) |
| 2 | A role column — "user" or "admin" |
Need a small back-office for ops or moderation |
| 3 | Multiple named roles | A handful of distinct user types |
| 4 | Full RBAC — users, roles, permissions, with a library | Complex apps, B2B products, regulated domains |
| 5 | ABAC / policy engines — attribute-based, dynamic rules | Enterprise, fine-grained data access |
The most common mistake is starting at Level 4 because it “feels right” before there’s any actual need. Permissions infrastructure has real cost: more code, more places to forget a check, more documentation, more onboarding for new team members. Build it when the business demands it, not before.
For your Books app — and 90% of apps you’ll build as a beginner — Level 1 (ownership) is enough. Add Level 2 (an admin role) the day you need a back-office. Anything beyond that, look at libraries.
What this looks like in Flask
If your app does outgrow simple ownership checks, the Flask ecosystem offers:
- Flask-Principal — a low-level helper for declaring “needs” (required permissions) and “identities” (what a user has). Integrates with Flask-Login. Compact and unopinionated.
- Flask-Security-Too — the same all-in-one library mentioned in Lesson 9. Comes with roles built in, plus registration, password reset, two-factor, the lot. If you want a complete auth+roles system out of the box, this is the option.
- Casbin /
casbin-flask— a more advanced policy engine. Roles, attributes, custom rules. Overkill for most apps; powerful when you need it.
The shape of using any of these — particularly Flask-Principal — is similar to the decorator pattern you already know. You replace @login_required with something like @permission_required("reviews.delete") and the library handles the lookup. The library makes the what explicit and the how invisible.
The danger of forgotten checks
The biggest authorization bug isn’t a wrong check — it’s a missing check.
Recall from Lesson 8: when you added @login_required to all five Books routes, the mini-project explicitly told you to audit every route and confirm none was forgotten. That’s because there’s no compiler or linter that catches “you forgot to check whether the user is allowed to do this.” The code runs fine, the page loads, the action succeeds — and an attacker who guesses the URL gets to do something they shouldn’t.
This is called broken object-level authorization (BOLA, or sometimes IDOR — insecure direct object reference), and it’s the most common API security vulnerability by far. It tops OWASP’s API security list almost every year. The class of bug is small but the surface area is huge: every route that loads a record by ID is a potential offender.
Two habits that catch most of these:
- Audit every route after every change. When you add a route, decide explicitly whether it’s public, login-required, or role-required. Apply the right decorator immediately. Don’t leave it for “later” — there isn’t a later.
- Treat the URL as untrusted. A path parameter like
<int:id>is user input. The decorator says “is someone signed in?”; the next line of the view should usually say “do they own this thing or have permission to act on it?”
If you remember nothing else from this lesson, remember those two habits.
Permissions are a business decision
A subtlety worth naming: deciding who can do what is rarely a purely technical question. It’s a policy question that lives in product specs, legal requirements, and user trust.
Examples:
- “Can a regular user delete their own account?” In the EU, GDPR effectively says yes — but you might need an admin to confirm there are no outstanding obligations first.
- “Can a moderator see the email addresses of users they’ve banned?” That’s a privacy decision, not a database decision.
- “Can a billing admin see invoice line items?” Depends on your industry; in healthcare or finance, “see what’s necessary, no more” is the law.
Authorization code enforces these answers; it doesn’t decide them. Get the answers from whoever owns the product, then encode them carefully.
Looking back at the module
This is the last lesson, so let’s pull back.
When you started, you knew about HTTP and forms. By Lesson 7 you’d built a complete authentication system from scratch in the playground: sessions, hashing, login, logout, decorators, context processors. By Lesson 8 you’d applied all of it to a real CRUD app and made it multi-user with ownership checks.
Lessons 9 through 12 were context — the shape of the larger world your hand-rolled auth lives in:
- Lesson 9 — Flask-Login packages what you wrote.
- Lesson 10 — JWTs are a different model for APIs and mobile clients.
- Lesson 11 — OAuth lets someone else handle identity for you.
- Lesson 12 — Roles and permissions are the next frontier when “logged in or not” stops being enough.
You don’t need to be an expert in any of these now. The point of the conceptual lessons is to make sure that the next time you encounter any of these terms — in a job description, a Stack Overflow answer, a library’s docs, a colleague’s PR — you’ll recognise what’s happening and know roughly where to look next.
Summary
- Authentication is “who are you?”. Authorization is “what can you do?”.
- Your Books app already does authorization — the ownership check is its simplest form.
- Multi-role systems start with one column (
role) and one decorator (@admin_required). - Beyond a handful of roles, model them properly with RBAC: users → roles → permissions.
- Flask has libraries for this (Flask-Principal, Flask-Security-Too, Casbin) — don’t write a permissions engine yourself unless you really mean to.
- The biggest authorization bug is a missing check. Audit every route. Treat URL parameters as untrusted.
- Decisions about who can do what are business decisions — your code only enforces them.
Outcome
You’ve completed Module 5. You can build a real authentication system from scratch and know when not to. You understand the difference between authentication and authorization, when to roll your own and when to reach for a library, and what the road ahead looks like when an app outgrows the basics.
You now have the foundation for almost every web application you’ll build. The remaining work is practice — building things, finding the edge cases, growing the patterns into a real product. Good luck.