DEV Community

Cover image for Rails Authorization Beyond Models: Securing Dashboards and Service Controllers with CanCanCan
Nemwel Boniface
Nemwel Boniface

Posted on

Rails Authorization Beyond Models: Securing Dashboards and Service Controllers with CanCanCan

Hello friends! I’m back with more Ruby on Rails concepts I refreshed this week to stay productive and avoid the social media "doom scroll." 2026 is the year we kill the bad habits hindering our potential, and for me, that means trading the doom-scroll for the docs.

Welcome to A Guide to Rails Authorization Beyond Models: Securing Dashboards and Service Controllers with CanCanCan

Introduction - Authentication and Authorization. What are these?

Authentication and Authorization. What are these?

Authentication and authorization are often confused, and many people think they are the same, but they are not. Similarly, their ease of implementation is often confused. You might think they are easy to integrate. But are they really? Let me explain.

Authentication, which is verifying who you are, is the easy part. A quick Devise gem install and a migration, and your system knows its users.

Authorization, which decides what they can do, is where the mess begins. Most of us start with CanCanCan basics: you add load_and_authorize_resource to a PostsController, and magic happens. Admins edit, users read. Life is good.

But what happens when you have a controller that doesn't map neatly to a database table? Think about a DashboardController or a SearchController. There is no "Dashboard" table in your database.

Suddenly, the magic breaks and reality sinks in. You start getting confusing error messages or, even worse, silent security gaps that only appear once a user accidentally accesses classified data.

In this article, we’ll explore how to handle authorization when you need to step outside the standard model framework, specifically using tools like authorize_resource class: false to keep your "non-model" controllers secure.

Lets begin A Guide to Rails Authorization Beyond Models: Securing Dashboards and Service Controllers with CanCanCan

The Foundation: The Rulebook (Ability.rb)

Before we touch our controllers, we have to understand the Source of Truth. In CanCanCan, your app/models/ability.rb file is the rulebook. Everything we do in the rest of the app is just asking this one file a single question: "Is this user allowed to do X to Y?". Let me show you the anatomy of one below:

class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new # guest user

    if user.admin?
      can :manage, :all
    else
      # Scenario A: Database Model Permission
      # Users can manage their own posts
      can :manage, Post, user_id: user.id

      # Scenario B: Abstract Permission
      # Users can read the dashboard, even though there is no Dashboard model
      can :read, :dashboard 
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Pay close attention to Scenario B. Notice how we defined that permission using a symbol (:dashboard) instead of a class name like Post. In Rails, we are used to everything being linked to a database table. But here, we are telling the rulebook that "Dashboard" is just a concept, a place the user can go. Understanding this shift from Classes to Symbols is the key to solving our future "Model Not Found" headaches.

The "Secure by Default" Mindset

Before we look at specific tools, we need to set a baseline. In a professional application, you shouldn't have to remember to add security; security should be there by default. You should only have to turn it off when you explicitly mean to, in very rare situations. In your ApplicationController, I recommend adding this:

class ApplicationController < ActionController::Base
  # Throw an error if a controller action runs without checking authorization first.
  check_authorization unless: :devise_controller?
end
Enter fullscreen mode Exit fullscreen mode

Think of this as your Authorization Alarm System. If you create a new controller and forget to define any permissions, Rails will yell at you with a CanCan::AuthorizationNotPerformed error, which is much better to have your app crash during development because you forgot a security check than to have it silently expose private data in production. We skip devise_controller? because Devise handles its own "Who are you?" logic, and we don't want to get locked out of the login page!

Now that the alarm system is armed, let me show you how we satisfy it in different scenarios below:

Scenario 1: The Standard CRUD Resource (This is the Happy Path)

Imagine you have a Product model and a standard ProductsController to handle the basics: looking at a list, viewing a single item, or editing details. The Solution would be to add the load_and_authorize_resource at the top of the controller. This will then tell CanCanCan: _"Hey, look at the controller name (Products), find the matching model (Product), load the data into a variable (like@product`), and check the Rulebook to see if this user is allowed to be here."_

