Most Rails apps end up with authorization and audit logging as two separate, loosely-connected concerns: Pundit or CanCanCan decide whether an action is allowed, and if anyone wants a record of who changed what role, when, that gets hand-rolled later — usually after a compliance review asks for it and nobody has it. Argus::Trail exists because I kept rebuilding that second half from scratch.
What it actually is
Argus::Trail is a mountable Rails engine that adds configurable roles and permissions to an existing app, where an actor can hold any number of roles, plus an immutable audit log of every role assignment, revocation, and permission grant or revoke — with ready-made, paginated admin screens to manage all of it.
# Gemfilegem "argus-trail"bundle installbin/rails generate argus:trail:installbin/rails db:migrateThat one generator does more than scaffold migrations. It creates the engine’s own tables (argus_trail_roles, argus_trail_permissions, argus_trail_role_permissions, argus_trail_role_assignments, argus_trail_audit_entries), mounts the engine at /admin/access, adds include Argus::Trail::Actor to your actor model, and wires a before_action in ApplicationController so the engine knows who’s currently acting. Both of those last two steps are idempotent and skip themselves with an explanatory message if the target file doesn’t exist yet.
Note
There’s no migration on your own users table. Role assignments live in the engine’s own
polymorphic join table, so adopting this on an existing app doesn’t touch a schema you already
depend on.
No hardcoded roles, no fixed actor class
The design constraint I cared most about: Argus::Trail doesn’t ship with opinions about what your roles or permissions are called, and it doesn’t assume your actor model is named User. Roles and permissions are rows you create, not constants baked into the gem. It auto-integrates with Pundit if it’s in your Gemfile and with Kaminari for pagination if that’s there too — but needs neither; without Pundit, it falls back to “any signed-in actor,” and without Kaminari it uses a small built-in pager.
Generating permissions from your actual routes
Rather than hand-writing a permission for every controller action, one rake task scans your routes:
bin/rails argus_trail:fetch_permissionsThis creates a Permission for every controller action it finds — skipping the engine’s own routes and Rails-internal ones — grouped by module ("admin/accounts") and action (read/create/update/destroy, or a custom action name verbatim). It only ever adds permissions, so it’s safe to rerun after adding new controllers. A companion fetch_permissions:prune task removes permissions whose route no longer exists and that aren’t granted to any role — so the permission list doesn’t quietly accumulate dead entries as the app evolves.
Gating a controller is then one line:
class AccountsController < ApplicationController include Argus::Trail::AuthorizableendThe required permission is derived automatically from the controller and action — the same module/action pair fetch_permissions generated — so there’s no second place to keep a permission name in sync with the route it protects.
The part that actually matters: audit entries you can’t bypass
Changing roles or permissions through the normal API methods writes to the audit log as a side effect you can’t opt out of:
user.sync_roles!([admin_role.id, support_role.id], changed_by: current_user)role.sync_permissions!(params[:permission_ids], changed_by: current_user)
user.has_permission?("manage_billing")user.has_permission?("admin/accounts", :read)sync_roles!/sync_permissions! exist specifically so there’s one path for making these changes, instead of assigning role_ids= directly and hoping someone remembers to log it separately. The one honest gotcha here, straight from the project’s own integration guide: if you’re also running ActiveAdmin and register these models there for convenience, ActiveAdmin’s default checkboxes bypass the audit trail entirely unless you call sync_permissions!/sync_roles! explicitly instead of relying on the default form submission.
Warning
has_permission? reflects the union of every role an actor holds — if any assigned role
grants it, the check passes. There’s no built-in way to ask “does this specific role grant this”
from the actor side; for that, check role.permissions.exists?(name: ...) on the role directly.
Authorization for the admin screens themselves
With neither Pundit policies nor config.authorize_with configured, the admin screens default to “any signed-in actor can manage roles” — which is a reasonable zero-config starting point, but worth tightening deliberately rather than by accident:
class Argus::Trail::RolePolicy < ApplicationPolicy def index? = user.admin?endOr skip Pundit entirely with a proc, which always takes priority even if Pundit happens to also be in the Gemfile:
Argus::Trail.configure do |config| config.authorize_with = ->(controller, record_or_class) { controller.current_user&.admin? }endOne integration bug worth calling out because it’s easy to hit and confusing to debug: if your ApplicationController has a shared rescue_from Pundit::NotAuthorizedError that redirects to root_path, a denial raised inside the engine’s own controllers redirects in an infinite loop. Argus::Trail is mounted as an isolated engine, so a bare root_path inside it resolves to the engine’s own root — the exact page that was just denied — not your app’s root. The fix is a one-word change: redirect_to main_app.root_path instead of root_path, inside that specific rescue handler.
Configuration surface
Argus::Trail.configure do |config| config.actor_class_name = "User" config.changed_by_resolver = -> { Argus::Trail.current_actor } config.authorize_with = nil config.current_actor_method = :current_user config.per_page = 30 config.layout = nil config.permission_scan_excludes = []endThe shipped admin UI renders with Tailwind via CDN, so it works with zero host asset-pipeline setup out of the box — or run bin/rails generate argus:trail:views to copy every view into your own app for full override, and point config.layout at one of your existing layouts instead.
Argus::Trail is MIT-licensed. For the full step-by-step walkthrough — including the authorization wiring for CanCanCan and Action Policy, not just Pundit — see the detailed Argus::Trail documentation.