Rails authorization
Status: Implemented for the admitted public Policy algebra and its Scaffold consumers. Other Policy shapes are listed gaps.
Admitted Policy roots lower to Action Policy record rules, and admitted protected consumers demand relation scopes;
the Policies owner (repository-only) lists both. Public, item, and gate
authorization remain distinct; protected member lookup starts from an authorized relation, and generated controls
repeat the request authorization decision. The Policies
owner (repository-only) also states which destroy? aliases a complete
exact-source operation census permits. Authentication supplies the principal through current_account; generated
Policies, controllers, and views consume it with Action Policy.
On this page
- Direction
- Policy record lowering
- Policy rules
- Scaffold consumption
- Authorization coverage
- Private Policy gate
Direction
Lower explicit Policy meaning with Action Policy and generated denial tests in the first profile. Generalized Web Scaffold output consumes exact public, item, gate, and profile decisions in requests and presentation. Other record sources and Expressions, attachments, native clients, and broader generated proof remain open. The Scaffold lowering includes the admitted indirect collection's destination Policy scope.
The first Rails profile selects Action Policy as the fixed lowering. The cumulative public candidate generates:
- a policy object and operation method from
allow_when; - a named authorized relation from that same Policy Expression when a collection consumer needs one;
- a nullable
userauthorization context backed by the generatedcurrent_accountseam; - exact output-name and pinned Policy-runtime collision checks;
- controller and Scaffold integration that guards protected content and authorizes mutations;
- relation-scoped index and member lookup, record and environment gates, and conditional view controls;
- membership-backed joined scopes only when the full table path has no repeated table; and
- generated positive, denied, guest, absent-record, broken-link, and Scaffold integration tests.
A complete application-wide authorization lowering still needs:
- attachment-byte authorization where protected records own files;
- broader Policy Expression and relation equivalents;
- native Policy output and non-Scaffold consumers;
- framework-wide authorization-verifier coverage beyond the generated Scaffold paths; and
- representative-user, hosted, deployed, and production enforcement proof.
Policy record lowering
Ordinary CompilationInput derives an immutable PolicyRecordLowering from the exact same-generation Domain,
relationship, Account, Policy-resolution, Policy-type, and Scaffold-definition evidence. It admits no Policy root
unless the exact Field-only public Web Account realization succeeds. The closed public algebra admits record
identity against current_account, one direct Reference-to-Account comparison, and a singular matches_policy
delegation whose dependency is also admitted. It omits and lists unsupported expression shapes, missing or
unemitted dependencies, generated Policy names that collide with Project or pinned runtime claims, and rule methods
that collide with the pinned Policy-instance method census.
Relation demand comes only from admitted Scaffold consumers. Bookmark manage_by_user supplies the protected
Bookmark index and member consumers, Rating manage_by_user supplies its update/destroy member lookup, and
Notification read_by_recipient supplies the protected collection. Case, Conversation, ConversationThread, and
Message participant-read rules supply the joined relation scopes needed by their admitted consumers. Only
demanded scopes are emitted; Policy rules with no relation consumer do not mint an unused scope. A gate over
current_account, a profile action resolved directly from current_account, and an omitted projection likewise do
not mint one.
The lowering derives target demand directly from captured Scaffold definitions and authorization rows plus the
typed Policy evidence. Each demanded relation scope filters the exact emitted Account foreign key and returns
relation.none for a guest.
The complete exact-source operation census records every Entity even when it owns zero Policies and independently
checks every Policy UUID and pointer against the submitted document. It permits destroy? to alias to a selected
standard manage? rule only when no exact authored destroy exists. Bookmark and Rating use that alias. An
explicit authored destroy would be direct; Oscar's custom manage_movie_catalog? remains the direct selected
rule for Movie, Person, and Credit destroy instead of acquiring an alias. Missing, stale, forged, or incomplete
census evidence omits the consumer.
PolicyRecordFiles emits one ApplicationPolicy with default_rule nil, one class and RSpec file per admitted owner
Entity, required relation scopes, and complete source provenance. Examples exercise authored operations and relation
visibility with actual records. The Policy owner (repository-only)
distinguishes emitted application cases from upstream identity edge cases. Setup limitations retain guest and
missing-ID guards and disclose remaining coverage in the README. The application-wide dependency plan adds the
qualified Action Policy block and projects the complete pair; application spec discovery runs Policy examples after
schema loading.
Policy rules
Lower the admitted allow_when algebra with Action Policy. Admitted protected consumers demand relation scopes beside
the record rules; the Policies owner (repository-only) lists both. Every
request and displayed Association selects explicit public access or one exact Policy record. Current output omits a
consumer when its record or relation projection cannot preserve the authored Expression, and lists the target gap.
This strict current slice is not a target-wide pre-alpha requirement: later breadth may retain a conventional public
scaffold consumer as disclosed starter code without claiming Policy coverage. Generated method-name collisions or
paths with no useful bootable projection may still select omission. Contradictory duplicate Entity operations remain
semantic errors. Emit default_rule nil, so unknown operations in the realized Policy layer raise
ActionPolicy::UnknownRule.
Public record rules and relation scopes use user&.id.present? as their identity boundary. Ownership then compares
record&.user_id == user.id; self rules compare record == user; membership rules inspect the preloaded members;
and delegation calls allowed_to? with the explicit dependency Policy class and nullable related record.
A conjunction joins its ordinary child expressions with &&, retaining their authored order without blanket
parentheses. Safe navigation handles expected absence without swallowing a wrong method interface.
Guests, ID-less Accounts, missing records, and missing delegated relationships remain
denied. The Action Policy 0.7.6 ownership examples use ordinary identity comparisons,
and its nested authorization API accepts a nil record with an explicit with: class.
Authentication supplies the principal through current_account; Policies do not repeat that responsibility.
The generated seam calls Rodauth's rails_account only for a logged-in session, and
rodauth-rails 2.2.0 builds the model from the resolved authentication account.
Dropping persisted? intentionally permits explicitly supplied unsaved or destroyed Accounts whose IDs match.
Rails 8.1.3.1 defines it as object lifecycle flags, not authentication or current row existence.
This replaces the redundant guards introduced in the initial public Policy renderer without changing admission,
the reviewed GapSet, preloads, Action Policy verification, or the authorization consumers.
Exact authored destroy is direct. A selected standard manage may supply conventional destroy? only when
the complete exact-source operation census proves no authored destroy; generated code aliases
destroy? to manage?. Bookmark and Rating meet that condition. An explicitly selected custom operation,
including Oscar's manage_movie_catalog, remains the direct rule and creates no alias. The Scaffold still owns
route selection and return behavior. Missing census evidence omits the alias and every dependent consumer.
The target-unlowered message.notifications relationship omits only that child. The admitted Message implicit
Ordering emits independently. Neither suppresses the six admitted Case participant Policy roots
or their other consumers.
Scaffold consumption
The Scaffold renderer consumes those decisions directly. Member lookup starts from the authorized relation when
required, and views repeat allowed_to? for protected content and controls. Relations preload the nested Policy,
control, and presentation graph under strict_loading; associated records may enter server memory even when their
display is denied. Action and collection-wide gates authorize before presentation queries. Public indexes, item
authorizations, environment gates, projection gates, and current-account profile authorization keep their distinct
records and preload ownership. The Rails profile owns the preload
lowering. Account is one authenticated /account destination. Its custom profile details and queries retain their
read Policy; editing retains the update Policy. Ordinary native clients admit neither Account nor protected Scaffold
navigation.
The cumulative Scaffold semantics emit Policy record projections plus demanded relation scopes. Oscar Party emits
User, Bookmark, and Rating rules; Bookmark and Rating each gain manage from admitted consumers. Case Chat emits
User, Case, Conversation, Participant, ConversationThread, Message, and Notification rules. Notification gains
read, while the four participant-read Policies gain the joined relation scopes demanded by their consumers.
Generated Web controllers, relations, projections, controls, mutations, and custom Account details consume those exact
decisions. Photogram remains the neutral control and emits no Policy or Scaffold files.
The target-unlowered message.notifications relationship still omits that nested child. The delivery import skip
remains a separate disclosure; Message's exact sent_at, id implicit Ordering now emits. None withholds the
six Case participant Policy roots or their other admitted consumers.
Friendly HTML and Turbo Policy denial responses remain in the generated application.
Authorization coverage
For requests claimed as authorized, configure Action Policy verification from authored semantics rather than
controller-action names alone. An item-authorized collection uses verify_authorized_scoped; a detail, create,
update, gate-authorized collection, or destroy request uses verify_authorized. An explicitly public request,
including a public index, joins the controller's skip_verify_authorized only: list. This keeps realized public
access intentional without weakening coverage for neighboring realized actions. Extra unguarded pre-alpha boilerplate
remains a Policy gap.
Private Policy gate
PolicyGateQualificationInput is a second internal type. It reconstructs that same Account and registration
prerequisite, requires web-only output, and admits only the Movie index targeted by the Account's admitted
favorite_movie Reference, linked to one Account-owned
manage_movie_catalog Policy. Pure same-generation Policy resolution and typing must prove the exact comparison
current_record == current_account; the authorization row must independently select
environment/current_account as its gate record. Target and Project constant collisions for Action Policy,
ruby-next-core, ApplicationPolicy, and the Entity Policy fail before rendering. RSpec examples add no named test
class claim. This path
also qualifies the same fixed Account self surface against the Policy entry's generated claims. Its shared
ApplicationNavigation contains Account self, while Web separately composes the authorized Movie entry. iOS is
declined because this qualifier is Web-only. Public CompilationInput for this exact Association-registration Plan
emits no Policy record and retains its Policy omission in the reviewed GapSet. Only the private qualifier emits this
controller, view, navigation, and request gate; it does not enter the durable Compilation lifecycle.
The Policy path contributes Action Policy ~> 0.7.6 to the application-wide dependency plan. Projection from the
maximal universe pins Action Policy 0.7.6 and ruby-next-core 1.2.1. Five Policy-specific renderers emit
ApplicationPolicy, the same-Account UserPolicy, its generated RSpec example, the protected index controller, and
the read-only index view. Shared web tasks add the route, locale, pagination, and conditional navigation. The
application controller authorizes through current_account, rescues only ActionPolicy::Unauthorized through the
shared Policy denial response (repository-only), and leaves all other
exceptions visible. The index controller verifies authorization, authorizes current_account before it constructs the
Movie relation, and then paginates. No relation scope, member lookup, mutation, Association authorization, native
output, or broader Policy Expression is admitted by that private gate.
The Policy renderers likewise select their five paths only for the exact private gate.
A distinct private Policy-gate qualifier accepts optional private Association registration only when the gated
Movie has a direct required short_text Primary Descriptor; its one-hop descriptor shape omits that input. It
requires the Movie index targeted by favorite_movie and one Account-owned manage_movie_catalog Policy typed as
current_record == current_account and explicit environment/current_account gate-record evidence. It pins
Action Policy 0.7.6 and ruby-next-core 1.2.1, emits ApplicationPolicy, a same-Account UserPolicy, its generated
same-account RSpec example, and controller authorization before the Movie relation. Denial rescues only
ActionPolicy::Unauthorized through the shared HTML 403 page, retaining empty responses for other formats;
navigation uses the same decision for visibility. This private
record-only slice has no relation scope, member lookup, mutation, Association authorization, native client, or
broader Expression support. Public record projection is separate and does not reuse this controller/view gate.