Preparing your learning space...
38% through Security for FDEs tutorials
Authentication tells you who the request came from; access control decides what that person may actually do. This tutorial covers the models you'll use to express that decision — ownership, roles, policies — plus the three principles that keep any model safe: least privilege, default-deny, and enforcement at the boundary. By the end you can look at any API endpoint and say exactly which checks must run before a request is allowed.
Access control and authorization both answer "what may this identity do?" — the difference is scope. Authorization is the moment your app checks one permission. Access control is the whole system that decides who can reach what: users, roles, policies, and the rules that govern them. Think of authorization as the individual check at each door, and access control as the building's entire plan of doors and keys.
A typical system does both: identify a user (authentication), then decide what that user may do (access control), and, behind each action, check one specific permission (authorization).
You'll see three dominant ways to express "who can do what." They build on each other:
| Model | Idea | Rule |
|---|---|---|
| Ownership / DAC | each resource records who can view it | the owner grants access |
| RBAC (roles) | users get roles; roles get permissions | a role bundles several rights |
| ABAC / policy | access by attributes of the request | cloud IAM, least-driven |
For most FDE work, RBAC plus ownership covers almost everything. Reach for ABAC only when you need finer rule logic, such as "allowed only between 9–5, and only for assets over $5k."
Every identity carries a set of attributes: who you are, the group you belong to, the roles you hold, perhaps the account you act for. Authentication binds these attributes to your identity; access control reads them back and decides. A permission check is really asking: "do the attributes of this identity satisfy this rule?"
You met this in Tutorial 1; here it is in access-control dress: grant the minimum permission that makes the action work. A viewer reads, an editor edits, an admin deletes. Separate, scoped permissions beat one wide god-permission, because a compromised small role stays small. Shrinking scope shrinks the blast radius of any leak.
Start with everything denied, then add allowances only where you need them. A hidden endpoint no one remembered to open is safe under default-deny; under default-allow it silently serves data. Default-deny turns "I forgot" into "no access," not "no leak."
Best Practice: every new endpoint and permission starts closed. Turn it on explicitly, then test it.
An access check that only lives inside your UI, or that one helper you forget to call somewhere, is easy to bypass. Enforce at the boundary: a single middleware that runs on every request before the handler. Then there is no unguarded path, and a missing check in one handler can't silently expose its data.
Role-based access control is the workhorse. Assign each user one or more roles, and each role bundles a set of permissions. Nobody gets a raw permission in a vacuum; they get a role that implies it. This makes granting ("promote Alice to admin") and revoking ("remove Bob from the team") a one-line change:
Alice → role: data_analyst → permissions: [read_reports, view_dashboard] Bob → role: team_admin → permissions: [read_reports, manage_users]
Your backend stores the user's roles and checks them at each handler. Usually a role gate and an ownership gate both run:
ROLES = {"alice": {"team_admin"}, "bob": {"data_analyst"}}
def export_report(user, report_id, ROLES, OWNERS):
if not ({"data_analyst", "team_admin"} & ROLES[user]): # role gate
raise PermissionError("role not allowed")
if user not in OWNERS.get(report_id, set()): # owner gate
raise PermissionError("not the owner")
return run_export(report_id)
Explanation: the request must pass both gates. The role gate admits analysts and admins; the ownership gate then guarantees the user is the owner. Two independent models that both can reject — that's access control done right, not a bug.
Two knobs you will set constantly:
archive permission."A teammate who can view a folder may still be blocked at the "delete" action. Combine the knobs to get "only Alice (owner) may delete her invoice" — the classic enterprise shape.
In Tutorial 1 you met IDOR (Insecure Direct Object Reference) — the leak where code trusts an id from the request. The access-control fix is the same, now written as a standing rule:
def get_invoice(requesting_user, invoice_id, OWNERS):
allowed = OWNERS.get(invoice_id, set())
if requesting_user not in allowed:
raise PermissionError("not an owner")
return fetch_invoice(invoice_id)
Why it must be a rule, not an afterthought: the moment any endpoint accepts an id and returns a record, someone with no gate can pass another person's id and read it. Make ownership the default in your data layer, and IDOR disappears.
Privilege escalation is an attacker gaining access they should not have: a normal user becoming the admin, or user A reading user B's data. It is almost always an authorization failure — a missing ownership check or an overly trusting role — not a clever technical exploit. This is the highest-severity class of bug in most business software.
Why it's invisible: the happy path works. Only the unhappy path (a caller passing an unowned id) reveals it. The earliest disaster in real apps is usually this and not the crypto. Red-team your endpoints: call them with a normal user's token against someone else's resource.
Never rely on a single gate. Layer them so that if one fails, the others stand: a DB user with only SELECT, a network rule that hides the DB, an endpoint that still checks ownership, and logs that record every request. "One wall" is a security smell — make layers the norm.
Every request that returns data passes these, in this order:
If any step stops, return 401 or 403. Keep steps 2 and 3 both — step 3 is what slays IDOR.
Keep the rule logic in one place (a policy module), not scattered across handlers. Handlers then declare their requirement ("requires ownership + archive"), and the shared gate enforces it. This is the practical way to guarantee enforce-at-the-boundary without repeating the same check thirty times.
Save your progress and earn XP for completing tutorials.
4 questions · Pass with 70%+
1Which approach grants "delete" via roles → permissions?
2Default-deny means…
3An endpoint returns any record by id with no owner check. This enables…
4When should you turn on ABAC/policy?
Technology
Forward Deployed Engineer
Lesson group
Security for FDEs
Progress
38% complete