Supporting reference · Working design and evidence, not a promise of complete support.

On this page

Rails authorization

Status: Implemented for the Policy Expression grammar and its Scaffold consumers. The Policies owner (repository-only) lists the leaves that remain gaps.

Each Policy lowers to an Action Policy record rule and, unless only current_account gates and create forms consume it, a relation scope; the Policies owner (repository-only) owns 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

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, and uploaded files follow the decisions of the pages that show them. Other record sources and Expressions, 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 complete application-wide authorization lowering still needs:

Policy record lowering

Ordinary CompilationInput derives an immutable PolicyRecordLowering from the exact same-generation Domain, relationship, Account, Policy-resolution, Policy-type, Predicate-type, and Scaffold-definition evidence. It generates no Policy unless the exact Field-only public Web Account realization succeeds. PolicyConditionBuilder turns each typed Expression into a PolicyCondition tree, and PolicyConditionSource renders that tree as the rule body and the scope body. A leaf outside the lowered vocabulary, a read of a Counter whose counting is left out and so reads 0, directly, through the signed-in Account, or through an Association's Predicate, a delegation to an omitted Policy, or a negated delegation or exists that depends on a stub makes the Policy an Unwritten condition instead, which PolicyRecordFiles renders as the stub that Policy stubs describes. An operation whose plain rule method is in the pinned Policy-instance method census, such as present?, or is show_files?, which uploads generate, gets a private rule name with a _rule? suffix, such as present_rule?. The Compiler writes every caller, and the Analyzer warns with foundation_plan.rails_target.policy.rule_renamed. Current output still omits a Policy, with a GapSet reason, when a generated Policy name collides with a Project or pinned runtime claim, when its renamed rule belongs to another Policy on the same Entity, or when it is part of a delegation cycle.

A relation scope is emitted for every Policy unless all of its consumers are current_account gates or create forms, and for every Policy an emitted scope delegates to. Admitted Scaffold consumers that look records up through the relation keep their exact source pointers as the scope's demand; an item consumer without a scope is omitted. A Policy whose rule needs the Account wraps its scope in if user&.id.present? and returns relation.none for a guest. A delegation calls authorized_scope on the delegated Policy and repeats its Account guard.

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 allow_when with Action Policy. A generated Policy gains a record rule and, unless only gates and create forms use it, a relation scope, as Policy record lowering describes; the Policies owner (repository-only) lists the leaves that make it a stub. Every request and displayed Association selects explicit public access or one exact Policy record. Generated method-name collisions and paths with no useful bootable projection omit their consumers and list the target gap. The Compiler never keeps an unguarded public consumer in their place. Contradictory duplicate Entity operations remain semantic errors, though analysis reports them only as warnings until format 0.24 (repository-only), and each omitted Policy's GapSet record names the other. Emit default_rule nil, so unknown operations in the realized Policy layer raise ActionPolicy::UnknownRule.

Record rules and relation scopes use user&.id.present? as their identity boundary wherever they read the Account. Ownership then compares record&.user_id == user.id; self rules compare record == user; collection exists uses any? over the preloaded Association; a singular path chains safe navigation; and delegation calls allowed_to? with the explicit dependency Policy class and nullable related record. A conjunction joins its children with &&, and a disjunction joins them with ||, parenthesized only where precedence requires it. A negated singular exists allows a missing related record and negates its whole condition, because ! binds more tightly than ==: record.movie.nil? || !(record.movie.status == "won"). Safe navigation handles expected absence without swallowing a wrong method interface; where nil would not already be false, as for != or an ordered comparison, the rule checks presence first. Guests, ID-less Accounts, missing records, and missing delegated relationships remain denied. The Action Policy 0.7.6 ownership examples use ordinary identity comparisons, its nested authorization API accepts a nil record with an explicit with: class, and its scoping guide shows authorized_scope inside a relation_scope.

Policy stubs

Under the code-worth-keeping requirement, a Policy the Compiler does not write keeps its class and every consumer, as the stub reach decision (repository-only) records:

def read?
  # club.read_open is not written yet.
  # README.md describes it; spec/policies/club_policy_spec.rb covers it.
  false
end

relation_scope(:read) do |relation|
  raise NotImplementedError,
    "club.read_open is not written yet. README.md describes it; spec/policies/club_policy_spec.rb covers it."
end

Action Policy 0.7.6 sends authorize! and allowed_to? through the same rule call, so the rule denies everywhere: a page answers 403, or sends a guest to sign-in, and the navigation link, controls, inline forms, and record links that ask the rule stay hidden until it is written. The scope keeps its relation parameter, because Action Policy defines the block as a method that receives it. Every item-authorized index, show, edit, update, and destroy looks its records up through the scope, so those pages raise before the rule is asked. So do a parent's collection and its nested pages, Reference options and submitted-ID lookups on another Entity's form, and Home when Home shows the stubbed index. A Policy that delegates to a stub calls the stub's rule and scope unchanged. A negated delegation, or a negated exists over records that delegate to one, would allow what the written rule may deny, so that Policy becomes a stub too. PolicyStubs names the pages that deny and raise for the README and for qualification.

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. Member actions that share one lookup load the record in a set_<record> callback, while each action keeps its own authorize!. A current_account gate becomes an earlier callback, so it still authorizes before that lookup; the Rails profile shows the shape. 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 their relation scopes. Oscar Party emits User, Bookmark, and Rating rules; Bookmark and Rating each gain a manage scope. Case Chat emits User, Case, Conversation, Participant, ConversationThread, Message, and Notification rules. Its participant-read scopes select through Participant.active subqueries, and its delegating scopes call the delegated Policy's scope. 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 for signed-in accounts. A guest is sent to sign in through Rodauth and returns afterwards, as the Policies owner (repository-only) describes.

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. Generated controllers add no unrequested action. An action whose Policy the Compiler cannot lower stays behind a stubbed Policy that admits nobody.

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 handler: sign-in for a guest, the HTML 403 page for a signed-in account, and 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.

References marked “repository-only” name implementation or internal material outside this public guide. They are intentionally not links. Publishing a design does not prove its implementation.