You are to use this ideally 90% of the time if your controller maps directly to a database table and performs standard RESTful actions. Example:

The Standard CRUD Resource

The beauty here is Clarity. By moving the data loading and the security check to a single line at the top, your controller actions stay "pure." They only focus on what happens after the door has been opened.

Scenario 2: The "Service" Controller (When there is no Database Model)

This is where the confusion usually starts. You have a controller that performs a service-like a DashboardsController, a SearchController, or a ReportsController. If you check your database, you will find that there is no "Dashboards" table. Dashboard here is just a page that aggregates data from other places.

If you try to use ur favourite load_and_authorize_resource here, CanCanCan will go looking for a Dashboard model, fail to find it, and crash your app because it does not exist. The solution is using authorize_resource class: false.

This tells CanCanCan: "I need to secure this controller, but stop looking for a matching database class. Just check if the user is allowed to access the concept of this controller.". When you set class: false, CanCanCan converts the controller name into a symbol (e.g., DashboardsController becomes :dashboard) and checks your Rulebook for that specific symbol. This is best used whenever your controller represents a concept, a process, or a service rather than a specific row in a database table.
Example (Recall our Ability.rb had can :read, :dashboard.):

The

If a user tries to access this page without the can :read, :dashboard rule in their ability.rb, they’ll get an "Access Denied" error. With this, you get the same level of security as a standard model, but without the database baggage.

Scenario 3: The Complex or Expensive Action

Sometimes, checking permissions at the very start of a request is too "blunt". Imagine you have an action that kicks off a heavy background job or calls an external API that charges you per request. You ideally don't want to waste any CPU cycles or money until you are 100% sure the user has the right to be there. The solution is the explicit inline authorize!

This is the manual approach, and instead of letting the controller handle security automatically at the top, you take the wheel and tell CanCanCan exactly when and what to check inside the method itself.

Situations when you are supposed to use this include:

  1. When your logic is too dynamic for the standard helpers.
  2. When you need to do some "cheap" setup (like checking params) before doing an "expensive" authorization check.
  3. When your action handles multiple different types of objects at once.

Example usage:

The Complex or Expensive Action

By using authorize! manually, you ensure that your expensive code is protected by a high-security gate. If the user isn't authorized, the method stops right there and raises an exception before the HeavyReportingJob ever gets queued.

Summary and a useful Cheat Sheet

Choosing the right tool comes down to two questions: "Does this match a database table?" and "When do I need the check to happen?"

Scenario Mapped to DB? Tool to Use The "Why"
Standard RESTful (Posts, Users) Yes load_and_authorize_resource The magic bullet. Loads data and checks rules automatically.
Service/Concept (Dashboard, Search) No authorize_resource class: false Authorizes against a symbol so Rails doesn't look for a missing class.
Complex/Expensive Actions Maybe Explicit authorize! in the action Gives you total control. Best for guarding expensive API calls or background jobs.

Now the different approaches to authorisation start to make sense

Conclusion

Doom-scrolling gives you a dopamine hit that lasts five seconds. Solving a tricky authorization bug gives you a secure, professional application that stays protected while you sleep.

By moving past the basic "magic" and mastering tools like class: false and explicit authorize! calls, you stop fighting the framework and start making it work for your specific needs. In 2026, we aren't just building apps that "work"; we are building apps that are secure, intentional, and architected for the long haul.

Next time Rails "yells" at you about a missing model, don't reach for your phone to scroll. Reach for your ability.rb and check if you're trying to authorize a class that doesn't exist.

Do you prefer the "magic" of automated authorization, or are you a "manual control" developer who likes to see every security gate in your code? I’d love to hear your horror stories about AccessDenied errors in the comments!

I will be back with more technical articles

A Quick Side Note: While I’m heads-down building this search engine, I’m also looking to pitch in on new projects. If you’re looking for a mid-level Rails engineer with solid JavaScript experience who prioritizes security, authorization logic, and building defensive, scalable systems, I'd love to chat. You can find me on [LinkedIn/Twitter] or check out my other work here.

I hope this was useful information for you. See you in my next article.

Top comments (0)