| name | avo-authorization |
| description | Lock down an Avo admin with Pundit policies — control who sees each resource, button, action, association, and file field via `app/policies/*.rb` policy methods plus `config.authorization_client = :pundit`. Use when the user wants to restrict who can do what in the admin, whether they say it in Avo terms ("set up Pundit policies", "gate this with a policy", "use act_on?/reorder?/search? in the policy", "the resource disappeared from the sidebar after I added a policy method") or in plain Rails/product terms with no Avo vocabulary: "only admins can delete/edit users", "hide the Users resource from non-admins", "non-admin users should only see published posts", "restrict who can run actions", "don't let clients attach/detach records", "hide the delete button for some users", "let only some users upload files", "lock down the admin per role", "gate the reorder/search/actions buttons", "make the admin read-only for support staff", or "different permissions per role". |
| allowed-tools | Read, Edit, Write, Glob, Grep, Bash, WebFetch |
| metadata | {"requires-gem":"avo-authorization — paid add-on (https://avohq.io/addons/authorization)"} |
Avo Authorization
Avo authorizes every resource, button, action, association panel, and file field through a policy client — Pundit out of the box. You write plain Pundit policy classes in app/policies/; Avo calls specially-named methods on them to decide what shows and what's reachable. A false return hides the control in the UI and blocks the underlying request.
- Files:
app/policies/<model>_policy.rb, one per model (e.g. PostPolicy for Avo::Resources::Post). Regular Pundit policies — Avo just calls extra methods on them.
- Config: one line in
config/initializers/avo.rb — config.authorization_client = :pundit.
- License: authorization is a paid add-on (
avo-authorization gem — avohq.io/addons/authorization). State this up front if the user is scoping the work.
Docs — fetch on demand with WebFetch; prefer the raw .md (clean, no HTML):
Read the authorization page before implementing anything beyond a single method — the association naming and explicit_authorization behavior are easy to get wrong from memory.
When this applies
Reach for authorization when the request is "who can see or do X in the admin" — a permission, a role gate, a hidden button:
| Request (Avo-shaped or plain Rails) | Where it's enforced |
|---|
| "Only admins can delete/edit users", "make the admin read-only for support" | Resource methods destroy? / edit? / update? |
| "Hide the Users resource from non-admins" | index? (controls the sidebar + Index access) |
| "Non-admin users should only see published posts" | Policy Scope (Index/Show/Edit) |
| "Restrict who can run actions", "gate the Actions button" | act_on? (and the action's own authorize) |
| "Don't let clients attach/detach related records" | Association methods attach_{assoc}? / detach_{assoc}? |
| "Hide the delete button on related rows" | destroy_{assoc}? |
| "Let only some users upload/download/delete files" | Attachment methods upload_/download_/delete_{FIELD_ID}? |
| "Gate the reorder / search buttons" | reorder? / search? |
| "Lock down the admin per role", "different permissions per role" | The whole policy, branching on user.role |
Note the boundary: policies decide visibility and access. What an action actually does is the avo-actions skill; which association fields exist is avo-associations; making Avo know who current_user is at all is avo-authentication (a hard prerequisite — see Gotchas).
Workflow
1. Enable the client
Pundit is not bundled — add it and require it yourself, then point Avo at it:
gem "pundit"
Avo.configure do |config|
config.authorization_client = :pundit
config.current_user_method = :current_user
end
If the app has never used Pundit, install it first:
bin/rails g pundit:install
2. Generate a policy per model
bin/rails g pundit:policy Post
Avo infers the policy from the resource's model (Avo::Resources::Post → PostPolicy). To point a resource at a different policy, set self.authorization_policy = PhotoCommentPolicy on the resource (useful when self.model_class is shared).
3. Add the methods you need
Fill in only the methods that gate something — every method returns a boolean (or is a Pundit Scope). A minimal "only admins can mutate" policy:
class PostPolicy < ApplicationPolicy
def index? = true
def show? = true
def edit? = user.admin?
def update? = user.admin?
def new? = user.admin?
def create? = user.admin?
def destroy? = user.admin?
class Scope < Scope
def resolve
user.admin? ? scope.all : scope.where(published: true)
end
end
end
Mind explicit_authorization (defaults to true): once a policy class exists, a missing method is treated as denied. Define every method whose control you want visible — don't rely on "I only added destroy?, the rest are untouched." See Gotchas.
Policy methods
Resource methods
Each maps to one controller action; false hides the control and blocks the request. create?/update? are checked at save time, so record carries the submitted form values.
| Method | What it hides / blocks |
|---|
index? | Resource in the auto-generated sidebar + access to the Index view. (Menu-editor items use their own visible block, not this.) |
show? | The view (eye) icon on a row + access to the Show view. |
new? | The "Create new" button (Index header + association Show pages) + the /new page. |
create? | The Save button on /new + association create; checked when persisting. |
edit? | The edit (pencil) icon on a row + access to the Edit view. |
update? | The Save button on edit; checked when persisting. |
destroy? | The delete (trash) icon + the destroy request. |
act_on? | The Actions button on Index. (An individual action can gate itself further with self.authorize — see avo-actions.) |
reorder? | The record-reordering controls on Index. |
search? | The resource search input on Index. |
preview? | Access to the preview endpoint (the preview field). Does not hide the field — use the field's visible: for that. |
Association methods
For association panels (has_many, has_one, HABTM), Avo authorizes against the parent resource's policy using method names built as "#{action}_#{association_name}?" — always plural, matching the association name. For Post has_many :comments, that's attach_comments?, detach_comments?, etc. — never detach_comment?.
The record passed in differs by method — parent for "add to the collection" actions, the row record for "act on this one row" actions:
Method ({assoc} = plural association name) | What it hides / blocks | record is |
|---|
attach_{assoc}? | The Attach button | parent (the Post) |
create_{assoc}? | The Create (new related record) button | parent |
act_on_{assoc}? | The association's Actions dropdown | parent |
view_{assoc}? | Whether the whole panel renders on the parent | parent |
show_{assoc}? | The view (eye) button on a related row | row record |
edit_{assoc}? | The edit button on a related row | row record |
destroy_{assoc}? | The delete button on a related row | row record |
detach_{assoc}? | The detach button on a related row | row record |
reorder_{assoc}? | The reordering controls on the association Index | row record |
view_{assoc}? vs show_{assoc}? and show? are three different things — see Gotchas. show_{assoc}?/edit_{assoc}? only control the button on the row, not access to the target record's own page (that's the target's PostPolicy.show?/edit?).
Attachment (file field) methods
For file/image fields, Avo authorizes per field id with "#{action}_#{FIELD_ID}?". Both record and user are available.
| Method | What it hides / blocks |
|---|
upload_{FIELD_ID}? | Uploading the attachment |
download_{FIELD_ID}? | Downloading the attachment |
delete_{FIELD_ID}? | Deleting the attachment |
For field :cover_photo, as: :file that's upload_cover_photo? / download_cover_photo? / delete_cover_photo?. These same methods also govern file fields inside actions that run on the resource — a field :file, as: :file in an action uses the resource policy's upload_file?. Bulk-define them with a little metaprogramming when a resource has many files:
[:cover_photo, :audio].each do |file|
[:upload, :download, :delete].each do |action|
define_method("#{action}_#{file}?") { true }
end
end
Scopes
A Pundit Scope filters the records a user sees. In Avo it applies to Index, Show, and Edit only — not to has_many/HABTM association fields.
class PostPolicy < ApplicationPolicy
class Scope < Scope
def resolve
user.admin? ? scope.all : scope.where(published: true)
end
end
end
To scope the rows inside a has_many panel, use the association field's own scope: option instead (see avo-associations):
field :comments, as: :has_many, scope: -> { Pundit.policy_scope(parent, query) }
Removing duplication across associations
You often want the same rule for a Comment on its own resource and as a Post's association. Delegate instead of copy-pasting:
class PostPolicy < ApplicationPolicy
def edit_comments? = Pundit.policy!(user, record).edit?
end
For the whole set, include the helper in ApplicationPolicy and call inherit_association_from_policy:
class ApplicationPolicy
include Avo::Authorization::Concerns::PolicyHelpers
end
class PostPolicy < ApplicationPolicy
inherit_association_from_policy :comments, CommentPolicy
def destroy_comments? = false
end
Note view_{assoc}? maps to the target's index?.
Config knobs
Set these in config/initializers/avo.rb (inside Avo.configure):
explicit_authorization (default true): with a policy class present, a missing method or class = denied. Set to false to fall back to "allowed" when a method/class is absent, or pass a Proc (runs in Avo::ExecutionContext) for conditional strictness.
raise_error_on_missing_policy (default false): when true, every resource must have a policy or Avo raises — a stricter, fail-loud posture.
authorization_methods: remap the method names Avo calls, e.g. { index: 'avo_index?', ... }, when your app already uses index? for something else.
authorization_client: :pundit (built-in), nil (off), or a custom client class name string (see below).
Per-resource: self.authorization_policy = SomePolicy overrides the inferred policy.
Custom authorization clients
To use something other than Pundit (e.g. Action Policy), set config.authorization_client = "Avo::ActionPolicyAuthorizationClient" and implement a thin adapter exposing four methods:
authorize(user, record, action, policy_class:, raise_exception:, **) — check an action. Must raise on denial (map to Avo::NotAuthorizedError); map missing policy to Avo::NoPolicyError.
policy(user, record, **) — return the policy instance, or nil if none.
policy!(user, record, **) — return the policy instance, raising Avo::NoPolicyError if none.
apply_policy(user, model, policy_class:, **) — return the scoped query for Index/Show/Edit.
Two rules make or break a custom client (see Gotchas): raise on denial, never return false, and accept extra **kwargs. Fetch the authorization doc for a complete Action Policy adapter example before writing one.
Gotchas
- Pundit is not bundled — require it yourself. Avo does not pull in
pundit. Add gem "pundit" to the Gemfile and set config.authorization_client = :pundit, or nothing authorizes.
- No
current_user, no authorization. Every policy receives user from Avo's current_user_method. If Avo doesn't know the current user, user is nil and policies misbehave across the board. Fix authentication first — see avo-authentication.
- Association methods are PLURAL.
detach_users?, not detach_user? — the name matches the association's pluralization (has_many :users → _users?). This is the #1 "why isn't my association button changing" cause. Cross-link avo-associations.
show? vs show_{assoc}? vs view_{assoc}? are three different controls. show? = the eye button + Show access on the resource's own rows. show_{assoc}? = the eye button on an associated row (button only, not access to the target). view_{assoc}? = whether the entire association panel renders on the parent. Don't reach for one expecting another's effect.
explicit_authorization defaults to true — a missing method is a denial. Adding one method (e.g. destroy?) to a fresh policy makes every other control (index?, show?, …) denied by default, so the resource can vanish from the sidebar the moment you add a policy. If a resource "disappeared" after you touched its policy, define the missing methods (or set explicit_authorization = false). Cross-link avo-troubleshoot.
- Policy
Scope does not reach has_many fields. It applies to Index/Show/Edit only. To filter rows inside an association panel, use the field's scope: option (avo-associations), not the policy scope.
Report
When done, tell the user:
- The policy file(s) created/edited (absolute
app/policies/<model>_policy.rb) and any initializer change (authorization_client, explicit_authorization, raise_error_on_missing_policy, authorization_methods).
- Which methods you added and what each gates (resource / association / attachment / scope), and the role logic they branch on.
- Any prerequisites still owed by the user: the
avohq.io/addons/authorization license (paid add-on), gem "pundit" + pundit:install, and a working current_user (hand off to avo-authentication if unset).
- If associations are involved, a reminder that the methods are plural and that panel-row scoping uses the field's
scope:, not the policy Scope — hand off to avo-associations if the field wiring needs changes.
- If a resource vanished or a control unexpectedly hides, point at
explicit_authorization (avo-troubleshoot).