ruby

Using Action Policy for a Ruby on Rails App: The Basics

Aestimo Kirina

Aestimo Kirina on

Using Action Policy for a Ruby on Rails App: The Basics

To keep your app secure, you need to control who and what can access it. Access control can be categorized into authentication — "who" to allow — and authorization — "what" they can access.

Authentication is a subject for another day, but when it comes to user authorization, you generally have two ways to go about it: using a role-based or resource-based strategy.

In this two-part series, we'll take a deep dive into using the Action Policy gem for a Ruby on Rails blog application.

In this part, we'll cover the basics of Action Policy.

Let's get started!

Prerequisites

  • Ruby (we're using version 3.2.2)
  • Rails (using version 7.0.7)
  • Some experience using Ruby

Let's get into it by first defining resource-based authorization.

What Is Resource-Based Authorization?

Where role-based authorization focuses on setting user permissions according to predefined user roles, resource-based authorization enforces access by setting rules on the actual resources within an application. Each resource is associated with a policy that explicitly defines what a user can do on that resource.

Even though this article is focused on resource-based authorization, knowing the differences between the two authorization strategies means you are better equipped to know what each can do.

When to Use Role-Based Authorization

You should use role-based authorization for:

  • Simple applications - If you're working on an app with a straightforward permissions system and fewer user roles, then a role-based authorization strategy could work for you.
  • Well-defined user groups - If your app has well-defined user groups such as "admins", "editors", "writers", and so forth, use role-based authorization as it handles access control at the user role level.

When to Use Resource-Based Authorization

Resource-based authorization is great for:

  • Dynamic or complex access control - When your application's authorization needs evolve frequently or are determined by dynamic conditions, then a resource-based authorization system makes for a better choice.
  • Fine-grained control - Useful for when you need to allow or deny access to resources based on multiple conditions (for example, a Rails helpdesk app where users submit support tickets based on a list of dynamic categories). Assuming support staff are assigned tickets by category, this is a scenario where resource-based authorization would really shine.
  • Object-oriented control - Because resource-based authorization happens at the object level, defining complex object-oriented rules is easier when you use resource-based authorization techniques.

That said, whether you choose between role-based and resource-based authorization will depend on your application's unique characteristics.

Let's now turn our attention to the Action Policy gem.

The Action Policy Gem for Ruby and Rails

Action Policy is a flexible, extensible, and performant authorization framework for Ruby and Rails apps. It uses multiple caching strategies out of the box, making it very fast, especially if your authorization rules require database queries.

Another feature that makes this gem ideal for building resource-based rules is its ability to be customized. It provides several Ruby classes and modules that can be combined in many ways. You can pretty much set fine-grained access control rules beyond Rails controller, anywhere in your app.

Check out the Action Policy project homepage to learn more.

Now let's go over the Rails app we'll be building today.

Create the Rails App

Moving forward, we'll reference a Rails 7 blog application where users can Create, Read, Update, and Delete (CRUD) posts.

We'll progressively define an action policy for the Post model that offers resource-based access control. You'll learn how to use Action Policy to make this access control strategy work.

Go ahead and generate a new Rails application:

shell
rails new blog_app

Note: We'll use Pico CSS styles for our example app, but you can use whatever you like.

Then quickly scaffold a Post resource that we'll use throughout the rest of the tutorial:

shell
rails g scaffold Post title body

Now run the migration with rails db:migrate.

Setting up User Authentication

As much as this article is about user authorization, there's something important we need to cover: user authentication. Without it, any authorization policies we try to define later on will be useless. But there is no need to write authentication from scratch. Let's use Devise.

Installing Devise

Begin by adding the Devise gem to your Gemfile:

ruby
# Gemfile gem "devise"

Then install it and generate a User model:

shell
bundle install rails generate devise:install rails generate devise User rails db:create rails db:migrate

Also, remember to add config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } to the app's development configuration.

Next up, let's implement a few basic user roles to test different user access scenarios for the Post resource.

Defining Basic User Roles

First, add the role column to the user table. Run this command to generate a migration:

shell
rails g migration AddColumnRoleToUser role:integer

Note: We use an integer data type for the role, so we can use an enum — a quick and easy way to implement roles.

Now run the migration with rails db:migrate, then open up the User model and edit it:

ruby
# app/models/users.rb class User < ApplicationRecord devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable enum :role, { reader: 0, author: 1, editor: 2 } end

With that done, let's shift our focus to installing Action Policy.

Setting up Action Policy for Ruby and Rails

Open the app's Gemfile and add the line below:

