Attribute level permission control in Rails

povilasjurcys

Povilas Jurčys

Posted on February 10, 2023

Attribute level permission control in Rails

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.

# 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
Enter fullscreen mode Exit fullscreen mode

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.

# 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
Enter fullscreen mode Exit fullscreen mode

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 gem.

The 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::

# 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
Enter fullscreen mode Exit fullscreen mode

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 gem:

# 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
Enter fullscreen mode Exit fullscreen mode

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:

{
  "id": 1,
  "body": "This is a sample post body",
  "author_phone": null
}
Enter fullscreen mode Exit fullscreen mode

Admin role:

{
  "id": 1,
  "body": "This is a sample post body",
  "author_phone": "555-555-1234"
}
Enter fullscreen mode Exit fullscreen mode

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 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 gem. The 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.

💖 💪 🙅 🚩
povilasjurcys
Povilas Jurčys

Posted on February 10, 2023

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

Sign up to receive the latest update from our blog.

Related