Attribute level permission control in Rails
Tags: ruby, rails, patterns, authorization
Published: 2023-02-10 11:13:22

Authorization in API can be a complex task for developers as it requires a thorough understanding of security and access controls. Authorization is the process of granting or denying access to specific resources, actions, or data based on the user's permissions or roles. In a Ruby on Rails API, authorization can be implemented at the controller level, action level, or attribute level.

## No authorization

To understand the complexity of authorization, let's start with a simple Ruby on Rails API controller example that does not have any authorization.

```ruby
# app/controllers/api/v1/posts_controller.rb

class API::V1::PostsController < ApplicationController
  def create
    @post = Post.new(post_params)
    @post.save!
    render json: serialized(@post)
  end

  def update
    @post = Post.find(params[:id])
    @post.update!(post_params)
    render json: serialized(@post)
  end

  def show
    @post = Post.find(params[:id])
    render json: serialized(@post)
  end

  private

  def serialize(post)
    # custom serialization logic
  end
end
```

In this example, anyone can create, update, or show posts. However, in most applications, only certain users should have access to certain resources or actions. 

## Action-level authorization

To implement action-level authorization, we can use the `before_action` method in the controller.

```ruby
# app/controllers/api/v1/posts_controller.rb

class API::V1::PostsController < ApplicationController
  before_action :authenticate_user!
  before_action :authorize_user!, only: [:create, :update]

  def create
    @post = Post.new(post_params)
    @post.save!
    render json: serialized(@post)
  end

  def update
    @post = Post.find(params[:id])
    @post.update!(post_params)
    render json: serialized(@post)
  end

  def show
    @post = Post.find(params[:id])
    render json: serialized(@post)
  end

  private

  def authorize_user!
    if current_user.role == 'author'
      flash[:error] = "You can't access this page."
      redirect_to root_path
    end
  end

  def serialize(post)
    # custom serialization logic
  end
end
```

In this example, the `authenticate_user!` method ensures that only logged-in users can access the API. The `authorize_user!` method checks if the current user has the author role, and if not, redirects the user to the root path.

While this implementation of action-level authorization works, it is limited in terms of granularity. 

## Attribute-level authorization

In most applications, authorization needs to be applied not only to actions but also to methods, attributes, and resources. To simplify attribute-level authorization, we can use the [resource_policy](https://github.com/samesystem/resource_policy) gem.

The [resource_policy](https://github.com/samesystem/resource_policy) is a policy-based authorization gem for Ruby applications. It provides a simple and flexible way to manage authorization at the action, attribute, and resource level.

The policy class looks like this::

```ruby
# app/policies/post_policy.rb

class PostPolicy
  include ResourcePolicy::Policy

  policy do |c|
    c.action(:create).allowed(if: :editor?)
    c.action(:update).allowed(if: :author?)
    c.action(:show).allowed

    c.attribute(:id).allowed(:read)
    c.attribute(:body).allowed(:read).allowed(:write, if: :author?)
    c.attribute(:author_phone).allowed(:read, if: :admin?)
  end

  delegate :admin?, :editor?, to: :@current_user

  def initialize(post, current_user)
    @post = post
    @current_user
  end

  def author?
    @post.author == current_user
  end
end
```

The `PostPolicy` is a policy class used for authorization of actions and attributes. It includes the `ResourcePolicy::Policy` module which provides the policy configuration via `policy` method. This method is used to define the authorization rules for each action and attribute.

The policy method defines the rules for creating, updating, and showing a post, as well as reading and writing the `body` and `author_phone` attributes. The authorization rules specify the conditions under which an action or attribute is allowed. For example, creating a post is only allowed if the current user is an editor and updating a post is only allowed if the current user is the author.

Here's the updated code for our API controller with attribute-level authorization using the [resource_policy](https://github.com/samesystem/resource_policy) gem:

```ruby
# app/controllers/api/v1/posts_controller.rb

class API::V1::PostsController < ApplicationController
  before_action :authorize_user!

  def create
    @post = Post.new(post_params)

    when_authorized(@post, :create) do |protected_post|
      @post.save!
      render json: serialized(protected_post)
    end      
  end

  def update
    @post = Post.find(params[:id])
    when_authorized(@post, :update) do |protected_post|
      @post.update!(post_params)
      render json: serialized(protected_post)
    end
  end

  def show
    @post = Post.find(params[:id])
    when_authorized(@post, :show) do |protected_post|
      render json: serialized(protected_post)
    end
  end

  private

  def when_authorized(post, action_name)
    policy = PostPolicy.new(post, current_user)
    
    if policy.action(action_name).allowed?
      return yield(policy.protected_resource)
    else
      render json: { error: 'not authorized' }, status: 403
    end
  end

  def serialize(post)
    # custom serialization logic
  end
end
```

Our updated controller does not have `before_action :authorize_user!` hook anymore, but handles all the authorization logic in `when_authorized` method.

The `when_authorized` method is used to wrap the logic in the action methods (`create`, `update`, and `show`). This method takes in two parameters: `post` and `action_name`.

The method starts by initializing a `PostPolicy` object with the `post` and `current_user` as parameters. The `PostPolicy` object at first will determine if the current user is allowed to perform the action specified by `action_name` on the post. Then it will return `protected_resource` object.

## Protected resource

The `protected_resource` is a special `ResourcePolicy` object that returns `nil` for each unauthorized attribute, and returns the actual value otherwise. This makes it very handy for API calls where you need to return only authorized data.

For demonstration purposes, let's assume we have the two user roles: guest and admin. The following examples show the output of the `show` action for each of these roles.

**Guest role:**

```javascript
{
  "id": 1,
  "body": "This is a sample post body",
  "author_phone": null
}
```

**Admin role:**

```javascript
{
  "id": 1,
  "body": "This is a sample post body",
  "author_phone": "555-555-1234"
}
```

We have single source of truth. We can always update our `PostPolicy` configuration when we need to protect more attributes. No more code duplication or accidental data exposure. It wasn't an easy journey, bet we made it.

## Final thoughts

Authorization is a complex topic and I tried really hard not to go to deep in to the details. That's why I only touched the very basics of [resource_policy](https://github.com/samesystem/resource_policy) gem and how to use it to solve simple-enough authorization problems.

In conclusion, authorization in a Ruby on Rails API is an important aspect of ensuring the security and privacy of data and resources. Without authorization, anyone can access any resource or action in the API, which is unacceptable in most applications. Action-level authorization is a simple solution that can be implemented using the before_action method, but it is limited in its granularity. Attribute-level authorization provides a more flexible and robust solution, and can be achieved using the [resource_policy](https://github.com/samesystem/resource_policy) gem. The [resource_policy](https://github.com/samesystem/resource_policy) gem allows for the definition of policy classes that specify the authorization rules for actions and attributes, making it easier to manage authorization at different levels of granularity.

In this article, we have covered the basics of authorization in a Ruby on Rails API, and provided examples of how to implement action-level and attribute-level authorization. By understanding the concepts and methods discussed in this article, developers can implement effective authorization in their Ruby on Rails APIs, ensuring the security and privacy of their data and resources.