rails-authorization
Design and review authorization as an explicit Rails policy, resource-scope, tenant, and execution-boundary contract.
SKILL.mdskills/rails-authorization/SKILL.md
GitHubRails Authorization Engineering
Authorization answers what an already identified actor is allowed to do to a resource in a context. Authentication establishes identity; authorization establishes permission. Do not merge the two contracts.
Purpose
Treat authorization as an explicit server-side permission boundary covering actor, action, resource, tenant, context, execution boundary, and denial semantics.
Activate when
Use this skill for policy objects, Pundit, CanCanCan, custom authorization, roles or permissions, protected controller/service/job/API/Action Cable actions, object-level authorization and IDOR prevention, tenant isolation, collection authorization, contextual permissions, authorization caching, auditability, and security regression work.
Repository inspection
Before changing authorization:
- Resolve Ruby, Rails, and authorization dependency versions.
- Identify the authoritative authorization mechanism already used.
- Locate policy/ability/permission classes and base abstractions.
- Inspect controller, service, job, API, mailer, and realtime entry points.
- Identify tenant/account ownership boundaries.
- Inspect routes and resource lookup before authorization.
- Inspect tests and factories for existing authorization contracts.
- Search for bypasses such as direct find-by-ID, ad-hoc role checks, and unscoped queries.
- Inspect background-job and cached-result paths.
- Preserve repository conventions unless there is evidence they are unsafe.
Do not introduce a second authorization framework merely because it is familiar.
Authorization model
Model authorization as:
authorize(actor, action, resource, context) -> allow | deny
Context may include tenant/account, organization membership, resource state, ownership, delegated authority, time, feature state, or service identity. Keep policy inputs explicit and avoid hidden global state.
Policy boundary
A policy should own a coherent permission decision, not persistence, rendering, authentication, or unrelated workflows.
Good policy responsibilities include action permission, ownership, role/capability checks, state-dependent permission, contextual constraints, and policy composition.
Avoid policies that mutate resources or perform network calls.
Object-level authorization
Resource lookup and authorization must be composed deliberately.
A direct Model.find(id) followed by authorization can still expose cross-tenant existence through lookup behavior. Prefer an authorized relation or equivalent tenant/resource boundary before resolving the object.
The exact API depends on the authorization library and resolved version.
Collection authorization
Authorizing one member does not authorize a collection.
Define a separate scope contract for index/list endpoints, search, exports, reports, associations, dashboards, and background enumeration.
The collection scope must be enforced at the data-access boundary, not merely filtered after loading rows.
Tenant isolation
Treat tenant/account membership as a security boundary.
For every tenant-owned resource determine the authoritative tenant identifier, actor membership check, resource tenant relationship, cross-tenant lookup behavior, nested association behavior, background-job behavior, cache-key isolation, and export/report isolation.
Never trust a client-supplied tenant ID as proof of tenancy.
Roles, permissions, and contextual authorization
Prefer capabilities that express the actual operation over scattered role-name conditionals.
Distinguish role, permission/capability, ownership, and contextual condition. Do not turn every business condition into a global role.
State-dependent authorization
Authorization often depends on resource state such as draft/published, active/suspended, or open/closed.
Keep authorization state checks aligned with the domain invariant. If a state transition is security-sensitive, enforce the invariant transactionally where necessary.
Controllers and APIs
Authorization must happen server-side before the protected side effect.
- Authenticate the actor.
- Establish tenant/context.
- Resolve an authorized resource scope.
- Authorize the action.
- Validate input.
- Perform the side effect.
- Return the deliberate denial/not-found contract.
Do not use frontend route guards as authorization.
GraphQL endpoints reach the same records through many query paths, so authorizing the /graphql controller action is not enough. Enforce the policy on every type, field, mutation, and list. The graphql-ruby hooks are in the GraphQL reference of rails-api-integration; this skill still owns the decision.
Verify denial with tests, not by trying an action by hand. For each protected action, a test as an actor without permission must observe the deliberate outcome: Pundit's Pundit::NotAuthorizedError, CanCanCan's CanCan::AccessDenied, or the repository's own denial response. A manual console or browser check proves nothing for the next change.
Service objects and domain workflows
A service invoked from multiple entry points must not assume that the controller already authorized it.
Choose an explicit boundary: authorize at the service boundary for security-sensitive application operations, or require an already-authorized capability/context and make that contract explicit.
Do not silently trust a caller because a controller normally calls the service.
Background jobs
Jobs run outside the original request authorization context.
Never serialize live credentials merely to carry authorization. Pass stable identifiers, re-resolve actor/tenant/resource state at execution time, and re-authorize the operation.
Define behavior for deleted, disabled, or membership-revoked actors. For destructive jobs, stale authorization must fail closed.
Action Cable and realtime
Authorize connection identity, channel subscription, stream/resource access, and actions that mutate state.
A valid WebSocket connection is not blanket authorization for every stream or action. Re-check resource authorization when the security boundary changes.
Webhooks and service identities
Inbound webhooks need authenticity verification and an explicit authorization model. A valid signature proves origin according to the integration contract; it does not automatically grant arbitrary application privileges.
Service-to-service identities should have bounded capabilities.
Authorization versus validation and strong parameters
These are separate controls:
- strong parameters: which input fields may be assigned;
- validation: whether data is structurally/domain-valid;
- authorization: whether this actor may perform the operation.
Passing strong parameters never implies permission.
Fail closed
For security-sensitive operations, missing actor, unknown tenant, missing policy, ambiguous ownership, and stale membership must deny according to an explicit safe failure contract.
Do not convert authorization exceptions into success or silently skip authorization because a policy is unavailable.
TOCTOU and transactional boundaries
If authorization depends on mutable state, identify the gap between permission check and mutation.
Examples include membership revoked after authorization, ownership changed, or approval state changed.
For security-critical invariants, combine authorization with the appropriate transaction, lock, and database constraint strategy. Authorization alone is not a concurrency primitive.
Caching
Never cache an authorization decision without including every input that can change the decision.
Cache identity may need actor, tenant, action, resource/version, permission version, and policy/context version.
When membership, role, ownership, or policy state changes, define invalidation semantics. Prefer short-lived caching when invalidation is difficult.
Audit and observability
Record security-relevant authorization outcomes where required: actor, tenant, action, resource type/id, decision, reason/category, and correlation ID.
Do not log secrets or sensitive request data merely to explain a denial. Use bounded reason codes rather than arbitrary policy internals.
Testing strategy
Test both allow and deny paths.
Required categories include actor/resource ownership, cross-tenant access, collection scope, role/capability boundaries, resource state, missing/disabled actor, direct service invocation, background-job re-authorization, API authorization, Action Cable subscription/action authorization, stale membership, authorization-cache invalidation, concurrent state changes where relevant, IDOR regression, and enumeration-safe denial behavior.
Prefer policy/unit tests for decision logic and request/system tests for boundary wiring.
Reference example
Authorization as objects: a policy answering one question per method, and a scope that owns the query boundary.
class InvoicePolicy
def initialize(user, invoice)
@user = user
@invoice = invoice
end
def show? = owner? || @user.admin?
def destroy? = false # invoices are voided, never destroyed
private
def owner? = @invoice.user_id == @user.id
end
class InvoiceScope
def initialize(user, scope)
@user = user
@scope = scope
end
def resolve
@user.admin? ? @scope.all : @scope.where(user_id: @user.id)
end
end
# Controller usage:
# head :forbidden unless InvoicePolicy.new(current_user, @invoice).show?
# @invoices = InvoiceScope.new(current_user, Invoice).resolve
Agent review checklist
- Is authentication distinct from authorization?
- Is there one authoritative authorization mechanism?
- Are resource lookup and authorization safely composed?
- Is collection scoping enforced at the query boundary?
- Is tenant isolation explicit?
- Are jobs, services, and realtime independently protected?
- Are denial semantics deliberate?
- Does mutable authorization state create a TOCTOU risk?
- Are authorization caches correctly keyed and invalidated?
- Are allow and deny paths executable tests?
- Is the change limited to the requested boundary?
Verification
For an authorization change:
identify mechanism -> inspect lookup/scope boundaries -> define permission and tenant contracts -> protect service/job/API/realtime boundaries -> test allow/deny and regression cases -> run security/contract validation -> inspect CI evidence
Do not claim authorization safety without verifying the actual repository path.
Source foundation
Primary Rails security guidance: https://guides.rubyonrails.org/security.html
The repository may use Pundit, CanCanCan, Rails-native mechanisms, or custom policy code. Framework/library APIs must be verified against resolved dependency versions.
Related skills: rails-authentication, rails-security, rails-security-engineering, rails-action-controller, rails-active-record, rails-active-job, rails-action-cable, rails-database-engineering, rails-test-engineering.
Rails authorization changes
For authorization changes:
- resolve the Ruby/Rails and authorization-library versions and identify the repository's authoritative mechanism before implementation;
- keep authentication and authorization separate; authentication establishes identity while authorization establishes permission;
- inspect resource lookup and collection scopes for IDOR and cross-tenant enumeration before adding policy checks;
- treat tenant/account membership as a security boundary and never trust a client-supplied tenant identifier as proof;
- reuse the existing Pundit, CanCanCan, custom policy, or capability mechanism instead of creating a parallel policy engine;
- authorize services, jobs, APIs, and Action Cable independently when they can be invoked outside the original controller request;
- re-resolve mutable membership/resource state in background jobs and fail closed when authorization is stale;
- keep strong parameters, validation, and authorization as distinct controls;
- review TOCTOU windows when authorization depends on mutable state and coordinate with transactions, locks, and database constraints;
- include tenant, actor, action, resource, and relevant permission version in authorization cache identity and define invalidation;
- never log credentials or sensitive policy inputs merely to explain an authorization decision;
- add deterministic allow/deny, cross-tenant, collection-scope, direct-service, job, API/realtime, and IDOR regression tests;
- verify the actual repository contract and report only observed validation evidence.
Authentication and authorization boundary
authentication and authorization remain separate concerns: authentication establishes identity and session state; authorization decides permitted actions within that context.