Using Action Policy for a Ruby on Rails App: The Basics
Tags: ruby, rails
Published: 2023-10-11 14:05:26

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](https://github.com/palkan/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](https://actionpolicy.evilmartians.io/#/) 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](https://picocss.com/) 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](https://github.com/heartcombo/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](https://blog.appsignal.com/images/blog/2023-10/user-2-access.png)

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](https://blog.appsignal.com/images/blog/2023-10/user-2-access-checked.png)

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](https://blog.appsignal.com/ruby-magic)!**