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
config.auth_strategy, if set.- The legacy
config.authenticatelambda, if set. - 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_basicwithout 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_sis"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 thecurrent_actorfallback).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,nilotherwise (e.g.index,new,create).context—surface(:web/:mcp),controller(web only), andrequest.
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.