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
Generate the migrations
rails g rabarber:roles users
Migrate the database
rails db:migrate
Now include the roles module to the user model
class User < ApplicationRecord
include Rabarber::HasRoles
# ...
end
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
User.first.assign_roles(:super_admin, :admin)
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
Install action policy with
rails generate action_policy:install
Let’s also generate a policy for the startup model
rails g action_policy:policy admin/startup
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
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
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)