Authorization

Reading Time: 4 minutes

Authorization determines which actions a user can perform on an application

There are a lot of alternatives which can help us solve this functionality issue, now I will show you the Pundit way (maybe the Pundit-Enums way).

The Pundit's paradigm

For Pundit, everything revolves around policies; as we already know, a policy is:

A statement of intent, and it is implemented as a procedure or protocol.

So, If I create a policy for a model inside an application, I will define its behavior (depending on the user’s permissions).

Let's see an example of this process....

We need defined rules

Our scenario is the following; a student community wants to create their own blogpost system and they need some roles to have better control of it. Luckily, they have a well-defined hierarchy, and they created only two roles: Writers and Moderators.

  • A Writer is able to:
    • Create, update and delete only their own posts.
    • Read all published posts and own posts.
  • A Moderator is able to:
    • Access published posts
    • They are not able to create, update or delete posts.
    • Create, update and delete new users

In this example, we are going to create only the Post Policy. You can apply these principles in order to create the other Policy.

Now, let's code.

rails new blog

Let's suppose that we are at this point of the project
So, we have already created:

  • User's table
  • Post table
  • Devise as user's authentication.

Note: If you want to see how I get at this point. Feel free to visit my repo

Add role to users

Note: If you already have a user-role association in your project, skip this part. I will just show you a fast alternative.

For this, we can use an enum to represent the user’s role. To do this we have to:

  1. Create a migration and add this column to the user's table
    • rails g migration AddRoleToUsers role:integer
    • Don't forget run your migration
  2. Define the enum in the user's model
class User < ApplicationRecord
  has_many :posts
  enum role: %w[writer moderator]
  # ...
end

Now we are able to use some ‘fancy’ methods, such as user.writer?or user.moderator?, and get a boolean value. This makes everything way easier.

Set up Pundit.

1. Installation

# Gemfile
#...
gem 'pundit'
#...

Run this in order to create your parent class. This will contain all the denied permissions.

rails g pundit:install

Now, If you do not define an action in a new policy, this will be denied. This scenario is better than allowing all the actions which have not been defined yet.

2. Create Post Policy

To create the Post Policy, we can use the generator that Pundit provides us with.

rails g pundit:policy post

Then we can define the Post permissions:

class PostPolicy < ApplicationPolicy
  def show?
    record.published? || record.user == user
  end

  def create?
    user.writer?
  end

  def update?
    user.writer? && record.user == user
  end

  def destroy?
    user.writer? && record.user == user
  end
end

user and record?

In Pundit, our object model is called record, and our current_user is called user. But if you want to change these names you will need to rewrite them in your constructor method. I do not recommend doing this. I prefer to keep this abstraction (it works for me). However,  I will leave you an example, in case you want to try this out:

class PostPolicy < ApplicationPolicy
  def initialize(user, record)
    @current_user = user
    @post = record
  end
  # ...
end

3. Apply Post restrictions in Controller

We have already defined the permissions, and now we have to apply this to the controller. We do it like this:

class PostsController < ApplicationController
  # ...
  def show
    authorize @post
  end
  #...
end

We have to add this method everywhere we want to apply the PostPolicy.

Why aren't we using before_action?

The authorize method automatically infers Post since we will have a matching PostPolicy class, and this instantiates this class, handling the current user and the given record. Then it infers from the action name, and in this case, it should call show? at this point of the policy.

4. Remove some actions from views

We already have correctly defined our permissions by applying the Pundit authorization from the controller, but we might want to remove some buttons or links from our views; maybe just to improve our interface or the user experience:

<% if policy(@post).create? %>
  <%= link_to "New post", new_post_path %>
<% end %>

5. Create subsets of data (Scopes)

Remember that we want to limit the data that a Writer can see in their Post index view.

  • A Writer can see all their posts as well as all previously published.
  • A Moderator can see all the posts.

Fine, so we have two different queries: Post.where(published: true).or(user.posts) and Post.all. We can put this logic into the index method, but I guess it would be better to delegate it to our Post Policy. Because if we do this, we will be able to use this logic anywhere else on the app.

To accomplish this task, we only have to define the scope in our Post Policy:

class PostPolicy < ApplicationPolicy
  # ...
  class Scope < Scope
    def resolve
      return scope.where(published: true).or(user.posts) if user.writer?
      return scope.all if user.moderator?
    end
  end
  #...
end

 

class PostsController < ApplicationController
  # ...
  def index
    @posts = policy_scope(Post.all)
  end
  #...
end

And that's all!!!

Add a useful Test Matcher

I'm fan of TDD , and If you visit this example you will see which matcher I used. To get fancy single line tests with RSpec, like this:

# ...
context 'when user has writer role' do
  let(:role) { :writer }

  it { should authorize(:create) }

  context 'when the current user is the post owner' do
    it { should authorize(:edit) }
    it { should authorize(:destroy) }
  end

  context 'when the current user is not the post owner' do
    let(:post) { create(:post, user: other_user) }

    it { should_not authorize(:edit) }
    it { should_not authorize(:destroy) }
  end
end

context 'when user has moderator role' do
  let(:role) { :moderator }

  it { should_not authorize(:create) }
  it { should_not authorize(:edit) }
  it { should_not authorize(:destroy) }
end
# ...

Well, Why should we test this logic?

Because we can ensure that our code actually works, and we can easily get the documentation about our project like this:
example

If you enjoyed reading this blog, you may want to follow us on Twitter, so you have access to more content like this @weAreMagmalabs.

Thanks for reading my blogpost, and let me know your comments (if any).

0 Shares:
You May Also Like