Module: CafeCar::Policy

Extended by:
ActiveSupport::Concern
Included in:
ApplicationPolicy
Defined in:
lib/cafe_car/policy.rb

Instance Method Summary collapse

Instance Method Details

#association_for_attribute(attribute) ⇒ Object



144
145
146
# File 'lib/cafe_car/policy.rb', line 144

def association_for_attribute(attribute)
  info(attribute).reflection&.name
end

#canonical_filter(key, klass = model) ⇒ Object

Canonical form of a (possibly dotted) filter key: each association hop keeps its association name and the terminal foreign key resolves to its association (client.owner_idclient.owner), so a declaration and the param a control posts compare equal regardless of which form the host wrote. Walks segment by segment through the associated models; a segment that isn't a real association (a bad hop, or the terminal attribute) passes through untouched, so an undeclared path simply fails the membership check — it never reaches SQL.



92
93
94
95
96
97
98
# File 'lib/cafe_car/policy.rb', line 92

def canonical_filter(key, klass = model)
  head, dot, rest = key.to_s.partition(".")
  return (info_for(klass, head).reflection&.name || head).to_s if dot.empty?

  ref = klass.reflect_on_association(head) or return key.to_s
  "#{head}.#{canonical_filter(rest, ref.klass)}"
end

#displayable_associationsObject



112
113
114
115
116
117
# File 'lib/cafe_car/policy.rb', line 112

def displayable_associations
  model.reflections.values
       .select { |a| !a.options[:autosave] && !a.options[:polymorphic] }
       .reject { _1.class_name =~ /^ActiveStorage::/ }
       .map    { _1.name.to_sym }
end

#displayable_attribute?(attribute) ⇒ Boolean

Returns:

  • (Boolean)


140
141
142
# File 'lib/cafe_car/policy.rb', line 140

def displayable_attribute?(attribute)
  attributes.displayable.include?(attribute.to_sym)
end

#filtered_attribute?(attribute) ⇒ Boolean

Returns:

  • (Boolean)


132
133
134
# File 'lib/cafe_car/policy.rb', line 132

def filtered_attribute?(attribute)
  model.inspection_filter.filter_param(attribute, nil).present?
end

#info(method) ⇒ Object



10
11
12
13
# File 'lib/cafe_car/policy.rb', line 10

def info(method)
  @info         ||= {}
  @info[method] ||= CafeCar[:FieldInfo].new(model:, method:)
end

#info_for(klass, method) ⇒ Object



100
# File 'lib/cafe_car/policy.rb', line 100

def info_for(klass, method) = CafeCar[:FieldInfo].new(model: klass, method:)

#logo_attributeObject



104
105
106
# File 'lib/cafe_car/policy.rb', line 104

def logo_attribute
  model.info.fields.listable.attachments.first&.method
end

#modelObject



6
7
8
# File 'lib/cafe_car/policy.rb', line 6

def model
  @model ||= object.try(:klass) or object.is_a?(Class) ? object : object.class
end

#permitted_association?(name) ⇒ Boolean

Returns:

  • (Boolean)


123
124
125
126
127
128
129
130
# File 'lib/cafe_car/policy.rb', line 123

def permitted_association?(name)
  ref = model.reflect_on_association(name)

  return false if ref.has_one?
  return permitted_attribute?(ref.foreign_key) if ref.belongs_to?

  permitted_attribute?("#{ref.name.to_s.singularize}_ids")
end

#permitted_attribute?(attribute) ⇒ Boolean

Returns:

  • (Boolean)


136
137
138
# File 'lib/cafe_car/policy.rb', line 136

def permitted_attribute?(attribute)
  permitted_attribute_keys.include?(attribute.to_sym)
end

#permitted_attribute_keysObject



119
120
121
# File 'lib/cafe_car/policy.rb', line 119

def permitted_attribute_keys
  permitted_attributes.flat_map { |a| a.try(:keys) || a }
end

#permitted_bulk_actionsObject

The bulk actions offered on this model's index table — the policy is the source of truth. Each name maps to a policy predicate (name?, authorized per record in Controller#batch) and a model bang method (name!, applied to each). Ships with :destroy; a host lists its own (e.g. %i[publish destroy]) by overriding this, with no registration anywhere else. Return [] to offer none.



24
# File 'lib/cafe_car/policy.rb', line 24

