Skip to content
On this page

Authorization

When you share access to Avo with your clients or large teams, you may want to restrict access to a resource or a subset of resources. One example may be that only admin-level users may delete or update records.

By default, Avo leverages Pundit under the hood to manage the authorization.

Pundit alternative

Pundit is just the default choice. You may plug in your own client using the instructions here.

WARNING

You must manually require pundit or your authorization library in your Gemfile.

ruby
# Minimal authorization through OO design and pure Ruby classes
gem "pundit"

Make sure Avo knows who your current user is

Before setting any policies up, please ensure Avo knows your current user. Usually, this 👇 set up should be fine, but follow the authentication guide for more information.

ruby
# config/initializers/avo.rb
Avo.configure do |config|
  config.current_user_method = :current_user
end

Policies

Just run the regular pundit bin/rails g pundit:policy Post to generate a new policy.

If this is a new app you need to install pundit first bin/rails g pundit:install.

With this new policy, you may control what every type o user can do with Avo. The policy has the default methods for the regular controller actions: index?, show?, create?, new?, update?, edit? and destroy?.

These methods control whether the resource appears on the sidebar, if the view/edit/destroy buttons are visible or if a user has access to those index/show/edit/create pages.

index?

index? is used to display/hide the resources on the sidebar and restrict access to the resources Index view.

INFO

This option is used in the auto-generated menu, not in the menu editor.

You'll have to use your own logic in the visible block for that.

show?

When setting show? to false, the user will not see the show icon on the resource row and will not have access to the Show view of a resource.

create?

The create? method will prevent the users from creating a resource. That will also apply to the Create new {model} button on the Index, the Save button on the /new page, and Create new {model} button on the association Show page.

new?

The new? method will control whether the users can save the new resource. You can also access the record variable with the form values pre-filled.

edit?

edit? to false will hide the edit button on the resource row and prevent the user from seeing the edit view.

update?

update? to false will prevent the user from updating a resource. You can also access the record variable with the form values pre-filled.

destroy?

destroy? to false will prevent the user from destroying a resource and hiding the delete button.

upload_attachments?

Controls whether the attachment upload input should be visible in the File and Files fields.

download_attachments?

Controls whether the attachment download button should be visible in the File and Files fields.

delete_attachments?

Controls whether the attachment delete button should be visible in the File and Files fields.

act_on?

Controls whether the user can see the actions button on the Index page.

Actions button

Associations

When using associations, you would like to set policies for creating new records on the association, allowing to attach, detach, create or destroy relevant records. Again, Avo makes this easy using a straightforward naming schema.

Example scenario

We'll have this example of a Post resource with many Comments through the has_many :comments association.

WARNING

Make sure you use the same pluralization as the association name.

For a has_many :users association use the plural version method view_users?, edit_users?, detach_users?, etc., not the singular version detach_user?.

The record variable in policy methods

In the Post has_many Comments example, when you want to authorize show_comments? in PostPolicy you will have a Comment instance as the record variable, but when you try to authorize the attach_comments?, you won't have that Comment instance because you want to create one, but we expose the parent Post instance so you have more information about that authorization action that you're trying to make.

attach_{association}?

Controls whether the Attach comment button is visible. The record variable is the parent record (a Post instance in our scenario).

detach_{association}?

Controls whether the detach button is available on the associated record row on the Index view. The record variable is the actual row record (a Comment instance in our scenario).

view_{association}?

Controls whether the view button is visible on the associated record row on the Index page.The record variable is the actual row record (a Comment instance in our scenario).

WARNING

This does not control whether the user has access to that record. You control that using the Policy of that record (PostPolicy.show? in our example).

edit_{association}?

Controls whether the edit button is visible on the associated record row on the Index page.The record variable is the actual row record (a Comment instance in our scenario).

WARNING

This does not control whether the user has access to that record's edit page. You control that using the Policy of that record (PostPolicy.show? in our example).

create_{association}?

Controls whether the Create comment button is visible. The record variable is the parent record (a Post instance in our scenario).

destroy_{association}?

Controls whether the delete button is visible on the associated record row on the Index page.The record variable is the actual row record (a Comment instance in our scenario).

act_on_{association}?

Controls whether the Actions dropdown is visible. The record variable is the parent record (a Post instance in our scenario).

Removing duplication

A note on duplication

Let's take the following example:

A User has many Contracts. And you represent that in your Avo resource. How do you handle authorization to the ContractResource?

For one, you set the ContractPolicy.index? and ContractPolicy.edit? methods to false so regular users don't have access to all contracts (see and edit), and the UserPolicy.view_contracts? and UserPolicy.edit_contracts? set to false, because, when viewing a user you want to see all the contracts associated with that user and don't let them edit it.

