DEV Community

Cover image for How to implement Authorizer pattern in Ruby on Rails?
Vlad Hilko
Vlad Hilko

Posted on • Updated on

How to implement Authorizer pattern in Ruby on Rails?

Overview

In this article, we will explore the Authorizer pattern in Ruby on Rails applications. We will provide a definition of the Authorizer pattern and discuss its use cases and examples. We will also delve into the need for this pattern and how it helps to solve certain problems in Rails applications.

Definition

In simple terms, the Authorizer pattern allows us to define and encapsulate complex business rule in our Rails applications. If the requirement specified by this rule is not met, an error will be raised. You can think of an Authorizer as a custom ActiveRecord Validator, but instead of validating input data, it is used to enforce business requirements.

Why do we need it and what problems can this pattern solve? 🤷🏻‍♂️

Let's take a look at the following example.

def create
  user = User.first
  raise 'User is not active' if user.status != 'active' || user.approved_at.blank?

  article = Aricle.create(user: user, title: 'title', body: 'body')
  render json: article
end
Enter fullscreen mode Exit fullscreen mode

In the following example, we want to ensure that certain requirements are fully met before calling the business logic.

Do we have any problems with this approach? 🤔

Yes, we have.

  • It is not possible to test separately from the controller.
  • It is not reusable.
  • We violate the Single Responsibility Principle because the controller becomes responsible for business rules validation.
  • The following code increases 'cognitive load' and requires more time to understand.
  • The following code is not scalable and it is difficult to add more rules.

How can we solve these problems? 🤔

  • We can move this logic to the model level and use before_create callback.
  • We can create Athorizer class and move the logic there.

Let's take a look at these options one by one.


Model with callback

This is the easiest option, we just need to extend our model in the following way:

# app/models/article.rb

class Aricle < ApplicationRecord

  before_save :check_user_status!

  def check_user_status!
    raise 'User is not active' if user.status != 'active' || user.approved_at.blank?
  end

end

user = User.last
Aricle.create(user: user, article_params)
Enter fullscreen mode Exit fullscreen mode

Are there any disadvantages to this approach?

There're 2 problems:

  • We violate the Single responsibility SOLID principle. The model becomes too FAT and responsible for too many things.
  • Using callbacks in Rails models can make the code more difficult to understand and maintain due to their tight coupling with the model and lack of transparency. Therefore, it is generally considered to be a bad pattern.

So it would be nice to encapsulate this logic in a separate class. How can we do that?


Authorizer Pattern

We're going to implement Athorizer Pattern via plain Ruby object because it's the clearest and the most straightforward solution.

Let's take a look at the following example.

# app/authorizers/has_active_user_authorizer.rb

class HasActiveUserAuthorizer

  def initialize(user)
    @user = user
  end

  def authorize!
    raise 'User is not active' unless active?
  end

  private

  attr_reader :user

  def active?
    user.status == 'active' && user.approved_at.present?
  end

end

HasActiveUserAuthorizer.new(user).authorize!
Enter fullscreen mode Exit fullscreen mode

We just encapsulated our rules inside a separate class, and that's it. This code adheres to the SOLID principles and is much easier to understand and maintain.

Naming convention

There are two common ways to name authorizers:

  • Inside a namespace (for example, Authorizers::HasActiveUser)
  • With a postfix (for example, HasActiveUserAuthorizer)

But they have one rule in common - the authorizer must clearly reflect in its name the rule it introduces. Let's take a look at other possible names that are commonly used for the Authorizer Pattern to get a wider picture:

  • CompanyHasNoEmployeesAuthorizer

Raises an error if the company has at least one employee

  • ArticleNotApprovedAuthorizer

Raises an error if the article is approved

  • Authorizers::HasAtLeastOneComment

Raises an error if there are no comments

  • Authorizers::NotPopulated

Raises an error if the table is already populated with records

  • UserHasActiveSubscriptionAuthorizer

Raises an error if the user does not have an active subscription

  • CustomerAgeApprovedAuthorizer

Raises an error if the customer's age is not approved

etc ...

Comparing the Authorizer Pattern to Other Patterns

Similar to Form Object

Form objects and Authorizers both perform validation and raise errors before running business logic. So, what are the differences? The key differences are:

  • Form objects validate data from the user input
  • Authorizers validate general business logic rules

P.S.. To learn more about using Form object pattern in Ruby on Rails, you can check out this article.

Similar to Policy Object

Authorizers are also similar to Policy objects because both encapsulate business rules. The key differences are:

  • Authorizers may contain only one business rule, but Policy objects can contain many.
  • Authorizers are more strict and demand that a business rule be followed, whereas a Policy object will only ask if the rule is true or false. This difference is reflected in the use of ! by Authorizers and ? by Policy objects at the end of their methods.

