DEV Community

Cover image for Securing Rails application with Action Policy
Given Ncube
Given Ncube

Posted on • Originally published at givenis.me

Securing Rails application with Action Policy

There is lot of yapping going on in this article if you just want to see the implementation you can jump to the Setup section

Recently, when I was building Pulse, I wanted an admin dashboard of sorts, I wanted to be able to manually create startups other users can then claim later, I also wanted to see a list of registered users, some basic stats, etc.

The problem now, I didn’t want every Joe and Jill to access the admin dashboard and do whatever they want simply because they registered an account.

To solve this, I did a deep dive, found solutions like cancan and it’s derivatives, and a bunch of other gems. However I wanted a setup that was a bit automatic that I could setup once and subsequently use and work out of the box without me writing extra code.

I guess you’re wondering, Gavin, why did you roll out your own dashboard when there are dashboard gems out there? Well, I tried, but I found that the amount of customization I’ll have to make requires me to write more code than just generating a scaffold_controller in the admin namespace.

Okay back to adding authorization, here is what I was looking to achieve

  • Users need to have roles,

  • Roles have permissions (still working on this)

  • Authorize controllers via Policy

I found 2 really good gems for this, ActionPolicy and Rabarber. ActionPolicy allowed me to write policies, and in those policies I will then decide if the user has a certain role before they can perform a given action.

Setup

To set this let’s start by installing Rabarber to add roles to the users

bundle add rabarber
Enter fullscreen mode Exit fullscreen mode

Generate the migrations

rails g rabarber:roles users
Enter fullscreen mode Exit fullscreen mode

Migrate the database

rails db:migrate
Enter fullscreen mode Exit fullscreen mode

Now include the roles module to the user model

class User < ApplicationRecord
  include Rabarber::HasRoles
  # ...
end
Enter fullscreen mode Exit fullscreen mode

Finally, let’s assign some roles to our users, I was lazy to build a UI for this so we’ll just use the rails console, I’m going to be the only admin for a while anyway and I will only do this once in production so no big deal

rails c
Enter fullscreen mode Exit fullscreen mode
User.first.assign_roles(:super_admin, :admin)
Enter fullscreen mode Exit fullscreen mode

And that’s it, this all we have to do to get this running. Now, let’s add action policy to add authorization to the controllers

bundle add action_policy
Enter fullscreen mode Exit fullscreen mode

Install action policy with

rails generate action_policy:install
Enter fullscreen mode Exit fullscreen mode

Let’s also generate a policy for the startup model

rails g action_policy:policy admin/startup
Enter fullscreen mode Exit fullscreen mode

Authorization with policies

My project structure looks like this, in the root controller directory I have unauthenticated controllers for public access which inherit from the default application_controller.rb I have controller in controllers/app namespace that inherit from app/application_controller.rb, finally controllers/admin with controllers that inherit from admin/application_controller.rb

In the app namespace I just call before_action :authenticate_user! in the application controller and I don’t have to do it ever again in inheriting controllers. Same with the admin namespace

From action policy docs, we have to authorize! on every controller action, which I was trying to get away from.

In the application_policy.rb I added this, to give basic access to anyone with role admin then in the specific policies I will then give access based that role and another role. For example if you’re admin you can access the dashboard, but you have to be admin + moderator to update startups

# app/policies/application_policy
class ApplicationPolicy < ActionPolicy::Base
  default_rule :manage?
  alias_rule :index?, :create?, :new?, to: :manage?

  def manage?
    user.has_role? :admin
  end

  private

  def owner?
    record.user_id == user.id
  end
end
Enter fullscreen mode Exit fullscreen mode

With this, if i don’t have any complex authorizations, I could just generate a policy that inherits application_policy and everything would work out of the box without adding extra code.

Okay, this alone will not work, we need to tell the admin/application_controller to automatically authorize all controllers based on the controller name, find a policy for that controller and use it to authorize the current controller. As long as we follow rails conventions this should work out of the box

# app/controllers/admin/application_controller.rb
class Admin::ApplicationController < ActionController::Base
  before_action :authenticate_user!
  before_action :authorize!
  verify_authorized
  layout "admin/application"

  def implicit_authorization_target
    # If you don't pass the target, it will be guessed
    # based on the controller name.
    # See https://actionpolicy.evilmartians.io/#/implicit_target
    super || controller_name.classify.to_sym
  end

  def authorization_strict_namespace
    true
  end
end
Enter fullscreen mode Exit fullscreen mode

First we authenticate the user, this is standard. Next, I added a before action to call :authorize so that I don’t to do this for every other action in the controllers. The docs say

You can also call authorize! without a resource specified. In that case, Action Policy tries to infer the resource class from the controller name

Then finally we added the implicit_authorization_target that first calls the parent method and if that returns nil we then use the current class name to find the policy.

Finally we enable strict namespaces. This allows us to have scoped policies, for example, policies in app/policies/admin/* will only authorize controllers in app/controllers/admin/* and so forth,

We can have other policies for non admin authenticated actions, perhaps we want an authenticated user to only be able to create startups only if they have certain roles and we don’t want that policy to affect the admin policy.

That’s it we are good to go, at this point authorization now works out of the box! The next thing would be to add custom pages for 401 errors.

Let me know what you think about this pattern.

Top comments (0)