Advanced Usages of Action Policy for Ruby on Rails

iamaestimo

Aestimo K.

Posted on October 25, 2023

Advanced Usages of Action Policy for Ruby on Rails

In part one of this series, we looked at some basic usages of Action Policy. Now we'll leverage Action Policy for more advanced authorization use cases.

First up, let's explore applying pre-checks.

Pre-checks

Let's say we want users with "editor" status to have access to a post's show, update, and destroy actions, like so:

# app/policies/posts_policy.rb

class PostPolicy < ApplicationPolicy
    def show?
      user.editor || user.reader || user.id == record.user_id
    end

    def update?
      user.editor || (user.id == record.user_id)
    end

    def destroy?
      user.editor || user.id == record.user_id
    end
end
Enter fullscreen mode Exit fullscreen mode

We can refactor this policy code using an Action Policy pre-check that extracts common rules into pre-checks:

# app/policies/posts_policy.rb

class PostPolicy < ApplicationPolicy
  pre-check :allow_editors

  def show?
    user.reader || user.id == record.user_id
  end

  def update?
    user.id == record.user_id
  end

  def destroy?
    user.id == record.user_id
  end

  private

  def allow_editors
    allow! if user.editor
  end
end
Enter fullscreen mode Exit fullscreen mode

Scopes

Another Action Policy feature that deserves our attention is scoping. Scopes filter data depending on any authorization rules you've set.

Using our blog app as an example, let's say we want to apply the following rules to the posts index action:

  • List all posts for all users with the "editor" role
  • List only posts that a user has created if they have the "author" role

Without using any Action Policy authorization, the posts controller index action looks like any generic index action:

# app/controllers/posts_controller.rb

class PostsController < ApplicationController
  ...

  def index
    @posts = Post.all
  end

  ...
end
Enter fullscreen mode Exit fullscreen mode

We'll need to utilize Action Policy scoping to refactor this action and apply the outlined access rules. Scoping rules are defined within a policy class and applied in the respective controller using the authorized_scope or authorized methods.

First modify the Post policy, like so:

# app/policies/posts_policy.rb

class PostPolicy < ApplicationPolicy
  ...

  relation_scope do |scope|
      if user.editor?
        scope
      end
    end
  end

  relation_scope(:own) do |scope|
      scope.where(user: user)
    end
  end

  ...
end
Enter fullscreen mode Exit fullscreen mode

Then the posts controller's index action, as shown below:

# app/controllers/posts_controller.rb

class PostsController < ApplicationController

  ...

  def index
    @posts = authorized_scope(Post, type: :relation, as: :own)
  end
  ...

end
Enter fullscreen mode Exit fullscreen mode

Caching in Action Policy for Ruby and Rails

Action Policy is a performant authorization library, partly thanks to its efficient use of caching.

It has several cache layers for you to leverage, including memoization and external caching.

Memoization

Consider a situation where the same rule is called several times on an object instance. For example, let's say we need to load all comments associated with a post within the index action:

# app/controllers/posts_controller.rb

class PostsController < ApplicationController

  ...

  def index
    @posts = authorized_scope(Post.includes(comments), ...)
  end
  ...

end
Enter fullscreen mode Exit fullscreen mode

Now, imagine there's an "edit" link for every comment a post has within the index view. As you can see, this is a resource-intensive undertaking since we need to check if the current user is allowed to first access the post index, and then edit a post's comments. If a post has multiple comments, this would mean loading the authorization policy multiple times.

In a situation like this, Action Policy will re-use the required policy instance — specifically, the record.policy_cache_key — as one of the identifiers in the local store.

This kind of caching works well for short-lived requests, but if you need to cache resource-intensive rules that will be persisted across requests, using an external cache store is a better option.

Using an External Cache Store

If you need to run access control rules that utilize complex database queries, for example, you can use an external cache store such as Redis. Your rules cache will be made available across requests. You just need to remember to explicitly define which rules to cache within the policy class:

# app/policies/post_policy.rb

class PostPolicy < ApplicationPolicy
  cache :index?

  def show?
    # some complex database heavy rules...
  end

  ....
end
Enter fullscreen mode Exit fullscreen mode

And configure the cache store for your app:

# config/application.rb

Rails.application.configure do |config|
  config.action_policy.cache_store = :redis_cache_store
end
Enter fullscreen mode Exit fullscreen mode

Quick tip: There's a lot more to this. Dig into the Action Policy caching documentation for more information.

Aliases in Action Policy for Rails

An alias is an alternative way to name policies so that they make more sense.

Let's check out an example using our blog app. Consider this Post policy:

# app/policies/post_policy.rb

class PostPolicy < ApplicationPolicy
  alias_rule :destroy?, :update?, to: :manage_post?

  ...

  def manage_article?
    user.editor || user.id == record.user_id
  end

  ...
end
Enter fullscreen mode Exit fullscreen mode

Here, we combine the update? and destroy? rules into one alias called manage_post?.

Then we use it in the controller, like so:

# app/controllers/posts_controller.rb

class PostsController < ApplicationController

  ...

  def update
    authorize! :manage_post?, @post
    ...
  end

  # DELETE /posts/1 or /posts/1.json
  def destroy
    authorize! :manage_post?, @post
    ...
  end
  ...
end
Enter fullscreen mode Exit fullscreen mode

Finally, let's quickly look at how to handle unauthorized access.

Handling Unauthorized Access in a Ruby and Rails App

If a user tries to access a resource they shouldn't, an ActionPolicy::Unauthorized error is raised.

You need to explicitly handle this error in your app's ApplicationController, like so:

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  rescue_from ActionPolicy::Unauthorized do
    redirect_to root_path, alert: 'Access denied.'
  end
end
Enter fullscreen mode Exit fullscreen mode

And that's it!

Wrapping Up

In this post, we explored advanced Action Policy features, including pre-checks, scopes, caching, aliases, and finally, handling unauthorized access.

From basic rules to complex conditional scenarios, Action Policy has everything you need to handle almost any authorization scenario. Use this library in your next project and see how powerful it is.

Happy coding!

P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!

💖 💪 🙅 🚩
iamaestimo
Aestimo K.

Posted on October 25, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related