P.S.. To learn more about using Policy object pattern in Ruby on Rails, you can check out this article.

Similar to Custom Validators

  • Authorizers are similar to custom ActiveRecord validations in terms of their responsibility, but custom validators validate one data field, while Authorizers validate general rules for the whole model.

Note:

Don't confuse the Authorizer pattern with authorization logic. Authorizers ask if the service is authorized to perform the action under the current conditions, while authorization logic happens at a higher level (usually on the controller level).


Bonus Chapter: Adding Authorizers to Service Objects

In the previous chapter, we mentioned that Authorizers are similar to custom validators. Let's take a look at the example of custom validators:

# app/models/article.rb

class Article < ApplicationRecord
  # ...
  validate :name_presence
  # ...
  def name_presence
    errors.add :base, "Name is empty" if name.blank?
  end
end

Enter fullscreen mode Exit fullscreen mode

Or in a separate class:

# app/models/article.rb

class Article < ApplicationRecord
  # ...
  validates_with NamePresenceValidator
  # ...
end

# app/validators/name_presence.rb

class NamePresenceValidator < ActiveModel::Validator
  def validate(record)
    record.errors.add :base, "Name is empty" if record.name.blank?
  end
end

Enter fullscreen mode Exit fullscreen mode

Essentially, we want to have a similar interface for our Authorizers within Service objects, something like this:

# app/services/create_article_service.rb

class CreateArticleService
  # ...
  authorize 'has_active_user_authorizer'
  authorize 'another_authorizer'
  authorize 'one_more_authorizer'
  # ...
end
Enter fullscreen mode Exit fullscreen mode

How can we do this? Let's consider what we need:

  • Service (to keep the authorize method)
  • A module with the authorize method definition (to be able to include it in every service)
  • Authorizer (to encapsulate complex business rule)
  • A base Authorizer (to avoid repeating the same code for every new Authorizer)

Let's add them one by one.

Note: We will only provide code snippets without explanation to keep it brief.

Service

# app/services/create_article_service.rb

class CreateArticleService

  include Authorizable

  authorize 'has_active_user_authorizer'

  attr_reader :user, :params

  def initialize(user: , params:)
    @user = user
    @params = params
  end

  def call
    authorize!
    # main logic
  end

end
Enter fullscreen mode Exit fullscreen mode

Authorizable module

# lib/authorizable.rb

module Authorizable
  extend ActiveSupport::Concern

  included do
    class_attribute :authorizers, default: []
  end

  class_methods do
    def authorize(authorizer_name)
      self.authorizers += [authorizer_name]
    end
  end

  def authorize!
    authorizers.each do |authorizer|
      authorizer.to_s.classify.constantize.new(self).authorize!
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Authorizer

# app/authorizers/has_active_user_authorizer.rb

class HasActiveUserAuthorizer < BaseAuthorizer

  def authorize!
    raise 'User is not active' unless active?
  end

  private

  def active?
    service_object.user.status == 'active' && service_object.user.approved_at.present?
  end

end
Enter fullscreen mode Exit fullscreen mode

BaseAuthorizer

# lib/base_authorizer.rb

class BaseAuthorizer

  def initialize(service_object)
    @service_object = service_object
  end

  def authorize!
    raise 'NotImplementedError'
  end

  private

  attr_reader :service_object

end

Enter fullscreen mode Exit fullscreen mode

That's it! Now you can use the Authorizer pattern in your service objects to encapsulate complex business rules and ensure that they are followed before the main logic is executed. To learn more about using the Service object pattern in Ruby on Rails, you can check out my other article.

So the final solution may look like this:

def create
  user = User.first
  UserIsActiveAuthorizer.new(user).authorize!

  article = Aricle.create(user: user, title: 'title', body: 'body')
  render json: article
end
Enter fullscreen mode Exit fullscreen mode

Or using a service object:

def create
  user = User.first

  article = CreateArticleService.new(user: user, params: { title: 'title', body: 'body' }).call
  render json: article
end
Enter fullscreen mode Exit fullscreen mode

Conclusion

  • The Authorizer Pattern helps us better separate logic, making it easier to test our code.
  • It is DRY and reusable.
  • The Authorizer Pattern allows us to avoid having a "fat" model and adhere to SOLID principles.
  • It reduces "cognitive overhead", making the code easier to understand.
  • It ensures that any necessary requirements are met as soon as possible by raising an error before running any form validations or business logic calculations.

Top comments (0)