reeve 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +191 -0
- data/README.md +280 -17
- data/lib/generators/reeve/install/install_generator.rb +46 -0
- data/lib/generators/reeve/install/templates/create_audit_entries.rb.tt +77 -0
- data/lib/generators/reeve/install/templates/initializer.rb.tt +65 -0
- data/lib/reeve/audit/entry.rb +54 -0
- data/lib/reeve/audit/query.rb +84 -0
- data/lib/reeve/audit/recorder.rb +181 -0
- data/lib/reeve/audit/redactor.rb +67 -0
- data/lib/reeve/audit.rb +97 -0
- data/lib/reeve/authorization/adapter.rb +71 -0
- data/lib/reeve/authorization/adapters/plain.rb +65 -0
- data/lib/reeve/authorization/adapters/pundit.rb +97 -0
- data/lib/reeve/authorization/authorizer.rb +23 -0
- data/lib/reeve/authorization/current.rb +66 -0
- data/lib/reeve/authorization/declaration.rb +73 -0
- data/lib/reeve/authorization/guard.rb +97 -0
- data/lib/reeve/authorization/registry.rb +118 -0
- data/lib/reeve/authorization/scoper.rb +399 -0
- data/lib/reeve/authorization.rb +76 -0
- data/lib/reeve/configuration.rb +158 -0
- data/lib/reeve/context.rb +111 -0
- data/lib/reeve/decision.rb +111 -0
- data/lib/reeve/errors.rb +89 -0
- data/lib/reeve/fast_mcp.rb +24 -0
- data/lib/reeve/integrations/fast_mcp/context_builder.rb +48 -0
- data/lib/reeve/integrations/fast_mcp/tool_extension.rb +55 -0
- data/lib/reeve/invocation.rb +260 -0
- data/lib/reeve/minitest.rb +19 -0
- data/lib/reeve/rspec.rb +18 -0
- data/lib/reeve/scope_result.rb +104 -0
- data/lib/reeve/testing/assertions.rb +76 -0
- data/lib/reeve/testing/checks/audit_coverage.rb +46 -0
- data/lib/reeve/testing/checks/base.rb +155 -0
- data/lib/reeve/testing/checks/contract_version.rb +97 -0
- data/lib/reeve/testing/checks/cross_principal_leak.rb +132 -0
- data/lib/reeve/testing/checks/guard_declared.rb +33 -0
- data/lib/reeve/testing/checks/principal_required.rb +60 -0
- data/lib/reeve/testing/checks/redaction_holds.rb +142 -0
- data/lib/reeve/testing/checks/rule_present.rb +55 -0
- data/lib/reeve/testing/checks.rb +95 -0
- data/lib/reeve/testing/compliance_assertions.rb +34 -0
- data/lib/reeve/testing/compliance_suite.rb +23 -0
- data/lib/reeve/testing/ledger.rb +82 -0
- data/lib/reeve/testing/matchers/audit_every_call.rb +39 -0
- data/lib/reeve/testing/matchers/base.rb +84 -0
- data/lib/reeve/testing/matchers/deny_access_for.rb +38 -0
- data/lib/reeve/testing/matchers/pass_reeve_check.rb +32 -0
- data/lib/reeve/testing/matchers.rb +33 -0
- data/lib/reeve/testing/report.rb +58 -0
- data/lib/reeve/testing/result.rb +49 -0
- data/lib/reeve/testing.rb +60 -0
- data/lib/reeve/version.rb +1 -1
- data/lib/reeve.rb +10 -3
- metadata +53 -2
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reeve
|
|
4
|
+
module Authorization
|
|
5
|
+
module Adapters
|
|
6
|
+
# Pundit policies, bridged rather than depended on. Pundit itself is never required
|
|
7
|
+
# by this gem (Constitution IV); this file only speaks its conventions, and reports
|
|
8
|
+
# that it supports nothing when Pundit is absent.
|
|
9
|
+
#
|
|
10
|
+
# class InvoicePolicy
|
|
11
|
+
# def index? = ...
|
|
12
|
+
# class Scope < ApplicationPolicy::Scope
|
|
13
|
+
# def resolve = scope.where(owner: user)
|
|
14
|
+
# end
|
|
15
|
+
# end
|
|
16
|
+
class Pundit
|
|
17
|
+
def self.pundit_loaded?
|
|
18
|
+
defined?(::Pundit) ? true : false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.supports?(policy)
|
|
22
|
+
return false unless pundit_loaded?
|
|
23
|
+
return false unless policy.is_a?(Class)
|
|
24
|
+
|
|
25
|
+
scope_defined?(policy) &&
|
|
26
|
+
policy.public_instance_methods.any? { |method| method.to_s.end_with?("?") }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Pundit policies ordinarily inherit their Scope from a base policy
|
|
30
|
+
# (`class LeadPolicy < LeadBasePolicy`), so asking only the policy's own namespace
|
|
31
|
+
# rejected the common case. This walks the ancestry instead, stopping before
|
|
32
|
+
# Object — which is the whole reason the check was narrow to begin with: a
|
|
33
|
+
# top-level `Scope` constant must never make an unrelated class look
|
|
34
|
+
# Pundit-shaped. `policy::Scope` resolves along the same ancestry, so what
|
|
35
|
+
# `supports?` accepts is exactly what `scope` can later reach.
|
|
36
|
+
def self.scope_defined?(policy)
|
|
37
|
+
policy_ancestry(policy).any? { |ancestor| ancestor.const_defined?(:Scope, false) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.policy_ancestry(policy)
|
|
41
|
+
policy.ancestors.take_while { |ancestor| ancestor != Object }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.missing_methods(policy)
|
|
45
|
+
return [] if supports?(policy)
|
|
46
|
+
return [:Scope] if policy.is_a?(Class) && !scope_defined?(policy)
|
|
47
|
+
|
|
48
|
+
%i[query_method Scope]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def authorize(principal:, policy:, action:, record: nil)
|
|
52
|
+
query = "#{action}?"
|
|
53
|
+
rule = "#{policy.name}##{query}"
|
|
54
|
+
subject = record || inferred_subject(policy)
|
|
55
|
+
|
|
56
|
+
if policy.new(principal,
|
|
57
|
+
subject).public_send(query)
|
|
58
|
+
Decision.allow(rule: rule)
|
|
59
|
+
else
|
|
60
|
+
Decision.deny(rule: rule)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def scope(principal:, policy:, relation:)
|
|
65
|
+
scoped = policy::Scope.new(principal, relation).resolve
|
|
66
|
+
return scoped unless scoped.nil?
|
|
67
|
+
|
|
68
|
+
raise Error, "#{scope_rule(policy)} returned nil; a scope must return a relation"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def scope_rule(policy)
|
|
72
|
+
"#{policy.name}::Scope"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Pundit's own convention, which is what makes per-type scoping of a mixed result
|
|
76
|
+
# possible at all: Invoice => InvoicePolicy.
|
|
77
|
+
def policy_for(record_class)
|
|
78
|
+
name = "#{record_class.name}Policy"
|
|
79
|
+
Object.const_defined?(name) ? Object.const_get(name) : nil
|
|
80
|
+
rescue NameError
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# Pundit policies are constructed with a record; for an index-style check there is
|
|
87
|
+
# no record yet, so the class stands in — the same thing `authorize Invoice` does.
|
|
88
|
+
def inferred_subject(policy)
|
|
89
|
+
name = policy.name.to_s.sub(/Policy\z/, "")
|
|
90
|
+
Object.const_defined?(name) ? Object.const_get(name) : nil
|
|
91
|
+
rescue NameError
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reeve
|
|
4
|
+
module Authorization
|
|
5
|
+
# The envelope's `authorizer` collaborator: turns a guard declaration plus a principal
|
|
6
|
+
# into a Decision, through whichever adapter the policy speaks.
|
|
7
|
+
#
|
|
8
|
+
# It deliberately does not rescue. The envelope converts a raising policy into
|
|
9
|
+
# `policy_error` and fails closed; swallowing it here would hide which policy broke.
|
|
10
|
+
class Authorizer
|
|
11
|
+
def authorize(context:, guard:)
|
|
12
|
+
adapter = Adapter.resolve(guard.policy)
|
|
13
|
+
|
|
14
|
+
adapter.authorize(
|
|
15
|
+
principal: context.principal,
|
|
16
|
+
policy: guard.policy,
|
|
17
|
+
action: guard.action,
|
|
18
|
+
record: nil
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reeve
|
|
4
|
+
module Authorization
|
|
5
|
+
# The invocation in progress on this thread.
|
|
6
|
+
#
|
|
7
|
+
# `scoped(Model)` is called from inside a tool body, which has no reference to the
|
|
8
|
+
# envelope, so the envelope's caller publishes the state here and clears it in an
|
|
9
|
+
# ensure. Fiber-local storage (Thread#[]) rather than thread-global, so concurrent
|
|
10
|
+
# invocations — the normal case for an MCP server — cannot see each other's principal.
|
|
11
|
+
module Current
|
|
12
|
+
KEY = :reeve_current_invocation
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def state
|
|
17
|
+
Thread.current[KEY]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def start(context:, declaration:, adapter:)
|
|
21
|
+
previous = state
|
|
22
|
+
Thread.current[KEY] =
|
|
23
|
+
State.new(context: context, declaration: declaration, adapter: adapter)
|
|
24
|
+
previous
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def finish(previous = nil)
|
|
28
|
+
Thread.current[KEY] = previous
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def with(context:, declaration:, adapter:)
|
|
32
|
+
previous = start(context: context, declaration: declaration, adapter: adapter)
|
|
33
|
+
yield(state)
|
|
34
|
+
ensure
|
|
35
|
+
finish(previous)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def active?
|
|
39
|
+
!state.nil?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Mutable for exactly two reasons: recording that `scoped` was used, and the size of
|
|
43
|
+
# what it scoped. Everything else about an invocation is fixed when it starts.
|
|
44
|
+
class State
|
|
45
|
+
attr_reader :context, :declaration, :adapter
|
|
46
|
+
attr_accessor :scoped_source_count
|
|
47
|
+
|
|
48
|
+
def initialize(context:, declaration:, adapter:)
|
|
49
|
+
@context = context
|
|
50
|
+
@declaration = declaration
|
|
51
|
+
@adapter = adapter
|
|
52
|
+
@scoped_used = false
|
|
53
|
+
@scoped_source_count = nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def scoped_used!
|
|
57
|
+
@scoped_used = true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def scoped_used?
|
|
61
|
+
@scoped_used
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reeve
|
|
4
|
+
module Authorization
|
|
5
|
+
# What `guard_with` records for one tool class: which policy governs it, which action
|
|
6
|
+
# to check, and which of its arguments never reach the ledger in the clear.
|
|
7
|
+
class Declaration
|
|
8
|
+
attr_reader :tool_class, :policy, :action, :redacted_arguments
|
|
9
|
+
|
|
10
|
+
def initialize(tool_class:, policy:, action:, redacted_arguments: [])
|
|
11
|
+
@tool_class = tool_class
|
|
12
|
+
@policy = policy
|
|
13
|
+
@action = action.to_sym
|
|
14
|
+
@redacted_arguments = redacted_arguments.map(&:to_sym).freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Resolved on first use rather than at construction: `guard_with` runs inside the
|
|
18
|
+
# class body, and a class defined as `Class.new { ... }` has no name until the
|
|
19
|
+
# constant it is assigned to exists.
|
|
20
|
+
def tool_name
|
|
21
|
+
@tool_name ||= derive_tool_name
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def with(policy: self.policy, action: self.action,
|
|
25
|
+
redacted_arguments: self.redacted_arguments)
|
|
26
|
+
self.class.new(
|
|
27
|
+
tool_class: tool_class, policy: policy, action: action,
|
|
28
|
+
redacted_arguments: redacted_arguments
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def for_subclass(subclass)
|
|
33
|
+
self.class.new(
|
|
34
|
+
tool_class: subclass, policy: policy, action: action,
|
|
35
|
+
redacted_arguments: redacted_arguments
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def policy_name
|
|
40
|
+
policy.respond_to?(:name) && policy.name ? policy.name : policy.class.name
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# The name the envelope looks a guard up by. MCP server libraries let a tool name
|
|
46
|
+
# itself; when it does, that name wins, because that is the name the protocol —
|
|
47
|
+
# and therefore the ledger — will use.
|
|
48
|
+
def derive_tool_name
|
|
49
|
+
declared = tool_class.respond_to?(:tool_name) ? tool_class.tool_name : nil
|
|
50
|
+
return declared.to_s unless declared.nil? || declared.to_s.empty?
|
|
51
|
+
|
|
52
|
+
name = tool_class.name
|
|
53
|
+
if name.nil? || name.empty?
|
|
54
|
+
raise ConfigurationError,
|
|
55
|
+
"#{tool_class.inspect} has no name, so reeve cannot identify it in the " \
|
|
56
|
+
"ledger. Assign it to a constant, or define `def self.tool_name`."
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
underscore(name)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# ActiveSupport is not a dependency of the core, so this is deliberately small:
|
|
63
|
+
# it handles the CamelCase and Namespaced::CamelCase shapes tool classes actually
|
|
64
|
+
# have, and nothing more.
|
|
65
|
+
def underscore(name)
|
|
66
|
+
name.gsub("::", "_")
|
|
67
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
68
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
69
|
+
.downcase
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reeve
|
|
4
|
+
# The tool-side surface: two macros and one helper. `include Reeve::Guard` in a tool
|
|
5
|
+
# class (the fast-mcp adapter does it for you) and declare which policy governs it.
|
|
6
|
+
#
|
|
7
|
+
# class InvoiceSearchTool
|
|
8
|
+
# include Reeve::Guard
|
|
9
|
+
# guard_with InvoicePolicy
|
|
10
|
+
# redact :customer_ssn
|
|
11
|
+
#
|
|
12
|
+
# def call(query:)
|
|
13
|
+
# Invoice.where("number LIKE ?", "%#{query}%")
|
|
14
|
+
# end
|
|
15
|
+
# end
|
|
16
|
+
module Guard
|
|
17
|
+
def self.included(base)
|
|
18
|
+
base.extend(ClassMethods)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Class-level DSL. See contracts/tool-dsl.md.
|
|
22
|
+
module ClassMethods
|
|
23
|
+
# Declares which policy governs this tool. Absence is not neutral: a tool with no
|
|
24
|
+
# declaration is denied by the envelope (FR-002, FR-004).
|
|
25
|
+
def guard_with(policy, action: nil)
|
|
26
|
+
Authorization::Adapter.validate!(policy)
|
|
27
|
+
|
|
28
|
+
@reeve_guard = Reeve.registry.register(
|
|
29
|
+
tool_class: self,
|
|
30
|
+
policy: policy,
|
|
31
|
+
action: action,
|
|
32
|
+
redacted_arguments: pending_redactions + inherited_redactions
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Argument names this tool never writes to the ledger in the clear, on top of the
|
|
37
|
+
# process-wide list (FR-011).
|
|
38
|
+
def redact(*names)
|
|
39
|
+
symbols = names.flatten.map(&:to_sym)
|
|
40
|
+
@pending_redactions = pending_redactions | symbols
|
|
41
|
+
|
|
42
|
+
declaration = reeve_guard
|
|
43
|
+
return symbols if declaration.nil?
|
|
44
|
+
|
|
45
|
+
@reeve_guard = Reeve.registry.add(
|
|
46
|
+
declaration.with(redacted_arguments: declaration.redacted_arguments | symbols)
|
|
47
|
+
)
|
|
48
|
+
symbols
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# This tool's declaration, inherited from a superclass when it has none of its own.
|
|
52
|
+
def reeve_guard
|
|
53
|
+
Reeve.registry.for_class(self)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def guarded?
|
|
57
|
+
!reeve_guard.nil?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# A subclass of a guarded tool is itself guarded, and is registered under its own
|
|
61
|
+
# name so the envelope — which only ever has a name — can find it.
|
|
62
|
+
def inherited(subclass)
|
|
63
|
+
super
|
|
64
|
+
declaration = reeve_guard
|
|
65
|
+
# An anonymous subclass has no name to be looked up by; it still inherits the
|
|
66
|
+
# declaration through the ancestry walk in Registry#for_class.
|
|
67
|
+
return if declaration.nil? || subclass.name.nil?
|
|
68
|
+
|
|
69
|
+
Reeve.registry.add(declaration.for_subclass(subclass))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def pending_redactions
|
|
73
|
+
@pending_redactions ||= []
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def inherited_redactions
|
|
79
|
+
return [] unless superclass.respond_to?(:reeve_guard)
|
|
80
|
+
|
|
81
|
+
inherited = superclass.reeve_guard
|
|
82
|
+
inherited ? inherited.redacted_arguments : []
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The scoped relation for the invoking principal. This is how a guarded tool returns
|
|
87
|
+
# anything that is not a record: a count, a sum, a rendered summary. Computing from
|
|
88
|
+
# `scoped(...)` means the tool never held unscoped data, so the derived value is safe
|
|
89
|
+
# by construction rather than by promise (R4).
|
|
90
|
+
def scoped(model_or_relation)
|
|
91
|
+
state = Authorization::Current.state
|
|
92
|
+
raise Error, "scoped(...) may only be called inside a guarded invocation" if state.nil?
|
|
93
|
+
|
|
94
|
+
Authorization::Scoper.scoped_relation(state, model_or_relation)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Reeve — per-record authorization and an append-only audit ledger for MCP tools.
|
|
4
|
+
module Reeve
|
|
5
|
+
module Authorization
|
|
6
|
+
# Every `guard_with` declaration in the process, keyed by tool class.
|
|
7
|
+
#
|
|
8
|
+
# Two lookups matter: by class, which the DSL and inheritance use, and by tool name,
|
|
9
|
+
# which is all the envelope has. Enumeration is what lets the compliance suite ask the
|
|
10
|
+
# question that matters — "is every tool this application exposes actually guarded?"
|
|
11
|
+
class Registry
|
|
12
|
+
include Enumerable
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@declarations = {}
|
|
16
|
+
@name_index = nil # invalidated on every add; see #name_index
|
|
17
|
+
@mutex = Mutex.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# The DSL's `guard_with`. Declaring twice on one class is a mistake worth naming;
|
|
21
|
+
# `add` is the quiet path used by `redact` and by inheritance, which refine an
|
|
22
|
+
# existing declaration rather than compete with it.
|
|
23
|
+
def register(tool_class:, policy:, action: nil, redacted_arguments: [])
|
|
24
|
+
warn_about_redeclaration_of(tool_class)
|
|
25
|
+
|
|
26
|
+
declaration = Declaration.new(
|
|
27
|
+
tool_class: tool_class,
|
|
28
|
+
policy: policy,
|
|
29
|
+
action: action || Reeve.config.default_action,
|
|
30
|
+
redacted_arguments: redacted_arguments
|
|
31
|
+
)
|
|
32
|
+
add(declaration)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def add(declaration)
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
@declarations[declaration.tool_class] = declaration
|
|
38
|
+
@name_index = nil
|
|
39
|
+
declaration
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Walks the ancestry, so a subclass inherits its parent's guard and may override it
|
|
44
|
+
# simply by declaring its own.
|
|
45
|
+
def for_class(tool_class)
|
|
46
|
+
return nil unless tool_class.respond_to?(:ancestors)
|
|
47
|
+
|
|
48
|
+
tool_class.ancestors.each do |ancestor|
|
|
49
|
+
declaration = @declarations[ancestor]
|
|
50
|
+
return declaration if declaration
|
|
51
|
+
end
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The envelope's lookup. Returns nil for an unknown tool, which is a denial.
|
|
56
|
+
def guard_for(tool_name)
|
|
57
|
+
name_index[tool_name.to_s]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def each(&block)
|
|
61
|
+
@declarations.values.each(&block)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def size
|
|
65
|
+
@declarations.size
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def empty?
|
|
69
|
+
@declarations.empty?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Forgets one tool. Test suites build throwaway tools, and the compliance suite
|
|
73
|
+
# walks this registry — a fixture left behind fails a later, unrelated example.
|
|
74
|
+
def remove(tool_class)
|
|
75
|
+
@mutex.synchronize do
|
|
76
|
+
@declarations.delete(tool_class)
|
|
77
|
+
@name_index = nil
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def reset!
|
|
82
|
+
@mutex.synchronize do
|
|
83
|
+
@declarations = {}
|
|
84
|
+
@name_index = nil
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def name_index
|
|
91
|
+
@name_index ||= @declarations.values.to_h do |declaration|
|
|
92
|
+
[declaration.tool_name, declaration]
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def warn_about_redeclaration_of(tool_class)
|
|
97
|
+
return unless @declarations.key?(tool_class)
|
|
98
|
+
|
|
99
|
+
message = "reeve: #{tool_class} declared guard_with more than once; " \
|
|
100
|
+
"the later declaration replaces the earlier one"
|
|
101
|
+
logger = Reeve.config.logger
|
|
102
|
+
logger ? logger.warn(message) : Kernel.warn(message)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
class << self
|
|
108
|
+
# The process-wide registry the DSL writes to.
|
|
109
|
+
def registry
|
|
110
|
+
@registry ||= Authorization::Registry.new
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Public so host test suites can isolate examples from one another.
|
|
114
|
+
def reset_registry!
|
|
115
|
+
registry.reset!
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|