Authorizer

Authorizer is designed using adaptor pattern to allow Wallaby to check permissions defined by e.g. CanCanCan ability or Pundit policy.

Therefore, if CanCanCan, Pundit or no authorization is used, there shouldn’t be anything to do about authorizer since Wallaby can pick up the authorization framework in use.

In order to tell Wallaby which authorization framework to apply or to reduce Wallaby’s detection time, provider_name can be set to accomplish these goals:

# app/authorizers/admin/application_authorizer.rb
class Admin::ApplicationAuthorizer < Wallaby::ModelAuthorizer
  self.provider_name = :cancancan # if CanCanCan is used
  self.provider_name = :pundit # if Pundit is used
  self.provider_name = :default # if none is used
end

This base authorizer can be generated by Wallaby generator as well:

rails g wallaby:install --include-authorizer

This is most likely how authorizer should be customized.

Continue reading if there is a need to add more functions to authorization (e.g. auditing).

Continue

First of all, it’s always recommended to create a base authorizer class Admin::ApplicationAuthorizer as below, so that devs can have better control of developing global changes/functions:

# app/authorizers/admin/application_authorizer.rb
class Admin::ApplicationAuthorizer < Wallaby::ModelAuthorizer
end

See Mapping - Authorizer for the configuration if Admin::ApplicationAuthorizer is taken for other purpose.

Starting with:

Configuration can be set for:

Accessing helper methods:

  • #user - accesing user object
  • #context - accessing controller context.

Customizing authorization operations:

  • #authorize - checking permission and raising error if any. It’s mostly used in controller.
  • #authorized? - checking if the user has access to given subject.
  • #unauthorized? - checking if the user has no access to given subject.
  • #accessible_for - applying the scope that the user has access to.
  • #attributes_for - applying the attribute values that the user can create/update.
  • #permit_params - whitelisting params for mass assignment.

How Wallaby checks permissions for following frameworks:

Declaration

Read more at Authorizer Naming Convention

Let’s see how au authorizer can be created so that Wallaby knows its existence.

Similar to the way in Rails, create a custom authorizer for model Product inheriting from Admin::ApplicationAuthorizer (the base authorizer mentioned above) as below:

# app/authorizers/product_authorizer.rb
class ProductAuthorizer < Admin::ApplicationAuthorizer
end

If ProductAuthorizer is taken, it is still possible to use another name (e.g. Admin::ProductAuthorizer). However, the attribute model_class must be specified. See model_class for examples.

.base_class!

All authorizers will be preloaded and processed by Wallaby in order to build up the mapping between authorizers and models. If the authorizer is considered not to be processed, it can be flagged by using base_class!:

# app/authorizers/admin/special_authorizer.rb
class Admin::SpecialAuthorizer < Admin::ApplicationAuthorizer
  base_class!
end

.model_class

According to Wallaby’s Authorizer Naming Convention, if a custom authorizer cannot reflect the association to the correct model, for example, as Admin::ProductAuthorizer to Product, it is required to specify the model class in the authorizer as below:

# app/authorizers/admin/product_authorizer.rb
class Admin::ProductAuthorizer < Admin::ApplicationAuthorizer
  self.model_class = Product
end

.provider_name

Wallaby has implemented the following authorization adaptors for ActiveRecord:

And Wallaby can detect which authorization that might be in use. However, there is still a chance that Wallaby can’t get it right.

In this case, for example, if Pundit is used, it can be configured as:

# app/authorizers/admin/application_authorizer.rb
class Admin::ApplicationAuthorizer < Wallaby::ModelAuthorizer
  self.provider_name = :pundit
end

NOTE: It’s also recommended to specify the provider name even if Wallaby has successfully detected the right one to use. Since it will improve the performance as Wallaby won’t do the detection on every request once provider_name is set.

Helper Methods

#context

It’s the reference of controller itself. To access context, it goes:

# app/authorizers/admin/application_authorizer.rb
class Admin::ApplicationAuthorizer < Wallaby::ModelAuthorizer
  private
  def rescue_permission_exception(action, error)
    context.flash[:alert] = translate_message_for action, error
  end
end

#user

It’s the reference of current user object. To access user, it goes:

# app/authorizers/admin/application_authorizer.rb
class Admin::ApplicationAuthorizer < Wallaby::ModelAuthorizer
  private
  def audit_log(action)
    AuditLog.log action, user
  end
end

Authorization

#authorize

This is the template method to check permission for a given subject and raise Wallaby::Forbidden exception if the user has no access. It’s used by controller mostly.

To customize how to check permission for a given subject, it goes:

# app/authorizers/admin/application_authorizer.rb
class Admin::ApplicationAuthorizer < Wallaby::ModelAuthorizer
  def authorize(action, subject)
    context.authorize! action, subject
    context.authorize subject, "#{action}?"
  end
