Class: Reeve::Authorization::Scoper

Inherits:
Object
  • Object
show all
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

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.

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


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.

Raises:



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