def permitted_bulk_actions = %i[destroy]

#permitted_collection_actionsObject

The custom actions offered on the whole collection — rendered in the index toolbar. The name? predicate authorizes (asked of the model class, not a record) and name! runs on the policy scope, which ActiveRecord delegates to a class method (see Controller#collection_action). Empty by default.



37
# File 'lib/cafe_car/policy.rb', line 37

def permitted_collection_actions = []

#permitted_fieldsObject



108
109
110
# File 'lib/cafe_car/policy.rb', line 108

def permitted_fields
  @permitted_fields ||= permitted_attribute_keys.map { info _1 }
end

#permitted_filter?(attribute) ⇒ Boolean

Is attribute filterable? Compares the attribute's canonical form against the policy's #permitted_filters, canonicalized the same way — so a foreign key and its association match (client_id:client), and a nested dot-path (client.status) is honored only when its FULL path is permitted. The lone implicit exception is a permitted association's .id set-membership control (see #set_membership?), which needs no separate declaration.

Returns:

  • (Boolean)


64
65
66
67
# File 'lib/cafe_car/policy.rb', line 64

def permitted_filter?(attribute)
  path = canonical_filter(attribute.to_s)
  permitted_filter_paths.include?(path) || set_membership?(path)
end

#permitted_filter_pathsObject

The permitted filters as canonical dot-paths (foreign keys resolved to their associations) — the gate's whitelist, memoized. A nested filter is permitted iff its canonical path is a member, exactly like a top-level one.



72
73
74
# File 'lib/cafe_car/policy.rb', line 72

def permitted_filter_paths
  @permitted_filter_paths ||= permitted_filters.map { canonical_filter(_1.to_s) }
end

#permitted_filtersObject

The attributes (columns and associations) a user may filter an index by — the same policy-is-source-of-truth pattern: the filter UI enumerates this list, and Controller::Filtering drops URL filter keys that aren't on it. Defaults to attributes.displayable (Rails' parameter filter already strips password/token columns); a host overrides this to narrow or widen the list.



50
# File 'lib/cafe_car/policy.rb', line 50

def permitted_filters = attributes.displayable

#permitted_member_actionsObject

The custom actions offered on a single record — rendered on the show page's Actions card and on each index row. Same derivation as #permitted_bulk_actions: the name? predicate authorizes and the record's name! bang method runs (see Controller#member_action). Empty by default; a host lists its own (e.g. %i[publish]) with no registration anywhere else.



31
# File 'lib/cafe_car/policy.rb', line 31

def permitted_member_actions = []

#permitted_metricsObject

The metrics the dashboard renders for this model by default — the same policy-is-source-of-truth pattern as #permitted_bulk_actions. Each name is a model scope (:all = the whole relation) whose row count becomes a tile; the metrics view helper reads this list. Empty by default (opt in per model).



43
# File 'lib/cafe_car/policy.rb', line 43

def permitted_metrics = []

#permitted_scope?(name) ⇒ Boolean

Returns:

  • (Boolean)


102
# File 'lib/cafe_car/policy.rb', line 102

def permitted_scope?(name) = permitted_scopes.include?(name.to_sym)

#permitted_scopesObject

The named model scopes invokable as URL filter params — ?published=true calls the published scope. Empty by default (opt in per model): a scope key reaches QueryBuilder#scope!, which can invoke any public class method with a URL-supplied argument, so a host lists the safe ones explicitly.



56
# File 'lib/cafe_car/policy.rb', line 56

def permitted_scopes = []

#policy(object) ⇒ Object



4
# File 'lib/cafe_car/policy.rb', line 4

def policy(object) = Pundit.policy(user, object)

#set_membership?(path) ⇒ Boolean

<assoc>.id is the association-membership control (?line_items.id[]=): pick associated records by id. It's sanctioned whenever the association itself is a permitted filter, so the has_many _has_many_filter control needs no separate .id declaration — the same rule that let it through before nested paths.

Returns:

  • (Boolean)


80
81
82
83
# File 'lib/cafe_car/policy.rb', line 80

def set_membership?(path)
  parent, _, leaf = path.to_s.rpartition(".")
  leaf == "id" && permitted_filter_paths.include?(parent)
end

#title_attributeObject



15
16
17
# File 'lib/cafe_car/policy.rb', line 15

def title_attribute
  @title_attribute ||= attributes.displayable.first
end