ruby
# Gemfile gem "action_policy"

Then run the command bundle install to install Action Policy.

Finalize the installation by running:

shell
rails g action_policy:install

This should give us a base — the ApplicationPolicy class under app/policies:

ruby
# app/policies/application_policy.rb class ApplicationPolicy < ActionPolicy::Base end

Basic Usage of Action Policy

Action Policy's core foundation is a policy class, ApplicationPolicy, where you can define global configurations from which all policies can inherit. A good recommendation is to place all policies under app/policies, and to separate policies according to the resources in your app. For example, if you have a Post resource, the corresponding policy should be PostPolicy, CommentPolicy would accompany a Comment resource, and so forth.

One more thing to consider when working with Action Policy: rule definitions must happen within public methods in policy classes. Using a private method will raise an error.

Moving on, let's use this information to create a PostPolicy where we'll progressively build various levels of user access.

Creating Our First Policy

Create a new file named post_policy.rb under app/policies:

ruby
# app/policies/post_policy.rb class PostPolicy < ApplicationPolicy def show? true end end

Here, we define a simple rule on the Post resource declaring that anyone can access a Post.

Associating Posts to Users

To work with this new post policy, we need to modify the Post resource we generated earlier so that it's associated with a logged-in user on creation (by adding a user_id column to the posts table):

shell
rails g migration AddColumnUserIDToPost user_id:integer

Then run the migration with rails db:migrate.

Next, we need to modify the posts controller to account for this change. First, let's add user_id to the allowed post_params:

ruby
# app/controllers/posts_controller.rb class PostsController < ApplicationController ... private def post_params params.require(:post).permit(:title, :body, :user_id) end end

Then modify the create action:

ruby
# app/controllers/posts_controller.rb class PostsController < ApplicationController ... def create @post = Post.new(post_params) @post.user_id = current_user.id # Devise gives us the logged in user as a helper method `current_user` respond_to do |format| if @post.save ... end end end ... end

Next, let's secure the PostsController.

Implementing Authorization in Controllers

We need to modify the PostsController with a callback to only allow access to logged-in users:

ruby
# app/controllers/posts_controller.rb class PostsController < ApplicationController before_action :authenticate_user!, except: :show ... end

Next, let's use the new Post policy to add some basic user access controls:

ruby
# app/policies/post_policy.rb class PostPolicy < ApplicationPolicy ... def update? # post can only be updated by an author role or the post's author user.author? || (user.id == record.user_id) end def destroy? # a post can only be deleted by its author user.id == record.user_id end end

Here, we define an access rule on the posts controller's update and destroy actions. We specify that a post's author (or a user with the "author" role) can update a post, but only a post's author can delete a post.

Quick tip: Action Policy can reference the current_user provided by Devise and assign it to a user within the policy.

Next, we need to use the access rule in the posts controller like so:

ruby
# app/controllers/posts_controller.rb class PostsController < ApplicationController before_action :set_post, only: %i[ show edit update destroy ] ... def update authorize! @post # Add this line respond_to do |format| if @post.update(post_params) ... end end end def destroy authorize! @post # Add this line @post.destroy ... end ... end

With the access rule applied to the posts controller, our next course of action is to ensure the views are protected.

Protecting Views with Authorization

In the screenshot below, a user with the email <user2@example.com> and "reader" role is logged in. As you can see, this user can view the post created by <user@example.com>, and they even have access to the edit and delete links for this post. This is not ideal — the edit and delete links should only be available to the post's author.

User 2 access

Our first task will be to remove access to these links for users that aren't authors of a post. Since we've already defined this access rule, we only need to apply it to the show view using Action Policy's nifty allowed_to? method:

erb
<!-- app/views/posts/show.html.erb --> <%= render @post %> <div> <% if allowed_to?(:update?, @post) %> <%= link_to "Edit this post", edit_post_path(@post) %> <% end %> <% if allowed_to?(:destroy?, @post) %> <%= button_to "Destroy this post", @post, method: :delete %> <% end %> <%= link_to "Back to posts", posts_path %> </div>

With this rule applied, we can now refresh the post's show page and see what we get:

User 2 access checked

The same user who had access is now limited in what they can do while on a post's view.

Wrapping Up

In the first part of this two-part series, we've learned some of the basics around the authorization gem Action Policy.

In the second and final part, we'll explore some more advanced use cases.

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!

Aestimo Kirina

Aestimo Kirina

Our guest author Aestimo is a full-stack developer, tech writer/author and SaaS entrepreneur.

All articles by Aestimo Kirina

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps