Class: RuboCop::Cop::DevDoc::Auth::RolePredicateOutsidePolicy

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

Overview

Role predicates must not be called outside the policy layer.

Rationale

A role predicate (admin?, super_admin?, effective_admin_of_organization?, ...) answers "what may this person do?" — an authorization question. Scattering those calls through controllers and views fragments the permission model: the Pundit policy says one thing, page code quietly decides another.

This cop is the receiver-agnostic companion to DevDoc/Auth/CurrentUserBranching: that cop tracks current_user and its same-file aliases, but taint cannot follow an ivar from a controller into a view, and any wrapper method breaks the chain. Keying on the PREDICATE NAME instead means current_user.admin?, @user.admin?, user.admin?, and viewer.admin? are all equally visible — no alias or wrapper hides the question being asked.

Remedy

Move the decision into the Pundit policy and query it where needed: policy(record).manage? — no role predicate at the call site. Name the policy query after the DECISION it guards (auto_approve?, initiation_blocked_for_admin?), never after the role: a generic getter (user_is_admin?) re-exports the scattering through the policy door — every call site still maps role to behavior itself. Genuine display logic keyed on a role (badges, admin-only hints) belongs in a helper (an excluded path), where the branching is openly presentation-side.

The ratchet

Once page code queries policies by purpose, add any role-shaped getters your policies DO export (user_is_super_admin?, ...) to RolePredicates. Policies are excluded paths, so policy-internal use stays free while new loose call sites in page code flag mechanically.

Configuration

RolePredicates is a closed, per-project list — add every role predicate your User/Member models define:

DevDoc/Auth/RolePredicateOutsidePolicy:
RolePredicates:
  - admin?
  - super_admin?
  - effective_admin_of_organization?
  - explicit_admin_of_organization?

Allowed paths (Exclude:)

By default the cop is silent in:

app/policies/**/*.rb   ← where the predicates belong
app/models/**/*.rb     ← where they are defined/composed
app/helpers/**/*.rb    ← sanctioned display branching
app/views/layouts/**/* ← sanctioned display branching
test/, spec/           ← tests assert role facts as fixture
                       preconditions; they verify the permission
                       model rather than fragment it

Examples:

# bad — controller decides a permission by role
if current_user.admin?
  auto_approve
end

# bad — the alias dodge is equally visible
if @user.admin?
  auto_approve
end

# good — the policy owns the decision; page code queries it
if policy(@organization).auto_approve?
  auto_approve
end

Constant Summary collapse

MSG =
'`%<method>s` is a role predicate — an authorization question that belongs in ' \
'the Pundit policy. Move the DECISION there and query it by purpose ' \
'(`policy(record).auto_approve?`) — name the query after the decision it ' \
'guards, never after the role (`user_is_admin?` just re-exports the ' \
'scattering through the policy door, and belongs in this cop\'s ' \
'RolePredicates list). For pure display branching (badges, admin hints), ' \
'use a helper. Renaming the receiver never exempts it: the flagged thing ' \
'is the question, not who is asked.'.freeze

Instance Method Summary collapse

Instance Method Details

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



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

def on_send(node)
  method_name = node.method_name
  return unless role_predicates.include?(method_name)

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