Authentication & authorization

AdminSuite fails closed: if you mount the engine without configuring authentication, every request is denied with a 403.

Fail-closed behavior

On each request the engine resolves an authentication strategy (see Precedence below). If none is configured — and config.allow_unauthenticated isn’t set for a non-production environment — the request is denied with:

AdminSuite: access denied because no authentication is configured.
Set config.auth_strategy (e.g. :http_basic) or config.authenticate in
config/initializers/admin_suite.rb. To run without authentication in
development/test only, set config.allow_unauthenticated = true.

(403 Forbidden, plain text.) This is intentional — there is no way to accidentally ship an unauthenticated admin panel.

Precedence

  1. config.auth_strategy, if set.
  2. The legacy config.authenticate lambda, if set.
  3. Neither set → fail closed (see above).

Standard strategy: :host_user (0.6.0)

Resolve the host’s existing user; enforce the role in the authorization hook.

config.auth_strategy = :host_user
config.auth_options = { resolve: ->(controller) { controller.current_user } }
config.authorize = ->(actor:, action:, resource:, record:, context:) { actor&.admin? }

Missing/raising resolvers deny. Actor normalization returns a user or nil, never a boolean authentication sentinel. Existing SSO strategies remain supported.

Additional strategy: :http_basic

config.auth_strategy = :http_basic
# config.auth_options = { username: "...", password: "..." }

Credentials come from config.auth_options[:username] / [:password], falling back to the ADMIN_SUITE_USERNAME / ADMIN_SUITE_PASSWORD environment variables.

  • Blank credentials deny every request. Enabling :http_basic without configuring credentials never leaves the admin open — it renders a 403 explaining that credentials are missing, instead of accepting any user/pass.
  • Credential comparison uses ActiveSupport::SecurityUtils.secure_compare (constant-time) for both username and password.
  • On success the actor is AdminSuite::Auth::HttpBasic::Actor, a struct whose #to_s is "http-basic:<username>".

Custom strategies

Subclass AdminSuite::Auth::Strategy and implement #authenticate!(controller). Return a truthy actor to allow the request; render/redirect on the controller (or return nil/false) to deny it.

# app/lib/my_sso_strategy.rb (or anywhere autoloadable)
class MySsoStrategy < AdminSuite::Auth::Strategy
  def authenticate!(controller)
    session_user = controller.session[:sso_user]
    return session_user if session_user&.admin?

    controller.redirect_to(controller.main_app.sso_login_path)
    nil
  end
end

Register it and point auth_strategy at the registered name (or the class itself):

AdminSuite::Auth.register(:my_sso, MySsoStrategy)
config.auth_strategy = :my_sso
# or: config.auth_strategy = MySsoStrategy

config.auth_options (a Hash) is passed to MySsoStrategy.new and available as options inside the strategy.

Legacy config.authenticate lambda

The pre-strategy API still works and is wrapped internally as a strategy:

config.authenticate = ->(controller) do
  controller.redirect_to(controller.main_app.root_path) unless controller.current_user&.admin?
end

Denial is expressed by rendering/redirecting on the controller (or the lambda simply halting); returning without halting means the request is authenticated. config.current_actor is consulted at most once per request to determine the actor for actions/auditing/authorization.

allow_unauthenticated

config.allow_unauthenticated = true

Runs AdminSuite without any authentication check. Ignored in production — even if set, Rails.env.production? requests still fail closed. Intended for local development and test only.

skip_host_before_actions

config.skip_host_before_actions = [ :require_authentication ]

Host apps often define global auth before_actions on their own ApplicationController (e.g. Rails 8’s require_authentication generator filter). AdminSuite authenticates itself via the strategy layer above, so it skips the filters named here (skip_before_action <name>, raise: false). Default: [:require_authentication]. Evaluated when the engine application controller class loads; changes apply on the next class load (or process boot when class caching is enabled).

Only name authentication filters here. Skipping unrelated host callbacks can bypass application behavior that AdminSuite still depends on.

Authorization: config.authorize

Authentication answers “who is this?”; config.authorize answers “may they do this?”. It’s called for every resource controller action:

config.authorize = ->(actor:, action:, resource:, record:, context:) { true }
  • actor — the authenticated actor (strategy-provided, or the current_actor fallback).
  • action — one of :read, :create, :update, :destroy, :execute (see the verb table below).
  • resource — the resource’s config class (e.g. Admin::Resources::WidgetResource).
  • record — the specific record for member actions/routes, nil otherwise (e.g. index, new, create).
  • context — surface (:web/:mcp), controller (web only), and request.

In 0.6.0 replace the old controller: keyword with context: and use context.controller for web-specific behavior. Old signatures raise at assignment.

The authorization hook runs after set_resource, so record: is populated for member actions and remains nil for collection actions.

config.authorize = nil (the default) means every authenticated web request is allowed — authentication remains the only gate. A falsy return from the hook renders 403 Forbidden.

Action → verb table

Controller action action:
index, show :read
new, create :create
edit, update, toggle :update
destroy :destroy
execute_action, bulk_action :execute

Interaction with read_only resources

Resources marked read_only reject every mutation route regardless of config.authorize: new/create/edit/update/destroy, toggle, and named execute_action / bulk_action (declared or not). Those requests 404 and must not change the model.

config.authorize still runs as an unconditional before_action on writable resources. For execute_action/bulk_action it is consulted (with action: :execute) before the controller checks whether the requested action name was declared. An undeclared action name 404s inside the action method itself, after authorization has already passed.

A hook that returns false or nil is fail-closed: 403 Forbidden, no record body, no mutation. A nil hook (config.authorize = nil) still means every authenticated request is allowed — authentication remains the only gate.

MCP authorization (0.6.0)

MCP fails closed when the hook is nil, even though web requests retain their existing semantics. A real actor is required. See Admin MCP for the resource-level collection boundary and the additional record check on get_record.