Class: RuboCop::Cop::DevDoc::Auth::NoRecordPresenceInPolicy

Inherits:
Base
  • Object
show all
Defined in:
lib/rubocop/cop/dev_doc/auth/no_record_presence_in_policy.rb

Overview

Policy code must not use record.present? / record.blank? as an authorization signal.

Rationale

A policy's record answers "what is being acted on". Whether it is present is a loading outcome — it only proves the controller's lookup resolved something — never an authorization fact. Gating on presence fails in two distinct ways:

  1. Fail-open via placeholder records. Pundit-style stacks support headless policies, where the "record" is a Symbol naming the policy rather than a model instance — and resource-resolution layers can fall back to that Symbol when a lookup resolves nothing. A gate written as record.present? is then true for exactly the request it was written to refuse: the one whose record could not be resolved. This is an observed production incident shape — a cross-tenant guard reduced to record.present? passed for a foreign-tenant id because the missing record arrived as a truthy placeholder.

  2. Presence asserts nothing about the record. Even when the record is genuinely a model instance, present? says nothing about its type, its tenant, or its relationship to the user. Authorization must bind the user to the record — a type check plus the ownership/attribute conditions that actually make the decision.

Remedy

Replace the presence check with a type check — it subsumes the nil guard and additionally asserts the record is the thing the policy believes it is — and bind the decision to the record's attributes:

❌
authorize :create do
admin_of_organization?(organization) && record.present?
end

✔️
authorize :create do
admin_of_organization?(organization) && record.is_a?(Member)
end

✔️ — bind to the user, not to mere existence
def viewing_own_record?
record.is_a?(Member) && record.user_id == user.id
end

NOTE: The cop is deliberately narrow: it flags present?/blank? only on the literal record receiver. Aliases (def member = record then member.present?) are not tracked — reviewers must catch those. Nil guards that precede attribute checks (return false if record.nil?) are not flagged; they fail closed and the attribute check that follows carries the authorization.

Examples:

# bad — presence as the authorization answer
authorize :create do
  record.present?
end

# bad — negated form, same loading fact
def member_missing?
  record.blank?
end

# good — type check binds the gate to a real record
authorize :create do
  record.is_a?(Member)
end

# good — authorization binds the user to the record
def viewing_own_record?
  record.is_a?(Member) && record.user_id == user.id
end

Constant Summary collapse

MSG =
'`record.%<method>s` is a loading fact, not an authorization fact — resolution ' \
'fallbacks can make it true via a placeholder even when nothing was resolved. ' \
'Use a type check (`record.is_a?(SomeModel)`) plus the ownership/attribute ' \
'conditions that actually authorize this user.'.freeze
RESTRICT_ON_SEND =
%i[present? blank?].freeze

Instance Method Summary collapse

Instance Method Details

#on_send(node) ⇒ Object Also known as: on_csend



86
87
88
89
90
# File 'lib/rubocop/cop/dev_doc/auth/no_record_presence_in_policy.rb', line 86

def on_send(node)
  return unless bare_record?(node.receiver)

  add_offense(node.loc.selector, message: format(MSG, method: node.method_name))
end