Authorization Checklist
What we built
This checklist covers every authorization concept from the course.
Roles
- Roles are defined per-organization membership, not per-user
- Role hierarchy is enforced (owner > editor > viewer)
- Built-in roles have hardcoded permissions that cannot be overridden
- Custom roles can be created per-organization
Permissions
- Permissions follow
resource:actionnaming (notes:read,notes:create) - Each role maps to a specific set of permissions
- Route handlers check specific permissions, not roles
- New permissions can be added without changing the role-checking code
Organization scoping
- Every data query includes
org_idin the WHERE clause - Resource lookups include both the resource ID and the
org_id - Non-members receive 404, not 403 (to avoid confirming the org exists)
- Users can switch between organizations they belong to
Invites
- Only users with
members:invitecan create invites - Invited role cannot exceed the inviter’s role (no privilege escalation)
- Invites are validated against the accepting user’s email
- Invites are single-use (deleted after acceptance)
- Duplicate invites are prevented (UNIQUE constraint)
API keys
- Keys are hashed (SHA-256) before storage
- Keys are shown once at creation and cannot be retrieved later
- Keys are bound to a specific user and organization
- Keys have scoped permissions (subset of the user’s role permissions)
- Key scopes are validated at creation (cannot exceed the user’s permissions)
Policy
- Authorization logic is centralized in a policy function
- The policy function handles special cases (e.g., creators can edit own resources)
- Route handlers call the policy, not raw permission checks
Audit
- Every authorization decision is logged (allowed and denied)
- Membership changes are logged (created, role changed, removed)
- API key lifecycle is logged (created, revoked)
- Logs include userId, orgId, action, and result
Common mistakes
Checking roles instead of permissions. requireRole("editor") tells you the role but not what the route actually needs. requirePermission("notes:create") is explicit and survives role changes.
Forgetting org scoping on one query. Every query needs org_id. One forgotten query leaks data across tenants. Use query helper functions to make this hard to forget.
Allowing role escalation via invites. An editor who can invite an owner has effectively become an owner. Always check that the invited role is at or below the inviter’s role.
Not scoping API keys. A key with full permissions is a stolen-password-equivalent. Always require scopes and default to the narrowest set needed.
Trusting the client to specify the org. The org should come from the URL path (which the server controls via routing) or the session’s active org (which the server validates). Never accept an org ID from the request body without membership validation.
What we did not build
Attribute-Based Access Control (ABAC). Instead of checking roles or permissions, ABAC evaluates policies based on attributes: “If the user’s department is Engineering AND the resource is tagged ‘engineering-only’ AND it is a weekday, allow access.” ABAC is more flexible than RBAC but more complex to implement and reason about.
External policy engines. Tools like Open Policy Agent (OPA), Cerbos, or Permit.io let you define and evaluate authorization policies outside your application code. They provide a policy language, a decision API, and an admin UI. Useful for complex organizations with many roles and resources.
Relationship-based access (ReBAC). Tools like SpiceDB and Authzed model authorization as a graph of relationships: “User A is a member of Org B, Org B owns Resource C, therefore User A can access Resource C.” This scales to complex hierarchies.
These are real production tools, but understanding RBAC and permission-based access (which this course taught) is the foundation they all build on.
Exercises
Exercise 1: Go through the checklist. How many items does your implementation cover?
Exercise 2: Research OPA (Open Policy Agent). How would you integrate it with your Hectoday HTTP app? (Hint: OPA provides a REST API that your policy function would call instead of checking permissions locally.)
What is the biggest risk of not scoping API keys?