Mountable Rails engine · authorization

Authorization as data you edit in a UI — not rules you hardcode.

Permissions auto-derived from your routes. Roles as editable rows. Per-record scoped grants. One ambient context so allowed_to? gives the same answer in controllers, views, and components.

$ bundle add current_scope
The idea

Stop scattering if user.admin? across the codebase.

Hardcoded checks drift, hide in views, and need a deploy to change. CurrentScope moves the policy into data your team edits in a UI — and reads it back the same way everywhere.

The usual way

Rules compiled into the app

Every rule is code. Changing what “Reviewer” means is a pull request, a review, and a deploy. The view and the controller can silently disagree.

# scattered, and a deploy to change
if user.admin? || user.role == "reviewer"
  approve!(report)
end
# …and did the view remember to hide the button?
With CurrentScope

Policy is data, read one way

Roles are editable rows in the mounted UI. One ambient resolver answers every check — the button and the gate can’t disagree, because they ask the same question.

# same answer in controller, view, component
allowed_to?(:approve, report)

# change "Reviewer" in the UI — no deploy
What you get

Add the gem, run the generator, and it’s wired.

Permissions from your routes

Every controller#action pair is a permission. Add an OrdersController and its actions appear in the grid — zero wiring.

Roles as rows, not classes

A role is a named, editable bundle of ticked cells on a controller × action grid. Change what “Reviewer” means without a deploy.

Scoped roles

The same role on one specific record. “Editor of Project #7” grants nothing on Project #8 — enforced by the same resolver.

Separation-of-duties veto

Opt in by listing actions. Once on, whoever initiated a record can never approve it — not grantable, not clickable, overriding even full access. A structural guarantee.

Fail-closed by default

No grant means denied. Everything is a permission — even the baseline actions every signed-in user can do. Nothing is implicitly allowed.

One ambient context

The subject flows through CurrentAttributes from the gate to the smallest ViewComponent. The view can’t disagree with the gate — no current_user threading, ever.

The resolver

One fixed decision order. Every check, everywhere.

There is exactly one path from “who is this and what do they want” to allow or deny. Controllers, views, and components all walk it — so they can never drift apart.

1
SoD veto
Initiator of this record? (opt-in) — overrides everything
DENY
2
full_access
Role grants everything, present and future
ALLOW
3
org-wide role
Role’s permission set includes this action
ALLOW
4
scoped role
A role held on this record
ALLOW
5
otherwise
Nothing matched
DENY

Why the order matters

The veto sits first on purpose. Separation of duties isn’t a permission you can out-rank — a full-access owner who filed a report still can’t approve their own. It’s not configurable in the UI, because it’s a control, not a preference.

Everything below is additive: full access short-circuits, then org-wide, then per-record. Miss all of them and you’re denied. There is no implicit allow.

The management UI

Mounted at /current_scope. Self-contained, light & dark.

No web fonts, no build step, CSP-safe. The people who own the policy edit it here — developers never become a ticketing queue for “can you give Dana approve rights?”.

permission grid · Reviewer
The permission grid: one row per controller, action columns (read, create, update, destroy, approve), with ticked cells glowing blue and a partial group shown indeterminate.
Permission grid. One row per controller, action groups derived from routes. Ticked cells glow; a partial group reads as indeterminate.
subjects · search
The subjects page: everyone who can hold a role, their single org-wide role in a dropdown, and per-record scoped roles as chips, with a server-side search box.
Subjects. Everyone who can hold a role, their org-wide role, and per-record scoped grants. Server-side search across all of them.
roles
The roles index listing editable roles.
members
The members view showing who holds a given role.
events
The append-only events ledger of authorization changes.
In your code

Ask once. The same answer, anywhere.

allowed_to? lives in controllers and views via one concern, and in any PORO or ViewComponent by mixing in CurrentScope::Permissions. The list-side question has an answer too.

# key derived from the record → reports#approve
allowed_to?(:approve, report)

# class form for collection actions
allowed_to?(:create, Report)

# explicit key when you need it
allowed_to?("admin/reports#approve")
class ApproveButtonComponent < ViewComponent::Base
  include CurrentScope::Permissions

  def render?
    !report.approved? && allowed_to?(:approve, report)
  end
end

# the button literally can't render when the gate would refuse
# allowed_to? answers "may I act on THIS record?"
# scope_for answers "WHICH records may I act on?"
# — from the same roles, permissions, and scoped grants.

def index
  @projects = scope_for(Project)
end

# the list and the per-record gate stay one source of truth
class ApplicationController < ActionController::Base
  include CurrentScope::Context   # ambient subject from current_user
  include CurrentScope::Guard     # fail-closed gate on every action
end

# opt out only where authz doesn't apply (sign-in, webhooks)
class SessionsController < ApplicationController
  skip_before_action :current_scope_check!
end
Quickstart

Mounted and gating in five steps.

Add the gem and install the engine.
# Gemfile
gem "current_scope"
Run the generator and migrations.
bin/rails generate current_scope:install
bin/rails current_scope:install:migrations && bin/rails db:migrate
Include the concerns in ApplicationController.
include CurrentScope::Context   # sets Current.user from current_user
include CurrentScope::Guard     # fail-closed gate on every action
Bootstrap the first full-access admin (the UI needs one to enter).
bin/rails current_scope:grant SUBJECT_ID=1
Manage everything at /current_scope — the role grid, org-wide assignments, scoped grants.

Authorization your team can read.

A structural guarantee where you need one, an editable grid everywhere else. Add it to a real Rails app in minutes.