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
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
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
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
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
}
Admin role:
{
"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 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.
Top comments (0)