Class: Reeve::Authorization::Scoper
- Inherits:
-
Object
- Object
- Reeve::Authorization::Scoper
- Defined in:
- lib/reeve/authorization/scoper.rb
Overview
Narrows whatever a tool returned to what the principal may actually see, and reports
what it narrowed for the ledger. Every row of the return-value table in
contracts/tool-dsl.md is one branch of scope.
The envelope reads records out of this result rather than out of the tool's return value, so "nothing is returned without being scoped" is structural.
Class Method Summary collapse
-
.base_class(record_class) ⇒ Object
Single-table inheritance:
TextCommentis governed byCommentPolicy, because the policy is written for the table, not for each subclass. - .base_class_name(record_class) ⇒ Object
- .countable_size(scoped) ⇒ Object
-
.declared_for_other_type?(declaration, record_class) ⇒ Boolean
True only when the declaration names a different model that actually exists —
InvoicePolicyon a tool that also returnedMemos. - .model_class(model_or_relation) ⇒ Object
-
.model_like?(target) ⇒ Boolean
A model, not just any class: something records are actually fetched from.
-
.policy_for(adapter, declaration, record_class) ⇒ Object
The declared policy governs, full stop — a declaration is an instruction, not a hint.
-
.policy_named_for?(policy, record_class) ⇒ Boolean
InvoicePolicyis named forInvoice. - .resolve_constant(name) ⇒ Object
-
.scoped_relation(state, model_or_relation) ⇒ Object
Called by
Reeve::Guard#scopedfrom inside a tool body.
Instance Method Summary collapse
Class Method Details
.base_class(record_class) ⇒ Object
Single-table inheritance: TextComment is governed by CommentPolicy, because
the policy is written for the table, not for each subclass. Comparing subclasses
directly denied every STI result with unknown_record_type.
82 83 84 85 86 87 88 89 |
# File 'lib/reeve/authorization/scoper.rb', line 82 def self.base_class(record_class) return record_class unless record_class.is_a?(Class) return record_class unless record_class.respond_to?(:base_class) record_class.base_class rescue StandardError record_class end |
.base_class_name(record_class) ⇒ Object
97 98 99 |
# File 'lib/reeve/authorization/scoper.rb', line 97 def self.base_class_name(record_class) base_class(record_class).name.to_s end |
.countable_size(scoped) ⇒ Object
108 109 110 111 112 |
# File 'lib/reeve/authorization/scoper.rb', line 108 def self.countable_size(scoped) scoped.respond_to?(:count) ? scoped.count : nil rescue StandardError nil end |
.declared_for_other_type?(declaration, record_class) ⇒ Boolean
True only when the declaration names a different model that actually exists —
InvoicePolicy on a tool that also returned Memos. A policy whose name matches
no model (LeakyPolicy, ApplicationPolicy) is generic, and governs whatever the
tool it was declared on returns.
"Exists" means is a model, not merely "is a defined constant". Asking
Object.const_defined? alone handed DataPolicy, SetPolicy, FilePolicy and
every anonymous policy class (whose name falls back to "Class") to a
convention-named policy instead, because Data, Set, File and Class are all
defined in Ruby — the same silent substitution this method exists to prevent.
55 56 57 58 59 60 61 62 63 |
# File 'lib/reeve/authorization/scoper.rb', line 55 def self.declared_for_other_type?(declaration, record_class) named = declaration.policy_name.to_s.sub(/Policy\z/, "") return false if named.empty? || named == base_class_name(record_class) target = resolve_constant(named) return false unless model_like?(target) target != base_class(record_class) end |
.model_class(model_or_relation) ⇒ Object
101 102 103 104 105 106 |
# File 'lib/reeve/authorization/scoper.rb', line 101 def self.model_class(model_or_relation) return model_or_relation if model_or_relation.is_a?(Class) return model_or_relation.klass if model_or_relation.respond_to?(:klass) model_or_relation.class end |
.model_like?(target) ⇒ Boolean
A model, not just any class: something records are actually fetched from.
72 73 74 75 76 77 |
# File 'lib/reeve/authorization/scoper.rb', line 72 def self.model_like?(target) return false unless target.is_a?(Class) return true if defined?(::ActiveRecord::Base) && target <= ::ActiveRecord::Base target.respond_to?(:all) end |
.policy_for(adapter, declaration, record_class) ⇒ Object
The declared policy governs, full stop — a declaration is an instruction, not a hint. Only a result mixing several types needs more than one policy, and only the types the declaration is not named for are resolved by convention; a type with no policy denies the call (unknown_record_type).
Inferring a policy from the record's class in the single-type case would mean a
tool declaring guard_with LeakyPolicy was silently enforced by InvoicePolicy:
the guard the developer declared would not be the guard that ran.
38 39 40 41 42 43 |
# File 'lib/reeve/authorization/scoper.rb', line 38 def self.policy_for(adapter, declaration, record_class) return declaration.policy if record_class.nil? return declaration.policy unless declared_for_other_type?(declaration, record_class) adapter.policy_for(record_class) || nil end |
.policy_named_for?(policy, record_class) ⇒ Boolean
InvoicePolicy is named for Invoice.
92 93 94 95 |
# File 'lib/reeve/authorization/scoper.rb', line 92 def self.policy_named_for?(policy, record_class) name = policy.respond_to?(:name) && policy.name ? policy.name : policy.class.name name.to_s.sub(/Policy\z/, "") == base_class_name(record_class) end |
.resolve_constant(name) ⇒ Object
65 66 67 68 69 |
# File 'lib/reeve/authorization/scoper.rb', line 65 def self.resolve_constant(name) Object.const_defined?(name) ? Object.const_get(name) : nil rescue NameError nil end |
.scoped_relation(state, model_or_relation) ⇒ Object
Called by Reeve::Guard#scoped from inside a tool body. Marks the invocation as
having asked for a scoped relation, which is what makes a derived return value
(a count, a sum) safe to allow.
15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# File 'lib/reeve/authorization/scoper.rb', line 15 def self.scoped_relation(state, model_or_relation) adapter = state.adapter declaration = state.declaration policy = policy_for(adapter, declaration, model_class(model_or_relation)) raise Error, "no policy for #{model_or_relation.inspect}" if policy.nil? relation = model_or_relation.respond_to?(:all) ? model_or_relation.all : model_or_relation scoped = adapter.scope(principal: state.context.principal, policy: policy, relation: relation) state.scoped_used! state.scoped_source_count = countable_size(scoped) scoped end |
Instance Method Details
#scope(context:, guard:, result:) ⇒ Object
114 115 116 117 118 119 120 121 122 123 |
# File 'lib/reeve/authorization/scoper.rb', line 114 def scope(context:, guard:, result:) state = Current.state adapter = state ? state.adapter : Adapter.resolve(guard.policy) case result when nil then ScopeResult.allow(records: result, record_count: 0) else dispatch(context: context, guard: guard, result: result, adapter: adapter, state: state) end end |