You might be thinking that there's code duplication here. "Why do I need to set a different rule for UserPolicy.edit_contracts? when I already set the ContractPolicy.edit? to false? Isn't that going to take precedence?"

Now, let's imagine we have a user that is an admin in the application. The business need is that an admin has access to all contracts and can edit them. This is when we go back to the ContractPolicy.edit? and turn that to true for the admin user. And now we can separately control who and where a user can edit a contract.

You may remove duplication by applying the same policy rule from the original policy.

ruby
class CommentPolicy
  # ... more policy methods
  def edit
    record.user_id == current_user.id
  end
end

class PostPolicy
  # ... more policy methods
  def edit_comments?
    Pundit.policy!(user, record).edit?
  end
end

Now, whatever action you take for one comment, it will be available for the edit_comments? method in PostPolicy.

Scopes

You may specify a scope for the Index, Show, and Edit views.

ruby
class PostPolicy < ApplicationPolicy
  class Scope < Scope
    def resolve
      if user.admin?
        scope.all
      else
        scope.where(published: true)
      end
    end
  end
end

Using different policy methods

By default Avo will use the generated Pundit methods (index?, show?, create?, new?, update?, edit? and destroy?). But maybe, in your app, you're already using these methods and would like to use different ones for Avo. You may want override these methods inside your configuration with a simple map using the authorization_methods key.

ruby
Avo.configure do |config|
  config.root_path = '/avo'
  config.app_name = 'Avocadelicious'
  config.license = 'pro'
  config.license_key = ENV['AVO_LICENSE_KEY']
  config.authorization_methods = {
    index: 'avo_index?',
    show: 'avo_show?',
    edit: 'avo_edit?',
    new: 'avo_new?',
    update: 'avo_update?',
    create: 'avo_create?',
    destroy: 'avo_destroy?',
  }
end

Now, Avo will use avo_index? instead of index? to manage the Index view authorization.

Raise errors when policies are missing

The default behavior of Avo is to allow missing policies for resources silently. So, if you have a User model and a UserResource but don't have a UserPolicy, Avo will not raise errors regarding missing policies and authorize that resource.

If, however, you need to be on the safe side of things and raise errors when a Resource is missing a Policy, you can toggle on the raise_error_on_missing_policy configuration.

ruby
# config/initializers/avo.rb
Avo.configure do |config|
  config.root_path = '/avo'
  config.app_name = 'Avocadelicious'
  config.license = 'pro'
  config.license_key = ENV['AVO_LICENSE_KEY']
  config.raise_error_on_missing_policy = true
end

Now, you'll have to provide a policy for each resource you have in your app, thus making it a more secure app.

Custom policies

Since v2.17

By default, Avo will infer the policy from the model of the resource object. If you wish to use a different policy for a given resource, you can specify it directly in the resource using the authorization_policy option.

ruby
class PhotoCommentResource < Avo::BaseResource
  self.model_class = ::Comment
  self.authorization_policy = PhotoCommentPolicy
  # ...
end

Custom authorization clients

INFO

Check out the Pundit client for reference.

Client methods

Each authorization client must expose a few methods.

authorize

Receives the user, record, action, and optionally, the policy_class and authorizez that action

ruby
# Pundit example
def authorize(user, record, action, policy_class: nil)
  Pundit.authorize(user, record, action, policy_class: policy_class)
rescue Pundit::NotDefinedError => error
  raise NoPolicyError.new error.message
rescue Pundit::NotAuthorizedError => error
  raise NotAuthorizedError.new error.message
end

policy

Receives the user and record and returns the policy to use.

ruby
def policy(user, record)
  Pundit.policy(user, record)
end

policy!

Receives the user and record and returns the policy to use. It will raise an error if no policy is found.

ruby
def policy!(user, record)
  Pundit.policy!(user, record)
rescue Pundit::NotDefinedError => error
  raise NoPolicyError.new error.message
end

apply_policy

Receives the user, record, and optionally, the policy class to use. It will apply a scope to a query.

ruby
def apply_policy(user, model, policy_class: nil)
  # Try and figure out the scope from a given policy or auto-detected one
  scope_from_policy_class = scope_for_policy_class(policy_class)

  # If we discover one use it.
  # Else fallback to pundit.
  if scope_from_policy_class.present?
    scope_from_policy_class.new(user, model).resolve
  else
    Pundit.policy_scope!(user, model)
  end
rescue Pundit::NotDefinedError => error
  raise NoPolicyError.new error.message
end