Authorization

Ruby on Rails Intermediate/Advanced — Authorization
Ruby on Rails Intermediate/Advanced
Course 2 · Chapter 2 · Authorization

🚧 Authorization

Chapter 1 answered who is making a request. This chapter answers a genuinely different question: what are they allowed to do? Authentication and authorization are easy to conflate but solve separate problems — a logged-in user (authenticated) still shouldn't be able to edit someone else's post (unauthorized).

🏷️ The Simplest Approach: A Role Column

$ rails generate migration AddRoleToUsers role:integer
# app/models/user.rb class User < ApplicationRecord has_secure_password enum role: { member: 0, admin: 1 } end current_user.admin? # => true/false -- enum generates this predicate method automatically

enum stores role as a plain integer in the database but gives you named, readable methods (admin?, member?) instead of comparing raw numbers everywhere — a small taste of Rails' broader convention-over-configuration habit applied to a single column. This is enough for the simplest cases:

# app/controllers/posts_controller.rb def destroy unless current_user.admin? redirect_to posts_path, alert: "Not authorized" return end # ... end
⚠ This gets unmanageable fast, past the simplest cases

The moment authorization rules get more nuanced than "admin or not" — "the post's own author OR an admin can edit it," "a moderator can hide comments but not delete posts" — hand-written unless checks scattered across every controller action become genuinely hard to keep consistent, and easy to forget in a new action entirely. This is the exact same "duplicated across every entry point" problem Chapter 1 Course 1's validations chapter solved by centralizing rules on the model. Authorization needs its own centralizing pattern too.

📜 Pundit — Policy Objects

$ bundle add pundit
# app/policies/post_policy.rb class PostPolicy < ApplicationPolicy def update? user.admin? || record.user == user # record is the Post being checked end def destroy? user.admin? # only admins can delete, regardless of authorship end end

Pundit's convention: one policy class per model (PostPolicy for Post, matching the same naming pattern controllers and models already follow), with a method per action ending in ? — the same predicate-method naming convention from both Ruby courses. Every rule for a given model lives in exactly one file.

# app/controllers/posts_controller.rb class PostsController < ApplicationController def update @post = Post.find(params[:id]) authorize @post # raises Pundit::NotAuthorizedError if PostPolicy#update? returns false # ... end end # app/controllers/application_controller.rb class ApplicationController < ActionController::Base include Pundit::Authorization rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized private def user_not_authorized redirect_to root_path, alert: "You're not authorized to do that" end end

authorize @post automatically looks up PostPolicy (from the object's class name) and calls the method matching the current action name — update calls update?. rescue_from is Rails' framework-wide exception handler declaration, catching the raised error anywhere in the app and redirecting gracefully instead of showing a raw error page.

🔍 Checking Permissions in Views

<% if policy(@post).update? %> <%= link_to "Edit", edit_post_path(@post) %> <% end %>

policy(@post).update? reuses the exact same PostPolicy logic to conditionally show the edit link — no separate, potentially-inconsistent permission check duplicated in the view. Contrast this with Express, where a role check to conditionally render a button in an EJS template is typically a separate, hand-written if statement with no guaranteed relationship to whatever check the corresponding route handler actually performs.

🆚 Pundit vs CanCanCan

Pundit — one policy per model

Rules for Post live in PostPolicy, rules for Comment in CommentPolicy. Fits naturally alongside Rails' existing one-class-per-concern convention (one controller per resource, one model per table).

CanCanCan — one Ability class, everything

A single Ability class defines every permission for every model in one place, using can :update, Post, user_id: user.id-style declarations. Convenient for seeing the whole permission system at a glance; can become one large, sprawling file as an app grows.

Both are well-established, widely-used gems — this course continues with Pundit specifically because its file-per-model split matches the organizational convention already established for controllers, models, and (in the next chapter) test files.

📜 Authorization, Side by Side

Express (manual) vs Rails (Pundit)

ConcernExpress (typical)Rails + Pundit
Where rules liveScattered if-checks per route, or ad hoc middlewareOne policy class per model
Controller checkHand-written conditional per routeauthorize @record — one line, consistent everywhere
View-level checkSeparate, potentially inconsistent logicpolicy(@record).action? — same source of truth
Unauthorized handlingManual per routerescue_from, handled once, app-wide

💻 Coding Challenges

Challenge 1: A Role Column

Write the migration and model changes to add a role enum (member/admin) to User. Then write a destroy action on a CommentsController that only allows admins to delete a comment, redirecting with an alert otherwise.

Goal: Build the simplest possible role-based check before reaching for a gem.

→ Solution

Challenge 2: A Pundit Policy

Write a CommentPolicy where update? allows the comment's own author or an admin, and destroy? allows the comment's author, the post's author, or an admin (assume record.post.user gives you the post's author). Then write the authorize @comment call in the controller's update action.

Goal: Practice writing policy logic more nuanced than a single role check.

→ Solution

Challenge 3: Conditional View Links

Using the CommentPolicy from Challenge 2, write the ERB for a comment partial that only shows "Edit" and "Delete" links when policy(comment) permits each action respectively.

Goal: Reuse policy logic in a view instead of duplicating the permission check.

→ Solution

💎 The Same Centralization Lesson, Applied Again

This chapter is really Course 1's validations chapter, replayed for a different concern: rules written once, in one predictable location, apply consistently everywhere they're needed — the controller, the view, anywhere else the app might eventually check permissions. The specific mechanism differs (validates on a model vs a separate policy class), but the underlying principle — one source of truth beats duplicated logic — is the same idea this course keeps returning to.

🎯 What's Next

Next chapter: Testing Rails Apps — RSpec request and model specs, and FactoryBot, building directly on Ruby Course 2's RSpec chapter to test everything covered so far, including this chapter's policies.