end

#authorized?

This is the template method that checks if the user has access to given subject and returns true if so.

To customize how to check permission for a given subject, it goes:

# app/authorizers/admin/application_authorizer.rb
class Admin::ApplicationAuthorizer < Wallaby::ModelAuthorizer
  def authorized?(action, subject)
    context.can?(action, subject) && context.policy(subject).try("#{action}?")
  end
end

#unauthorized?

This is the template method that checks if the user has no access to given subject and returns true if so. It’s simply the opposite version of authorized?.

To customize how to check permission for given subject, it goes:

# app/authorizers/admin/application_authorizer.rb
class Admin::ApplicationAuthorizer < Wallaby::ModelAuthorizer
  def unauthorized?(action, subject)
    !(context.can?(action, subject) && context.policy(subject).try("#{action}?"))
  end
end

#accessible_for

This is the template method to ensure the user can only query the data that they are allowed to access.

To customize how to restrict the query, it goes:

# app/authorizers/admin/application_authorizer.rb
class Admin::ApplicationAuthorizer < Wallaby::ModelAuthorizer
  def accessible_for(action, scope)
    query = scope.accessible_by context.current_ability, action
    context.policy_scope query
  end
end

#attributes_for

This is the template method to ensure the user can only update the data with the value that they are allowed to assign.

To customize how to restrict the assignment, it goes:

# app/authorizers/admin/application_authorizer.rb
class Admin::ApplicationAuthorizer < Wallaby::ModelAuthorizer
  def attributes_for(action, subject)
    context.current_ability.attributes_for action, subject
    context.policy(subject).try :attributes_for
  end
end

#permit_params

This is the template method to permit parameters for mass assignment.

To customize how to permit parameters, it goes:

# app/authorizers/admin/application_authorizer.rb
class Admin::ApplicationAuthorizer < Wallaby::ModelAuthorizer
  def permit_params(action, subject)
    context.policy(subject).public_send "permitted_attributes_for_#{action}"
  end
end

How Wallaby Applies Authorization

CanCanCan

For authorize, authorized? and unauthorized?

Wallaby has implemented the support for general Rails resourcesful actions (index/new/create/show/edit/update/destroy) and CanCanCan comes with alias actions that mapped to Rails actions:

# lib/cancan/ability/actions.rb
def default_alias_actions
  {
    read: %i(index show),
    create: [:new],
    update: [:edit]
  }
end

Therefore, when Wallaby checks permission in each resourcesful action, they will be mapped to CanCanCan’s action accordingly as below:

authorizer.authorized? :index, Product # => Same as `can? :read, Product`
authorizer.authorized? :new, product # => Same as `can? :create, product`
authorizer.authorized? :create, product # => Same as `can? :create, product`
authorizer.authorized? :show, product # => Same as `can? :read, product`
authorizer.authorized? :edit, product # => Same as `can? :update, product`
authorizer.authorized? :update, product # => Same as `can? :update, product`
authorizer.authorized? :destroy, product # => Same as `can? :destroy, product`

For permit_params

Since CanCanCan does not support this feature, Wallaby won’t do anything either for permit_params.

Pundit

For authorize, authorized? and unauthorized?

Similar to Cancancan, when Wallaby checks permission, they will work the same as how Pundit works:

authorizer.authorized? :index, Product # => Same as `policy(Product).index?`
authorizer.authorized? :new, product # => Same as `policy(product).new?`
authorizer.authorized? :create, product # => Same as `policy(product).create?`
authorizer.authorized? :show, product # => Same as `policy(product).show?`
authorizer.authorized? :edit, product # => Same as `policy(product).edit?`
authorizer.authorized? :update, product # => Same as `policy(product).update?`
authorizer.authorized? :destroy, product # => Same as `policy(product).destroy?`

For attributes_for

If the policy has implemented attributes_for and attributes_for_#{action}:

# app/policies/product_policy.rb
class ProductPolicy
  def attributes_for_create
    product.published = false
  end

  def attributes_for
    product.published = !!product.published_at
  end
end

Then when Wallaby applies attributes_for restriction for create action, published will be set to false. However, when Wallaby applies attributes_for for udpate action, published will be true if published_at is set.

For permit_params

Similar to attributes_for above, if the policy has implemented permit_params and permit_params_for_#{action}:

# app/policies/product_policy.rb
class ProductPolicy
  def permit_params_for_create
    [:name, :sku]
  end

  def permit_params
    [:name]
  end
end

Then when Wallaby applies permit_params restriction for create action, user will be able to set name and sku. However, when Wallaby applies permit_params for udpate action, user will be able to update name only.