Class: RuboCop::Cop::DevDoc::Auth::NoRecordPresenceInPolicy
- Inherits:
-
Base
- Object
- Base
- RuboCop::Cop::DevDoc::Auth::NoRecordPresenceInPolicy
- 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:
-
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 torecord.present?passed for a foreign-tenant id because the missing record arrived as a truthy placeholder. -
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.
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
- #on_send(node) ⇒ Object (also: #on_csend)
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 (node.receiver) add_offense(node.loc.selector, message: format(MSG, method: node.method_name)) end |