ESC
Type to search...
S
Soli Docs

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 + Secure persistent cookie, revoked on password reset), per-IP throttling on all three credential endpoints, 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, and set APP_BASE_URL to your https:// origin — unset in production, reset and confirmation emails carry http://localhost:5011 links.

Sign-in tells an attacker nothing

An unknown email and a wrong password return the same message and take the same time: the miss path spends the same Argon2id work instead of returning early, so response timing is not an enumeration oracle either. The specific “account locked” message appears only after the password has been verified — the owner learns what happened, someone guessing does not learn that the address exists. AUTH_MIN_PASSWORD_LENGTH (default 12) is enforced through User#password_error, which both sign-up and password reset call, so the reset flow cannot install a password registration would have rejected.

terminal
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.

app/policies/post_policy.sl
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.

app/controllers/posts_controller.sl
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 / policiesthe record, or raises 403
policy_for(record)controllers / policiesthe policy instance
current_user()controllers / policies / viewsthe signed-in User, or nil
signed_in?()controllers / policies / viewstrue when signed in
current_action()anywherethe in-flight action name
forbidden(message?)anywhereraises 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.