Module: CafeCar::Policy
- Extended by:
- ActiveSupport::Concern
- Included in:
- ApplicationPolicy
- Defined in:
- lib/cafe_car/policy.rb
Instance Method Summary collapse
- #association_for_attribute(attribute) ⇒ Object
-
#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_id→client.owner), so a declaration and the param a control posts compare equal regardless of which form the host wrote. - #displayable_associations ⇒ Object
- #displayable_attribute?(attribute) ⇒ Boolean
- #filtered_attribute?(attribute) ⇒ Boolean
- #info(method) ⇒ Object
- #info_for(klass, method) ⇒ Object
- #logo_attribute ⇒ Object
- #model ⇒ Object
- #permitted_association?(name) ⇒ Boolean
- #permitted_attribute?(attribute) ⇒ Boolean
- #permitted_attribute_keys ⇒ Object
-
#permitted_bulk_actions ⇒ Object
The bulk actions offered on this model's index table — the policy is the source of truth.
-
#permitted_collection_actions ⇒ Object
The custom actions offered on the whole collection — rendered in the index toolbar.
- #permitted_fields ⇒ Object
-
#permitted_filter?(attribute) ⇒ Boolean
Is
attributefilterable? 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. -
#permitted_filter_paths ⇒ Object
The permitted filters as canonical dot-paths (foreign keys resolved to their associations) — the gate's whitelist, memoized.
-
#permitted_filters ⇒ Object
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.
-
#permitted_member_actions ⇒ Object
The custom actions offered on a single record — rendered on the show page's Actions card and on each index row.
-
#permitted_metrics ⇒ Object
The metrics the dashboard renders for this model by default — the same policy-is-source-of-truth pattern as #permitted_bulk_actions.
- #permitted_scope?(name) ⇒ Boolean
-
#permitted_scopes ⇒ Object
The named model scopes invokable as URL filter params —
?published=truecalls thepublishedscope. - #policy(object) ⇒ Object
-
#set_membership?(path) ⇒ Boolean
<assoc>.idis the association-membership control (?line_items.id[]=): pick associated records by id. - #title_attribute ⇒ Object
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_id → client.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_associations ⇒ Object
112 113 114 115 116 117 |
# File 'lib/cafe_car/policy.rb', line 112 def displayable_associations model.reflections.values .select { |a| !a.[:autosave] && !a.[:polymorphic] } .reject { _1.class_name =~ /^ActiveStorage::/ } .map { _1.name.to_sym } end |
#displayable_attribute?(attribute) ⇒ 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
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_attribute ⇒ Object
104 105 106 |
# File 'lib/cafe_car/policy.rb', line 104 def logo_attribute model.info.fields.listable..first&.method end |
#model ⇒ Object
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
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
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_keys ⇒ Object
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_actions ⇒ Object
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_actions ⇒ Object
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_fields ⇒ Object
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.
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_paths ⇒ Object
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_filters ⇒ Object
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_actions ⇒ Object
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_metrics ⇒ Object
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
102 |
# File 'lib/cafe_car/policy.rb', line 102 def permitted_scope?(name) = permitted_scopes.include?(name.to_sym) |
#permitted_scopes ⇒ Object
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.
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_attribute ⇒ Object
15 16 17 |
# File 'lib/cafe_car/policy.rb', line 15 def title_attribute @title_attribute ||= attributes.displayable.first end |