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

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.