Module: Legion::Extensions::Node::ControlAuth
- Defined in:
- lib/legion/extensions/node/control_auth.rb
Defined Under Namespace
Classes: UnauthorizedControlMessage
Constant Summary
collapse
- AUTH_MODES =
%w[auto required disabled].freeze
Class Method Summary
collapse
Class Method Details
.auth_disabled? ⇒ Boolean
80
81
82
|
# File 'lib/legion/extensions/node/control_auth.rb', line 80
def auth_disabled?
auth_mode == 'disabled'
end
|
.auth_mode ⇒ Object
66
67
68
69
70
71
72
73
74
|
# File 'lib/legion/extensions/node/control_auth.rb', line 66
def auth_mode
configured = auth_settings[:mode] || auth_settings[:enabled]
mode = case configured
when true then 'required'
when false then 'disabled'
else configured.to_s
end
AUTH_MODES.include?(mode) ? mode : 'auto'
end
|
.auth_required? ⇒ Boolean
76
77
78
|
# File 'lib/legion/extensions/node/control_auth.rb', line 76
def auth_required?
auth_mode == 'required'
end
|
.canonical_json(value) ⇒ Object
110
111
112
|
# File 'lib/legion/extensions/node/control_auth.rb', line 110
def canonical_json(value)
::JSON.generate(canonicalize(value))
end
|
.canonicalize(value) ⇒ Object
114
115
116
117
118
119
120
121
122
123
124
125
|
# File 'lib/legion/extensions/node/control_auth.rb', line 114
def canonicalize(value)
case value
when Hash
value.each_with_object({}) do |(key, val), result|
result[key.to_s] = canonicalize(val)
end.sort.to_h
when Array
value.map { |item| canonicalize(item) }
else
value
end
end
|
92
93
94
95
96
97
98
99
100
101
|
# File 'lib/legion/extensions/node/control_auth.rb', line 92
def configured_secret
if defined?(Legion::Settings) && Legion::Settings.respond_to?(:dig)
auth_settings[:secret] ||
Legion::Settings.dig(:cluster, :control_secret) ||
Legion::Settings.dig(:crypt, :cluster_secret)
end
rescue StandardError => e
log.debug("control secret lookup failed: #{e.message}")
nil
end
|
.deep_symbolize(value) ⇒ Object
127
128
129
130
131
132
133
134
135
136
137
138
|
# File 'lib/legion/extensions/node/control_auth.rb', line 127
def deep_symbolize(value)
case value
when Hash
value.each_with_object({}) do |(key, val), result|
result[key.to_sym] = deep_symbolize(val)
end
when Array
value.map { |item| deep_symbolize(item) }
else
value
end
end
|
.fresh_timestamp?(timestamp) ⇒ Boolean
140
141
142
143
144
145
146
|
# File 'lib/legion/extensions/node/control_auth.rb', line 140
def fresh_timestamp?(timestamp)
ts = Integer(timestamp)
(Time.now.utc.to_i - ts).abs <= timestamp_skew_seconds
rescue ArgumentError, TypeError => e
log.debug("control timestamp validation failed: #{e.message}")
false
end
|
.log ⇒ Object
180
181
182
|
# File 'lib/legion/extensions/node/control_auth.rb', line 180
def log
Legion::Logging.respond_to?(:logger) ? Legion::Logging.logger : Legion::Logging
end
|
.node_name ⇒ Object
173
174
175
176
177
178
|
# File 'lib/legion/extensions/node/control_auth.rb', line 173
def node_name
(Legion::Settings[:client]&.fetch(:name, nil) if defined?(Legion::Settings)) || 'unknown'
rescue StandardError => e
log.debug("node name lookup failed: #{e.message}")
'unknown'
end
|
.nonce_bytes ⇒ Object
155
156
157
158
159
160
161
|
# File 'lib/legion/extensions/node/control_auth.rb', line 155
def nonce_bytes
bytes = Integer(auth_settings[:nonce_bytes])
bytes.positive? ? bytes : 16
rescue ArgumentError, TypeError => e
log.debug("control nonce byte setting invalid: #{e.message}")
16
end
|
.secret ⇒ Object
88
89
90
|
# File 'lib/legion/extensions/node/control_auth.rb', line 88
def secret
configured_secret || ENV.fetch('LEGION_NODE_CONTROL_SECRET', nil)
end
|
.secure_compare(left, right) ⇒ Object
167
168
169
170
171
|
# File 'lib/legion/extensions/node/control_auth.rb', line 167
def secure_compare(left, right)
return false unless left.bytesize == right.bytesize
left.bytes.zip(right.bytes).reduce(0) { |acc, (a, b)| acc | (a ^ b) }.zero?
end
|
.sign(payload) ⇒ Object
18
19
20
21
22
23
24
25
26
27
28
29
|
# File 'lib/legion/extensions/node/control_auth.rb', line 18
def sign(payload)
normalized = deep_symbolize(payload).dup
return normalized unless sign_control_messages?
control = {
sender: node_name,
timestamp: Time.now.utc.to_i,
nonce: SecureRandom.hex(nonce_bytes)
}
normalized[:control] = control.merge(signature: signature_for(normalized.merge(control: control)))
normalized
end
|
.sign_control_messages? ⇒ Boolean
52
53
54
55
56
57
|
# File 'lib/legion/extensions/node/control_auth.rb', line 52
def sign_control_messages?
return false if auth_disabled?
return true if auth_required?
!secret.to_s.empty?
end
|
.signature_for(payload) ⇒ Object
103
104
105
106
107
108
|
# File 'lib/legion/extensions/node/control_auth.rb', line 103
def signature_for(payload)
value = secret
raise UnauthorizedControlMessage, 'cluster control secret is not configured' if value.to_s.empty?
OpenSSL::HMAC.hexdigest('SHA256', value.to_s, canonical_json(payload))
end
|
.timestamp_skew_seconds ⇒ Object
148
149
150
151
152
153
|
# File 'lib/legion/extensions/node/control_auth.rb', line 148
def timestamp_skew_seconds
Integer(auth_settings[:timestamp_skew_seconds])
rescue ArgumentError, TypeError => e
log.debug("control timestamp skew setting invalid: #{e.message}")
300
end
|
.valid_nonce?(nonce) ⇒ Boolean
163
164
165
|
# File 'lib/legion/extensions/node/control_auth.rb', line 163
def valid_nonce?(nonce)
nonce.to_s.match?(/\A[0-9a-f]{#{nonce_bytes * 2}}\z/i)
end
|
.verify!(payload) ⇒ Object
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
# File 'lib/legion/extensions/node/control_auth.rb', line 31
def verify!(payload)
normalized = deep_symbolize(payload)
raise UnauthorizedControlMessage, 'missing control signature' unless normalized.is_a?(Hash)
return normalized unless verify_control_messages?
control = normalized[:control]
raise UnauthorizedControlMessage, 'missing control signature' unless control.is_a?(Hash)
signature = control[:signature].to_s
raise UnauthorizedControlMessage, 'missing control signature' if signature.empty?
raise UnauthorizedControlMessage, 'stale control message' unless fresh_timestamp?(control[:timestamp])
raise UnauthorizedControlMessage, 'invalid control nonce' unless valid_nonce?(control[:nonce])
unsigned_control = control.dup
unsigned_control.delete(:signature)
expected = signature_for(normalized.merge(control: unsigned_control))
raise UnauthorizedControlMessage, 'invalid control signature' unless secure_compare(signature, expected)
normalized
end
|
.verify_control_messages? ⇒ Boolean
59
60
61
62
63
64
|
# File 'lib/legion/extensions/node/control_auth.rb', line 59
def verify_control_messages?
return false if auth_disabled?
return true if auth_required?
!secret.to_s.empty?
end
|