Authorization & Policies
A Pundit-style policy layer that answers one question: is the current user allowed to perform this action on this record?
Deny by default
Policies start out forbidding everything — you opt in to what you want to allow. A record class with no matching policy is denied, so a forgotten rule fails closed rather than open. Scaffold the whole thing, including a User model and login flow, with one command.
1 Generate it
soli generate auth scaffolds the full Devise-style auth suite — login/signup, password reset (hashed one-time tokens, 2h expiry), email confirmation with resend, remember-me (HttpOnly persistent cookie), account lockout with auto-unlock — and the policy layer together. Thresholds and the confirmation gate are constants/toggles at the top of the generated app/models/user.sl; configure SMTP (SOLI_SMTP_*) so the emails go out.
soli generate auth
soli db:migrate up # create the users collection
soli serve . --dev # then visit /signup, /login
Files in app/policies/ are auto-loaded into the global scope at boot, like models. Restart the server after adding a new policy.
2 Write a policy
A policy is a class named <Model>Policy with one predicate per action: index?, show?, create?, new?, update?, edit?, destroy?. Inside it, this.user is the current user and this.record is the record being checked.
class PostPolicy < ApplicationPolicy
def show?
true # anyone may read a post
end
def update?
return false unless this.signed_in?() # guard the nil user first
return this.user["_key"] == this.record["author_id"]
end
def destroy?
return this.update?() # same rule as update
end
end
ApplicationPolicy returns false for every predicate, so you only override what you allow. new? falls back to create? and edit? to update? unless overridden.
3 Authorize in a controller
Call authorize(record) at the top of an action. It builds the matching policy and calls the predicate for the current action — a falsey result raises 403 Forbidden; otherwise it returns the record.
def update
post = Post.find(params["id"])
authorize(post) # 403 unless PostPolicy#update? is true
post.update(this._permit_params(params))
return redirect(post_path(post))
end
# Pass an explicit action when it differs from the controller action:
authorize(post, "show")
Helpers
| Helper | Available in | Returns |
|---|---|---|
authorize(record, action?) | controllers / policies | the record, or raises 403 |
policy_for(record) | controllers / policies | the policy instance |
current_user() | controllers / policies / views | the signed-in User, or nil |
signed_in?() | controllers / policies / views | true when signed in |
current_action() | anywhere | the in-flight action name |
forbidden(message?) | anywhere | raises 403 immediately |
current_user is populated per request by the generated load_current_user middleware, which reads the user id from the session and loads the User. A denied request renders through the standard error pipeline, so a custom app/views/errors/403.html.slv is used automatically.